feat: 主人拍板的 4 条全部落地(单条作废/重录、破纪录驳回、历史纪录、仪表盘卡片)+ 迁移 0021(dev_0.3.0)
## ① 成绩「作废本条 / 重新录入」——手滑填错了怎么办
- 新增 `POST /api/results/{id}/reopen`:把已发布的那条退回草稿(state=draft、
status=ok、rank=NULL、note=原因)并重算推榜;口径与既有 `void` 完全一致
(回滚类动作,不受 `standings.live_on_publish` 约束)。
- 成绩录入页新增「操作」列:已发布行可「作废本条」,已作废行可「重新录入」,
两者都必填原因、确认后立即重算+推大屏。**验收用例**:作废→重录→改→重发,
名次/流水/榜单与「一开始就填对」逐字节一致。
## ② 破纪录的驳回 / 作废
- 迁移 `0021_record_breaks_note.sql`:`record_breaks` 加一列可空 `note`。
- 新增 `GET /api/record-breaks?state=`(recorder 起可读,guest 403)与
`POST /api/record-breaks/{id}/reject`(必填原因):已批准的登记被作废时
**撤回该笔加分、重算推榜、并把该级别当前纪录恢复为上一条**;`approve`
放开为可对 rejected 再次批准。
- 破纪录页新增「已批准的破纪录」卡片(作废入口);原先只存在浏览器内存里的
「拒绝」改为持久化(先登记再驳回,刷新不再复现)。
## ③ 历史纪录的写入路径(`records.source` 以前恒为「本届」)
- 新增 `POST /api/records`(source 只收 history)与 `DELETE /api/records/{id}`
(删当前纪录时恢复上一条)。纪录库卡片新增「新增历史纪录」与「删除」。
- 至此《使用手册》6.2 承诺的「来源(本届/历史)」这一列才真正有意义。
## ④ 仪表盘补齐承诺的卡片
今天的小项(按 scheduledAt 的本地时区当天 + 进度)、草稿待发布、待确认破纪录、
大屏在线数;数据源读不到显示 `—` 而不是 0。文档里「管理端在线数」服务端没有
接口,按实际改写成本机管理端连接状态。
## 旧版数据包兼容(主人特别交代,三处验证)
迁移只加**可空**列、不重建表、不动数据行;旧赛事包导入后由 `main.go` 的
`store.MigrateEvent` 在启动时补跑迁移。证据:`migration_0021_test.go`
(旧库升级 + 幂等 + 老行取 NULL)、`migration_legacy_test.go` 扩到 0021、
`legacy_pack_upgrade_test.go`(真实旧 event.db 走 main 同一条路径后装配 API,
驳回老登记 → 加分撤回、纪录回退、旧数据逐表一行不差),另有真二进制冒烟。
未迁移的库上不存在任何新列读取。
## 文档
升级说明新增 dev_0.3.0(含「旧包仍可导入、自动补跑迁移」);使用手册新增
§4.10 与 §6.1/§6.2 明细;常见问题新增 Q98/Q99/Q100;devdocs 04/05/07/13/02
同步(含新接口表与错误分级)。
自检:服务端 13 包 ×2 全绿(httpapi 新增 reopen/reject/records/旧包 四个用例文件)、
前端 vue-tsc + 604 例 ×2 + build、Console 19 文件 + 54 例。
This commit is contained in:
1 parent
6841cf49aa
commit
ed2c0b3f3c
35 files changed
+4003
-115
No files matched your search
@@ -10,7 +10,7 @@ import (
|
||||
// C12:0001–0011 的老库一次性升到最新。
|
||||
//
|
||||
// 此前每个迁移用例都是 applyBefore 到「目标那一条的前一条」,覆盖的是相邻一两跳;
|
||||
// 真实升级现场拿到的却是很久以前的库(甚至跨好几个版本),一次要连跑 0012~0020 九条,
|
||||
// 真实升级现场拿到的却是很久以前的库(甚至跨好几个版本),一次要连跑 0012~0021 十条,
|
||||
// 其中 0016 会就地改写 events 的建表语句、0017 会重建 score_ledger——这些步骤正是
|
||||
// 「只在老库上才走得到」的路径。这条用例把这条路整条走一遍:
|
||||
// 数据一行不少、新列取文档写明的默认值、新表建出来、升级后新能力可用、再跑一次也不报错。
|
||||
@@ -72,7 +72,7 @@ func TestMigration0001ToLatestKeepsData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 走真实升级路径:一次补跑 0012~0020 全部九条。
|
||||
// 走真实升级路径:一次补跑 0012~0021 全部十条(含只加一列的 0018/0019/0021)。
|
||||
if err := store.MigrateEvent(db); err != nil {
|
||||
t.Fatalf("从 0001–0011 直接升到最新失败: %v", err)
|
||||
}
|
||||
@@ -213,6 +213,15 @@ func TestMigration0001ToLatestKeepsData(t *testing.T) {
|
||||
if countToMedals != 1 || countToTeamMedals != 1 {
|
||||
t.Errorf("老通道奖牌开关 = (%d, %d),期望 (1, 1)", countToMedals, countToTeamMedals)
|
||||
}
|
||||
// 0021 的 note 是老库上不存在的列,老登记(这条是 approved 的破纪录)升级后必须是 NULL:
|
||||
// 历史行没有驳回理由,NULL 才是事实,写成空串或默认文案都会让审核清单说谎。
|
||||
var breakNote sql.NullString
|
||||
if err := db.QueryRow(`SELECT note FROM record_breaks WHERE id = 1`).Scan(&breakNote); err != nil {
|
||||
t.Fatalf("读破纪录登记的 note: %v", err)
|
||||
}
|
||||
if breakNote.Valid {
|
||||
t.Errorf("老破纪录登记的 note 应为 NULL,得到 %q", breakNote.String)
|
||||
}
|
||||
|
||||
// ④ 新表建出来了,且是空的(升级不该凭空造名次)。
|
||||
for _, table := range []string{"channel_ranks", "channel_manual_ranks"} {
|
||||
@@ -238,7 +247,7 @@ func TestMigration0001ToLatestKeepsData(t *testing.T) {
|
||||
}
|
||||
defer func() { _ = fkRows.Close() }()
|
||||
if fkRows.Next() {
|
||||
t.Error("跨九条迁移升级后存在外键悬挂")
|
||||
t.Error("跨十条迁移升级后存在外键悬挂")
|
||||
}
|
||||
var fkEnabled int
|
||||
if err := db.QueryRow(`PRAGMA foreign_keys`).Scan(&fkEnabled); err != nil {
|
||||
@@ -267,6 +276,13 @@ func TestMigration0001ToLatestKeepsData(t *testing.T) {
|
||||
VALUES (1, 1, 1, 'result', 'athlete', 'e:1', 1, 1, 1, 10)`); err != nil {
|
||||
t.Fatalf("写 0020 新建的名次表: %v", err)
|
||||
}
|
||||
// 0021 的 note 也要能写能清:列建了却写不进去,等于没升。
|
||||
if _, err := db.Exec(`UPDATE record_breaks SET state = 'rejected', note = '录像复核未破' WHERE id = 1`); err != nil {
|
||||
t.Fatalf("写 0021 的驳回理由: %v", err)
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE record_breaks SET state = 'approved', note = NULL WHERE id = 1`); err != nil {
|
||||
t.Fatalf("清 0021 的驳回理由: %v", err)
|
||||
}
|
||||
// 新 CHECK 也在:合法的拒绝了非法值才算真的生效。
|
||||
if _, err := db.Exec(`INSERT INTO events(sport_id, code, name, event_type) VALUES (1, 'E3', '非法', 'bogus')`); err == nil {
|
||||
t.Error("升级后非法 event_type 应被 CHECK 拒绝")
|
||||
|
||||
Reference in new issue
Block a user