feat: 参赛单位「彻底删除」= 假删除(界面隐藏,数据与榜单全保留)
与运动员的真物理删除是**两套相反口径**,有意如此(两个按钮同名,代码与
文档里都做了显著区分):
- 运动员 `?purge=1`:真删人员+报名+成绩+流水并重算积分(上一版 `9d4ee09`)
- 参赛单位 `?purge=1`:只置 `teams.deleted_at` + `active = 0`,让单位从列表里
消失;报名、成绩、积分流水、人工调整、纪录、名下运动员全部原样,
**不触发重算、积分榜不变**(单位下挂着几十号人,真删代价太大)
实现:
- 迁移 0018 `ALTER TABLE teams ADD COLUMN deleted_at TEXT`
- `listTeams` 默认排除已隐藏单位;`?includeDeleted=1` 仅 admin;
`GET /api/teams/{id}` 对已隐藏单位:admin 200 / 非 admin 404
- 审计 `team.purge`(detail 标明是隐藏);成功走 `writeVoid`(无统计对象)
- 报名导入按 `team_code` 命中已隐藏单位时复活(清 deleted_at、置 active=1)
- 故意不过滤的地方都加了注释:榜单补单位名、写路径与审计、导入按 code 查、
赛事包导出、大屏快照、按 team_id JOIN 取历史单位名——过滤会让历史成绩的
单位名变空白或让包的引用悬挂
管理端「参赛单位」行加「彻底删除」按钮 + 危险确认框(如实写明"从列表移除、
数据不受影响、积分榜不会变化"),失败原文展示、404 不降级。
顺带修一处界面文案与行为不符:单位页原写「停用后新报名不再可选该单位」,
但报名选择器其实仍会列出已停用单位供手动勾选(只有「全选」跳过它们),
已按真实行为改写(两处)。
测试:服务端 12 包全绿(新增 team_purge_test.go 与 0018 迁移用例);
前端 172 例全过(新增 team-purge-api.test.ts 6 例)。docs/ 与 devdocs/ 同步。
This commit is contained in:
1 parent
9d4ee0946c
commit
5ae49b001c
18 files changed
+767
-13
No files matched your search
@@ -139,22 +139,32 @@ func exists(ctx context.Context, db *sql.DB, query string, args ...any) (bool, e
|
||||
|
||||
// ---------- teams ----------
|
||||
|
||||
const teamColumns = `id, code, name, short_name, logo_path, color, sort_order, active`
|
||||
const teamColumns = `id, code, name, short_name, logo_path, color, sort_order, active, deleted_at`
|
||||
|
||||
func scanTeam(row interface{ Scan(...any) error }) (teamDTO, error) {
|
||||
var (
|
||||
t teamDTO
|
||||
shortName, logoPath, color sql.NullString
|
||||
deletedAt sql.NullString
|
||||
)
|
||||
err := row.Scan(&t.ID, &t.Code, &t.Name, &shortName, &logoPath, &color, &t.SortOrder, &t.Active)
|
||||
err := row.Scan(&t.ID, &t.Code, &t.Name, &shortName, &logoPath, &color, &t.SortOrder, &t.Active, &deletedAt)
|
||||
t.ShortName = strPtr(shortName)
|
||||
t.LogoPath = strPtr(logoPath)
|
||||
t.Color = strPtr(color)
|
||||
t.DeletedAt = strPtr(deletedAt)
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (s *server) listTeams(ctx context.Context) ([]teamDTO, error) {
|
||||
rows, err := s.ev.QueryContext(ctx, `SELECT `+teamColumns+` FROM teams ORDER BY sort_order, code`)
|
||||
// listTeams 列出参赛单位。默认排除「彻底删除」(deleted_at 非空)的单位,
|
||||
// includeDeleted 只给运维排查用,由 handler 限 admin;停用(active = 0)仍在列表里。
|
||||
func (s *server) listTeams(ctx context.Context, includeDeleted bool) ([]teamDTO, error) {
|
||||
query := `SELECT ` + teamColumns + ` FROM teams`
|
||||
if !includeDeleted {
|
||||
query += ` WHERE deleted_at IS NULL`
|
||||
}
|
||||
query += ` ORDER BY sort_order, code`
|
||||
|
||||
rows, err := s.ev.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
@@ -302,6 +312,34 @@ func (s *server) deleteTeam(ctx context.Context, id int64) error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// hideTeam 彻底删除参赛单位:只打「对界面不可见」的标记,行本身留在库里,
|
||||
// 返回更新后的单位(含 deletedAt)供审计记录。
|
||||
//
|
||||
// 报名、成绩、积分流水、人工调整、纪录、名下运动员全部原样,所以榜单不会变、
|
||||
// 也不需要任何重算;active 一并置 0,任何漏了 deleted_at 判断的老查询也不会再选中它。
|
||||
func (s *server) hideTeam(ctx context.Context, id int64) (teamDTO, error) {
|
||||
res, err := s.ev.ExecContext(ctx,
|
||||
`UPDATE teams SET deleted_at = datetime('now'), active = 0 WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return teamDTO{}, internal(err)
|
||||
}
|
||||
if err := requireChanges(res, "单位不存在"); err != nil {
|
||||
return teamDTO{}, err
|
||||
}
|
||||
return s.getTeam(ctx, id)
|
||||
}
|
||||
|
||||
// reviveTeam 把被彻底删除的单位放回界面:导入再次命中它的编码时视为同一个单位复用。
|
||||
// 单位编码有唯一索引,漏掉已隐藏的行只会撞索引或被当成新单位重复建档。
|
||||
// 停用(active = 0)是另一种状态,这里一并恢复,否则复活后仍显示成「已停用」。
|
||||
func (s *server) reviveTeam(ctx context.Context, id int64) error {
|
||||
if _, err := s.ev.ExecContext(ctx,
|
||||
`UPDATE teams SET deleted_at = NULL, active = 1 WHERE id = ? AND deleted_at IS NOT NULL`, id); err != nil {
|
||||
return internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- athletes ----------
|
||||
|
||||
const athleteColumns = `id, name, gender, team_id, employee_no, id_card, bib, age, active`
|
||||
|
||||
Reference in new issue
Block a user