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
@@ -17,7 +17,11 @@ import type {
|
||||
PageQuery,
|
||||
PointAdjustment,
|
||||
PublishPayload,
|
||||
RecordBreak,
|
||||
RecordBreakState,
|
||||
RecordEntry,
|
||||
RecordEntryInput,
|
||||
ReopenResultPayload,
|
||||
Result,
|
||||
SaveDraftPayload,
|
||||
ScoringProfile,
|
||||
@@ -27,6 +31,7 @@ import type {
|
||||
TeamBreakdown,
|
||||
TeamRanksInput,
|
||||
TeamRanksView,
|
||||
VoidResultPayload,
|
||||
} from '@/shared'
|
||||
import { mockDb } from './mock'
|
||||
|
||||
@@ -219,6 +224,18 @@ export const adminApi = {
|
||||
unpublish: (payload: PublishPayload): Promise<Result[]> =>
|
||||
callWrite(() => client.results.unpublish(payload)),
|
||||
|
||||
/**
|
||||
* 作废一条已发布的成绩(外科手术式,影响面远小于整组撤销发布)。
|
||||
*
|
||||
* 写操作不降级:示例数据模式下没有后端可落库,页面会先用 data.isMock 拦下。
|
||||
*/
|
||||
voidResult: (id: number, payload: VoidResultPayload): Promise<Result> =>
|
||||
callWrite(() => client.results.void(id, payload)),
|
||||
|
||||
/** 把一条已作废的成绩退回草稿,改完再发布本组;同样是不降级的写操作。 */
|
||||
reopenResult: (id: number, payload: ReopenResultPayload): Promise<Result> =>
|
||||
callWrite(() => client.results.reopen(id, payload)),
|
||||
|
||||
standings: (): Promise<StandingRow[]> =>
|
||||
call(() => client.standings.teams(), () => mockDb.standings()),
|
||||
|
||||
@@ -244,6 +261,36 @@ export const adminApi = {
|
||||
records: (): Promise<RecordEntry[]> =>
|
||||
call(() => client.records.list(), () => mockDb.records()),
|
||||
|
||||
/**
|
||||
* 破纪录登记列表(GET /api/record-breaks?state=...)。
|
||||
*
|
||||
* 刻意**不走示例数据降级**:示例数据里根本没有登记表,回一个空数组会让页面把
|
||||
* 「读不到」显示成「没有已批准的破纪录」。读不到时原样抛出,页面据此显示空态说明。
|
||||
*/
|
||||
recordBreaks: (state: RecordBreakState): Promise<RecordBreak[]> =>
|
||||
client.records.listBreaks(state),
|
||||
|
||||
/** 驳回 / 作废一条登记;原本 approved 的会连加分一起撤掉(服务端行为)。 */
|
||||
rejectBreak: (id: number, reason: string): Promise<RecordBreak> =>
|
||||
callWrite(() => client.records.rejectBreak(id, reason)),
|
||||
|
||||
/** 登记一条历史纪录;服务端固定 source = history。 */
|
||||
createRecord: (input: RecordEntryInput): Promise<RecordEntry> =>
|
||||
callWrite(() => client.records.createRecord(input)),
|
||||
|
||||
/** 删除一条纪录;删的若是当前纪录,服务端恢复上一条。 */
|
||||
removeRecord: (id: number): Promise<void> => callWrite(() => client.records.removeRecord(id)),
|
||||
|
||||
/**
|
||||
* 「草稿待发布数」:分页只取 1 条,要的是 Page.total 这个总数。
|
||||
*
|
||||
* 同样不降级:读不到时页面显示 `—`,不用 0 冒充「一条草稿都没有」。
|
||||
*/
|
||||
draftResultCount: async (): Promise<number> => {
|
||||
const page = await client.results.list({ state: 'draft', page: 1, size: 1 })
|
||||
return page.total
|
||||
},
|
||||
|
||||
adjustments: (query: PageQuery = {}): Promise<Page<PointAdjustment>> =>
|
||||
call(() => client.adjustments.list(query), () => mockDb.adjustments()),
|
||||
|
||||
|
||||
Reference in new issue
Block a user