feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)

- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对)
  与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的
  重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端
- export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep),
  自动、手动、发布前三类备份共用同一把互斥锁与滚动清理
- realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行,
  跨站需命中 server.allowedOrigins)
- console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀),
  服务管理器新增「已连接的大屏实例」卡片
- deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空,
  导致最近这次构建的 exe 里根本没注入署名
- docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
This commit is contained in:
AskaEth committed 2026-09-09 21:41:08 +08:00
1 parent 4d56c73ae6
commit 2c9ff65fc4
42 files changed
+1271 -275

No files matched your search

@@ -8,11 +8,8 @@ import PageFrame from '../PageFrame.vue'
const { athletes, settings } = useScoreboard()
const rows = computed(() => athletes.value)
// 奖牌列由独立开关控制;旧配置没有 showIndividualMedals 时回退到「个人榜上屏」开关,避免升级后突然多出一列。
const showMedals = computed(() => {
const standings = settings.value?.standings
return standings?.showIndividualMedals ?? standings?.showIndividualBoard ?? true
})
// 奖牌列由独立开关控制;缺字段时的回退已在 guards.parseSettings 里按「跟随个人榜上屏」处理。
const showMedals = computed(() => settings.value?.standings.showIndividualMedals ?? true)
const rowFont = computed(() => fitRowFont(rows.value.length, 560))
</script>
+13 -14
View File
@@ -296,14 +296,10 @@ export function parseRecordEntry(source: unknown): RecordEntry {
}
/**
* 大屏侧扩展的赛事配置。
*
* `standings.showIndividualMedals` 还没进入 shared 契约,但大屏需要它独立控制奖牌列;
* 声明成可选,旧配置(服务端只给 showIndividualBoard)仍可赋值,页面再回退。
* 大屏侧赛事配置。字段与 shared 契约一致;
* `showIndividualMedals` 缺失时由 parseSettings 回退到 `showIndividualBoard`(旧服务端只下发后者)。
*/
export type DisplaySettings = EventSettings & {
standings: EventSettings['standings'] & { showIndividualMedals?: boolean }
}
export type DisplaySettings = EventSettings
/**
* 嵌套路径取值。
@@ -330,6 +326,10 @@ function settingValue(
export function parseSettings(source: unknown): DisplaySettings {
const rec = record(source)
const showIndividualBoard = bool(
settingValue(rec, 'standings.showIndividualBoard', 'standings.show_individual_board'),
true,
)
return {
event: {
name: str(settingValue(rec, 'event.name', 'event.name')),
@@ -355,13 +355,12 @@ export function parseSettings(source: unknown): DisplaySettings {
settingValue(rec, 'standings.rankingMode', 'standings.ranking_mode'),
'points',
),
showIndividualBoard: bool(
settingValue(rec, 'standings.showIndividualBoard', 'standings.show_individual_board'),
true,
),
showIndividualMedals: boolOrUndefined(
settingValue(rec, 'standings.showIndividualMedals', 'standings.show_individual_medals'),
),
showIndividualBoard,
// 旧服务端只下发 showIndividualBoard,奖牌列跟随它,避免升级后突然多出一列。
showIndividualMedals:
boolOrUndefined(
settingValue(rec, 'standings.showIndividualMedals', 'standings.show_individual_medals'),
) ?? showIndividualBoard,
},
defaultProfileId: numOrNull(rec.defaultProfileId),
requireApproval: bool(rec.requireApproval),
+1
View File
@@ -271,6 +271,7 @@ export function createMockSnapshot(): SnapshotMessage {
individualToTeamWeight: 1,
rankingMode: 'points',
showIndividualBoard: true,
showIndividualMedals: true,
},
defaultProfileId: null,
requireApproval: false,