## 运动员「彻底删除」= 真删除
`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/ 同步更新。
55 lines
2.0 KiB
Go
55 lines
2.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// status 归一:大小写不敏感、认中文别名、空值按 registered,认不出的必须报错而不是原样入库。
|
|
func TestNormalizeImportStatus(t *testing.T) {
|
|
ok := map[string]string{
|
|
"": "registered",
|
|
" ": "registered",
|
|
"\u3000": "registered", // 只有全角空格
|
|
"registered": "registered",
|
|
"Registered": "registered",
|
|
"REGISTERED": "registered",
|
|
" \u3000registered\u3000 ": "registered",
|
|
"正常": "registered",
|
|
"正常参赛": "registered",
|
|
"参赛": "registered",
|
|
"scratched": "scratched",
|
|
"Scratched": "scratched",
|
|
"退赛": "scratched",
|
|
"弃赛": "scratched",
|
|
"请假": "scratched",
|
|
"dq": "dq",
|
|
"DQ": "dq",
|
|
"取消资格": "dq",
|
|
"犯规": "dq",
|
|
"dns": "dns",
|
|
"DNS": "dns",
|
|
"未出场": "dns",
|
|
"未到": "dns",
|
|
"缺席": "dns",
|
|
}
|
|
for raw, want := range ok {
|
|
got, err := normalizeImportStatus(raw)
|
|
if err != nil || got != want {
|
|
t.Errorf("normalizeImportStatus(%q) = (%q, %v),期望 (%q, nil)", raw, got, err, want)
|
|
}
|
|
}
|
|
|
|
for _, raw := range []string{"已报到", "unknown", "男子", "DNF", "registered/scratched"} {
|
|
got, err := normalizeImportStatus(raw)
|
|
if err == nil {
|
|
t.Errorf("normalizeImportStatus(%q) 应报错,得到 %q", raw, got)
|
|
continue
|
|
}
|
|
// 错误消息要能直接拼在「第 N 行:」后面,且带上原始值。
|
|
if !strings.Contains(err.Error(), raw) || !strings.Contains(err.Error(), "无法识别") {
|
|
t.Errorf("normalizeImportStatus(%q) 的错误应含原值与「无法识别」,得到 %q", raw, err)
|
|
}
|
|
}
|
|
}
|