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:
1 parent
4d56c73ae6
commit
2c9ff65fc4
42 files changed
+1271
-275
No files matched your search
@@ -237,7 +237,12 @@ const SETTINGS: EventSettings = {
|
||||
primaryColor: '#3482FF',
|
||||
rotateSeconds: 15,
|
||||
},
|
||||
standings: { individualToTeamWeight: 1, rankingMode: 'points', showIndividualBoard: true },
|
||||
standings: {
|
||||
individualToTeamWeight: 1,
|
||||
rankingMode: 'points',
|
||||
showIndividualBoard: true,
|
||||
showIndividualMedals: true,
|
||||
},
|
||||
defaultProfileId: 1,
|
||||
requireApproval: false,
|
||||
}
|
||||
|
||||
@@ -184,6 +184,16 @@ async function save(): Promise<void> {
|
||||
<span class="switch-text">{{ form.standings.showIndividualBoard ? '展示' : '隐藏' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="field switch-field">
|
||||
<span class="mx-label">个人榜奖牌列</span>
|
||||
<span class="switch-row">
|
||||
<label class="mx-switch">
|
||||
<input v-model="form.standings.showIndividualMedals" type="checkbox" />
|
||||
<span class="mx-switch-track"><span class="mx-switch-thumb"></span></span>
|
||||
</label>
|
||||
<span class="switch-text">{{ form.standings.showIndividualMedals ? '展示' : '隐藏' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="notice">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type AdjustmentCategory,
|
||||
type PointAdjustment,
|
||||
type RulePack,
|
||||
type RuleSwitches,
|
||||
} from '@/shared'
|
||||
import MxIcon from '../components/MxIcon.vue'
|
||||
import PageHeader from '../components/PageHeader.vue'
|
||||
@@ -32,6 +33,7 @@ const size = 20
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const rulePack = ref<RulePack | null>(null)
|
||||
const ruleSwitches = ref<RuleSwitches | null>(null)
|
||||
const ruleError = ref<string | null>(null)
|
||||
const search = ref('')
|
||||
const categoryFilter = ref<AdjustmentCategory | 'all'>('all')
|
||||
@@ -57,12 +59,12 @@ const canWrite = computed(() => !auth.readOnly && data.canWrite)
|
||||
/**
|
||||
* 人工调整开关:true / false / null(未知)。
|
||||
*
|
||||
* 规则包只有 admin 能读(GET /api/rules/export 是 admin-only),录入员必然 403——
|
||||
* 不能据此断言「已关闭」,那会把已启用的功能谎报成关闭。读不到时为 null,
|
||||
* 界面如实显示「未知」,最终由服务端校验决定是否受理。
|
||||
* 后端补一个 recorder 可读的只读开关端点后,把 loadRules 换成读该端点即可。
|
||||
* 优先取 GET /api/rules/switches(recorder 可读);读不到时再退回完整规则包(admin)。
|
||||
* 两者都没有则为 null,界面如实显示「未知」,最终由服务端校验决定是否受理。
|
||||
*/
|
||||
const adjustmentEnabled = computed<boolean | null>(() => rulePack.value?.manualAdjustment.enabled ?? null)
|
||||
const adjustmentEnabled = computed<boolean | null>(
|
||||
() => ruleSwitches.value?.adjustmentsEnabled ?? rulePack.value?.manualAdjustment.enabled ?? null,
|
||||
)
|
||||
|
||||
const items = computed(() => [...localItems.value, ...serverItems.value])
|
||||
|
||||
@@ -122,6 +124,12 @@ async function loadRules(): Promise<void> {
|
||||
rulePack.value = null
|
||||
ruleError.value = caught instanceof Error ? caught.message : '无法读取规则包'
|
||||
}
|
||||
// 规则包只有 admin 能读;录入员改用只读开关端点,避免把已启用功能误报成关闭。
|
||||
try {
|
||||
ruleSwitches.value = await adminApi.client.rules.switches()
|
||||
} catch {
|
||||
ruleSwitches.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function load(pageNumber = page.value): Promise<void> {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type RecordEntry,
|
||||
type RecordLevel,
|
||||
type RulePack,
|
||||
type RuleSwitches,
|
||||
} from '@/shared'
|
||||
import MxIcon from '../components/MxIcon.vue'
|
||||
import PageHeader from '../components/PageHeader.vue'
|
||||
@@ -64,6 +65,7 @@ const suspected = ref<SuspectedBreak[]>([])
|
||||
const suspectedLoading = ref(false)
|
||||
const suspectedError = ref<string | null>(null)
|
||||
const rulePack = ref<RulePack | null>(null)
|
||||
const ruleSwitches = ref<RuleSwitches | null>(null)
|
||||
const ruleError = ref<string | null>(null)
|
||||
const rulesLoading = ref(false)
|
||||
const search = ref('')
|
||||
@@ -79,12 +81,12 @@ const canWrite = computed(() => !auth.readOnly && data.canWrite)
|
||||
/**
|
||||
* 破纪录加分开关:true / false / null(未知)。
|
||||
*
|
||||
* 规则包只有 admin 能读(GET /api/rules/export 是 admin-only),
|
||||
* 录入员必然 403——不能据此断言「已关闭」,那会把已启用的功能谎报成关闭。
|
||||
* 读不到时为 null,界面如实显示「未知」,最终由服务端校验决定是否加分。
|
||||
* 后端补一个 recorder 可读的只读开关端点后,把 loadRules 换成读该端点即可。
|
||||
* 优先取 GET /api/rules/switches(recorder 可读);读不到时再退回完整规则包(admin)。
|
||||
* 两者都没有则为 null,界面如实显示「未知」,最终由服务端校验决定是否加分。
|
||||
*/
|
||||
const recordBonusEnabled = computed<boolean | null>(() => rulePack.value?.recordBonus.enabled ?? null)
|
||||
const recordBonusEnabled = computed<boolean | null>(
|
||||
() => ruleSwitches.value?.recordBonusEnabled ?? rulePack.value?.recordBonus.enabled ?? null,
|
||||
)
|
||||
const bonusMode = computed(() =>
|
||||
rulePack.value === null ? '—' : rulePack.value.recordBonus.mode === 'sum' ? '多档累加' : '取最高档',
|
||||
)
|
||||
@@ -167,6 +169,12 @@ async function loadRules(): Promise<void> {
|
||||
} finally {
|
||||
rulesLoading.value = false
|
||||
}
|
||||
// 规则包只有 admin 能读;录入员改用只读开关端点,避免把已启用功能误报成关闭。
|
||||
try {
|
||||
ruleSwitches.value = await adminApi.client.rules.switches()
|
||||
} catch {
|
||||
ruleSwitches.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategoryLabels(eventIds: number[]): Promise<void> {
|
||||
|
||||
Reference in new issue
Block a user