Files
Scoreboard/deploy/prepare-extra.mjs
T
AskaEth 9d4ee0946c feat: 运动员彻底删除(真物理删除 + 重算积分)+ 报名表 status 容错 + 部署包带导入规范
## 运动员「彻底删除」= 真删除
`DELETE /api/athletes/{id}?purge=1`(不带参数仍是「停用」软删除)。在一个事务里:
删积分流水(含已作废历史流水)→ 删破纪录登记 → 删成绩 → 删报名 → 删人员 →
对受影响的 (小项, 组别) 调 recomputeLedgerTx + applyComputedRanks 重算流水与名次 →
bumpEvent/bumpStandings 推大屏 → 审计 athlete.purge(含删除规模)。
响应带 {deletedEntries, deletedResults, deletedLedger, deletedBreaks, recomputedEvents}。
必须先删 results 再删 entries(results.entry_id 无 ON DELETE);流水按 athlete_id / entry_id /
result_id 三种引用一把清,漏一种就留悬挂。

管理端「运动员」行加「彻底删除」按钮 + 危险确认框(如实写明报名与成绩一并删除、
相关小项排名与积分会重算),成功 toast 报出删除规模。

## 报名表 status 列容错
新增 entry_status.go 的 normalizeImportStatus:大小写不敏感(DNS/Registered 都认),
认「正常/退赛/弃赛/请假/取消资格/犯规/未出场/未到/缺席」等中文别名,留空即 registered,
认不出则给中文行级错误并跳过该行。此前原样入库,撞 entries.status 的 CHECK 约束后
抛原始 SQLite 错误,填表人看不懂。

## 修 isUniqueViolation 把 CHECK 失败误报成「已存在」
SQLite 的 CHECK / NOT NULL 失败也带 "constraint failed",旧匹配把「小项 status 取值非法」
报成 409「小项编码已存在」,现场会去找一个根本不存在的重名编码。收紧为只认
"unique constraint failed"。实测:重复编码仍 409;非法 status 不再误报。

## 安装包平铺《报名名单导入规范》
deploy/prepare-extra.mjs 改清单式,部署包根目录除 使用说明.txt 外新增
报名名单导入规范.txt。

测试:服务端 12 包全绿(新增 athlete_purge_test.go、entry_status_test.go);
前端 vue-tsc 通过、166 例全过;Console 语法检查 17 文件。
另在真二进制 + 真 SQLite 上跑通破坏性路径:删人后本人痕迹全为 0、他人不受影响、
榜单按新排序重算、foreign_key_check 无悬挂。docs/ 与 devdocs/ 同步更新。
2026-09-10 22:37:25 +08:00

107 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 构建前准备两样东西:
// 1) 把 docs/ 下的使用说明与报名名单导入规范暂存到 console/extra/,供 electron-builder 的 extraFiles 平铺到部署包根目录;
// 2) 生成安装器自定义脚本(console/resources/installer-custom.nsh):许可页顶部文案(署名来自构建机本地文件)
// + 记住上次安装目录(重装时沿用,而不是退回系统默认路径)。
// 用 Node 而不是批处理:中文文件名/中文内容在 .bat 里会被 cmd 解析错乱。
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
const root = dirname(import.meta.dirname)
// ---- 1. usage docs ----------------------------------------------------------
// 都暂存成 .txt:部署包根目录里给现场双击打开,.md 在记事本里全是符号。
// 源文件优先取 docs/ 下同名 .txt(便于临时替换),否则用仓库里的 .md。
const extraDir = join(root, 'console', 'extra')
mkdirSync(extraDir, { recursive: true })
const docsToStage = [
{ target: '使用说明.txt', sources: ['使用说明.txt', '使用手册.md'], label: 'usage doc' },
{ target: '报名名单导入规范.txt', sources: ['报名名单导入规范.md'], label: 'entry import spec' },
]
for (const doc of docsToStage) {
const source = doc.sources.map((name) => join(root, 'docs', name)).find((file) => existsSync(file))
if (!source) {
console.log(` [docs] no ${doc.label}, skip`)
continue
}
copyFileSync(source, join(extraDir, doc.target))
console.log(` ${doc.label} staged from ${source}`)
}
// ---- 2. installer custom script ---------------------------------------------
// electron-builder 的 nsis.include 只收一个文件,所以许可页文案与安装目录记忆写在同一个 .nsh 里。
const signatureFile = join(root, 'deploy', 'signature.local.txt')
const nshDir = join(root, 'console', 'resources')
const nshFile = join(nshDir, 'installer-custom.nsh')
const legacyNshFile = join(nshDir, 'installer-signature.nsh')
mkdirSync(nshDir, { recursive: true })
let topText = '请阅读并接受许可协议'
if (existsSync(signatureFile)) {
const signature = readFileSync(signatureFile, 'utf8').trim()
if (signature !== '') topText = `内部署名:${signature}`
}
// 换行用 CRLF:.gitattributes 里 *.nsh 就是 CRLF,NSIS 在 Windows 上按CRLF 读最稳。
const nsh = [
'!include "LogicLib.nsh"',
'',
'; 安装器自定义脚本:由 deploy/prepare-extra.mjs 生成,不要手改。',
'; 内容只有三块:许可页文案、上次安装目录记忆、卸载钩子。',
'',
'; 自己记的安装目录(per-user 落 HKCU,per-all-users 落 HKLM)。',
'; electron-builder 自带的 ${INSTALL_REGISTRY_KEY} 在卸载时会被整键删掉,',
'; 「先卸载再装新版」或换过安装模式就会退回系统默认路径,所以另存一份不会被卸载器清理的。',
'!define SCOREBOARD_INSTALL_KEY "Software\\Scoreboard Console"',
'',
'!macro customHeader',
` !define MUI_LICENSEPAGE_TEXT_TOP "${topText}"`,
'!macroend',
'',
'!macro customInit',
' ; 在 .onInit 里、initMultiUser 之后执行:此时改 $INSTDIR 还来得及影响后面的目录页。',
' ReadRegStr $R9 HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"',
' ${If} $R9 != ""',
' StrCpy $INSTDIR $R9',
' ; 安装模式页(per-user / per-all-users)离开时,electron-builder 会用自己那个键',
' ; 重新算一遍 $INSTDIR;卸载后它已经被删了,把记忆值补写回同一个 hive 里。',
' ; 不跨 hive 写:否则会把 per-machine 装的机器悄悄带成默认 per-user 模式(反之亦然)。',
' WriteRegStr HKCU "${INSTALL_REGISTRY_KEY}" "InstallLocation" "$R9"',
' ${Else}',
' ReadRegStr $R9 HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"',
' ${If} $R9 != ""',
' StrCpy $INSTDIR $R9',
' WriteRegStr HKLM "${INSTALL_REGISTRY_KEY}" "InstallLocation" "$R9"',
' ${Else}',
' ; 没有自己的记忆时,退回 electron-builder 的键(老版本装过、没卸载过的情况)',
' ReadRegStr $R9 HKCU "${INSTALL_REGISTRY_KEY}" "InstallLocation"',
' ${If} $R9 == ""',
' ReadRegStr $R9 HKLM "${INSTALL_REGISTRY_KEY}" "InstallLocation"',
' ${EndIf}',
' ; 只认非空值;旧目录已经被删掉也照样用(这就是「记住我的路径」的语义)。',
' ${If} $R9 != ""',
' StrCpy $INSTDIR $R9',
' ${EndIf}',
' ${EndIf}',
' ${EndIf}',
' ClearErrors',
'!macroend',
'',
'!macro customInstall',
' ; SHELL_CONTEXT 跟随安装模式:per-user → HKCU,per-all-users → HKLM。',
' WriteRegStr SHELL_CONTEXT "${SCOREBOARD_INSTALL_KEY}" "InstallLocation" "$INSTDIR"',
'!macroend',
'',
'!macro customUnInstall',
' ; 故意什么都不删:需求就是「卸载后重装仍沿用上次的目录」。',
' ; 若改成卸载即遗忘,在这里补两行(卸载时 SHELL_CONTEXT 不一定已设定,要写死根键):',
' ; DeleteRegValue HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"',
' ; DeleteRegValue HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"',
'!macroend',
'',
].join('\r\n')
writeFileSync(nshFile, nsh, 'utf8')
// 改名前生成的旧脚本已经没人引用,构建机上顺手清掉,免得看起来还在用。
rmSync(legacyNshFile, { force: true })
console.log(` installer custom script ready (${existsSync(signatureFile) ? 'with' : 'without'} internal signature)`)