## 运动员「彻底删除」= 真删除
`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/ 同步更新。
107 lines
5.5 KiB
JavaScript
107 lines
5.5 KiB
JavaScript
// 构建前准备两样东西:
|
||
// 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)`)
|