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/ 同步更新。
This commit is contained in:
AskaEth committed 2026-09-10 22:37:25 +08:00
1 parent 22ab49e73a
commit 9d4ee0946c
22 files changed
+1264 -54

No files matched your search

+38 -6
View File
@@ -323,9 +323,35 @@
1. 点「新增运动员」,填写姓名 *、性别、年龄、所属单位 *、工号 / 学号、证件号、号码布等(年龄可留空,填了必须是 0–120 的整数);
2. **证件号(如身份证号)与号码布二者取其一**即可唯一标识一个人,用于区分重名;两个人员不能共用同一个证件号,也不能共用同一个号码布,重复会被拒绝;没有身份证信息的人可以只填号码布。已有历史数据两项可以都为空;
3. 「编辑」修改,「停用」后不再出现在新报名选择中;
3. 每行有三个动作:**编辑**、**停用**(停用后按钮变成「启用」)、**彻底删除**;
4. 个人项目的报名项会关联到运动员,团体项目可以留空。
**停用与彻底删除的区别**
| 状态 | 人员列表里 | 还能被报名选中吗 | 他名下的报名 / 成绩 / 积分 | 能恢复吗 |
| --- | --- | --- | --- | --- |
| 启用(默认) | 在 | 能 | 保留 | —— |
| 停用 | 还在(标记「已停用」) | 不能 | **保留** | 点「启用」即可 |
| 彻底删除 | **不在了** | 不能 | **一并删除** | 恢复不了 |
「停用」是软删除,只是不让他再报名;「彻底删除」是**真删除**。操作步骤:
1. 点该行右侧红色的「彻底删除」;
2. 在确认框里核对提示(会写明报名与成绩一并删除、相关小项的排名与积分会重算),再点一次「彻底删除」;
3. 完成后页面给出删除规模,例如「已彻底删除「张伟」:报名 1 条、成绩 1 条、积分流水 2 条,已重算 1 个小项的积分与名次」。
**「彻底删除」会永久删掉什么**
- 这个人的**人员档案**:从运动员名册里消失,报名选择器里也选不到,界面上再也恢复不了;
- 他名下的**报名**与**成绩**;
- 这些成绩产生的**积分流水**(含已作废的历史流水)与**破纪录记录**。
删掉成绩会改变同组其他人的名次与得分,所以系统在同一个动作里**按新排序重算受影响小项的排名与积分**,并把结果推到大屏。因此:
- 已经有成绩的比赛,删人**会让名次和积分发生变化**(例如原来的第二名升到第一、得分跟着变),删完请核对一下榜单;
- **只是想暂时不让他报名**,请用「停用」——它保留全部历史数据,随时可以再启用;
- 该人的姓名也不会再出现在「分项成绩表」「个人积分榜」上:这些榜单是现算的,数据删了他就不在榜里。
### 10.7 报名导入
报名有两种方式,都在本页完成:**从已有名单勾选**适合零散补报,**表格导入**适合批量录入。
@@ -354,15 +380,21 @@
1. 点「下载 CSV 模板」获取表头样例;
2. 表头共 11 列:`event_code`、`category`、`team_code`、`athlete_name`、`id_card`、`gender`、`age`、`bib`、`seed`、`status`、`member_names`(大小写不敏感)。其中 `id_card`(证件号)、`gender`(性别)、`age`(年龄)为可选列,缺失不影响导入,其余列必须包含;
3. **只要有姓名的行,`id_card` 与 `bib` 必须至少填一个**,否则该行报错并跳过;`gender` 只认「男」「女」,其他值忽略;`age` 可留空,填了必须是 0–120 的整数;同一份文件里同一个证件号或同一个号码布重复出现时,重复的行会报错;
4. 人员匹配顺序:
4. **`status` 列很宽容**:大小写不敏感(`DNS`、`Registered`、`registered` 都认),也认中文别名——
- `正常` / `正常参赛` / `参赛` → 正常参赛;
- `退赛` / `弃赛` / `请假` → 退赛;
- `取消资格` / `犯规` → 取消资格;
- `未出场` / `未到` / `缺席` → 未出场。
留空即「正常参赛」。真正认不出的写法会给出中文行级错误、该行跳过,例如「第 12 行:status「已报到」无法识别,只能填 registered / scratched / dq / dns(或留空)」;
5. 人员匹配顺序:
1. 先按 `id_card` 匹配已有人员;
2. 没匹配到,再按 `bib`(号码布)匹配;
3. 还没匹配到,按「姓名 + 单位」匹配且该人员证件号为空,视为同一人,把表里给的证件号 / 号码布 / 年龄补进去(不算冲突);如果该人员已有号码布且与表里不同,则算冲突;
4. 都没匹配到则新建人员(姓名、性别、年龄、单位、证件号、号码布);
5. `bib` 列会同时写入人员档案与报名记录;从已有名单勾选生成的报名也会带上人员的证件号(优先)或号码布;
6. 把 `.csv` 或 `.xlsx` 拖入虚线框,或点击选择文件(单个文件不超过 16MB;xlsx 只解析第一个工作表);
4. 都没匹配到则新建人员(姓名、性别、年龄、单位、证件号、号码布)——被「彻底删除」的人(见 10.6)档案已经不在库里,匹配不到他,再次导入会当作**新人员**重新建档,他以前的报名与成绩不会回来;
6. `bib` 列会同时写入人员档案与报名记录;从已有名单勾选生成的报名也会带上人员的证件号(优先)或号码布;
7. 把 `.csv` 或 `.xlsx` 拖入虚线框,或点击选择文件(单个文件不超过 16MB;xlsx 只解析第一个工作表);
- **CSV 编码不用管**:UTF-8、带 BOM 的 UTF-8(Excel 的「CSV UTF-8」)、GBK / GB18030(中文 Windows 上 Excel「另存为 CSV」的默认编码)、UTF-16 都能直接导入,中文不会乱码,服务端会自动识别;
7. 点「开始导入」,页面显示**新增 / 更新 / 跳过**数量;匹配不到的行会跳过并回显行号,修正后可重新上传。
8. 点「开始导入」,页面显示**新增 / 更新 / 跳过**数量;匹配不到的行会跳过并回显行号,修正后可重新上传。
**人员信息冲突的处理**