Files
Scoreboard/server/internal/httpapi/entry_status.go
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

33 lines
1.3 KiB
Go
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.
package httpapi
import (
"fmt"
"strings"
)
// normalizeImportStatus 把报名表 status 列归一成 entries.status 允许的四个取值。
//
// entries.status 上有 CHECK (status IN ('registered','scratched','dq','dns')),
// 而填表人习惯写大写(DNS、Registered)或中文(退赛、取消资格),原样入库会撞约束,
// 报出来的又是原始 SQLite 错误,现场看不出是 status 这一格填错了。
func normalizeImportStatus(raw string) (string, error) {
// 中文输入法打出的全角空格 TrimSpace 不认,先统一成半角再裁首尾。
v := strings.TrimSpace(strings.ReplaceAll(raw, "\u3000", " "))
// 大小写不敏感,所以先转小写再比对;中文别名不受影响。
switch strings.ToLower(v) {
case "":
// 空值即建表默认值:不填就是不填,按正常参赛处理。
return "registered", nil
case "registered", "正常", "正常参赛", "参赛":
return "registered", nil
case "scratched", "退赛", "弃赛", "请假":
return "scratched", nil
case "dq", "取消资格", "犯规":
return "dq", nil
case "dns", "未出场", "未到", "缺席":
return "dns", nil
}
return "", fmt.Errorf("status「%s」无法识别,只能填 registered / scratched / dq / dns(或留空)", v)
}