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:
1 parent
22ab49e73a
commit
9d4ee0946c
22 files changed
+1264
-54
No files matched your search
@@ -518,20 +518,40 @@ func (s *server) updateAthleteHandler(w http.ResponseWriter, r *http.Request) (a
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *server) deleteAthleteHandler(w http.ResponseWriter, r *http.Request) error {
|
||||
// deleteAthleteHandler 默认软删除(停用);带 purge=1 时物理删除人员、报名、成绩并重算受影响的积分。
|
||||
func (s *server) deleteAthleteHandler(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
before, err := s.getAthlete(r.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if err := s.deleteAthlete(r.Context(), id); err != nil {
|
||||
return err
|
||||
if !isQueryTrue(r, "purge") {
|
||||
if err := s.deleteAthlete(r.Context(), id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.audit(r.Context(), "athlete.delete", "athlete", id, before, nil)
|
||||
return nil, nil
|
||||
}
|
||||
s.audit(r.Context(), "athlete.delete", "athlete", id, before, nil)
|
||||
return nil
|
||||
purged, err := s.purgeAthlete(r.Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 审计带上删除规模与重算过的小项,事后能对上「榜为什么变了」。
|
||||
s.audit(r.Context(), "athlete.purge", "athlete", id, before, purged)
|
||||
return purged, nil
|
||||
}
|
||||
|
||||
// isQueryTrue 解析开关型查询参数:只认 1 / true(大小写不敏感),其余值一律当 false。
|
||||
// 写错的值不会误开开关——purge=0、purge=false 都还是软删除。
|
||||
func isQueryTrue(r *http.Request, key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get(key))) {
|
||||
case "1", "true":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------- 报名表导入 ----------
|
||||
@@ -776,6 +796,13 @@ func (s *server) importEntriesWith(ctx context.Context, rows [][]string, decisio
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("第 %d 行:event_code / team_code 必填", line))
|
||||
continue
|
||||
}
|
||||
// status 先归一:填表人写 DNS、退赛这类值不会撞 CHECK,认不出的填法在这里给出行级提示。
|
||||
status, err := normalizeImportStatus(cell("status"))
|
||||
if err != nil {
|
||||
result.Skipped++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("第 %d 行:%s", line, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
eventID, err := s.lookupID(ctx, `SELECT id FROM events WHERE code = ?`, eventCode)
|
||||
if err != nil {
|
||||
@@ -872,7 +899,7 @@ func (s *server) importEntriesWith(ctx context.Context, rows [][]string, decisio
|
||||
MemberNames: splitNames(cell("member_names")),
|
||||
Bib: nilIfEmpty(cell("bib")),
|
||||
Seed: nilIfEmpty(cell("seed")),
|
||||
Status: orDefault(cell("status"), "registered"),
|
||||
Status: status,
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
|
||||
Reference in new issue
Block a user