用户点名的三件事(模板功能、删死代码、剩余缺口)这一批全部落地,另修掉两个刚暴露的导入缺陷:
**新增能力**
- 备份恢复:`POST /api/backup/restore`(admin)。先把校验做在拍快照之前(只读打开 + integrity_check +
关键表校验 + 异类库标志表互斥),再用 VACUUM INTO 拍「恢复前安全副本」,然后关连接、整文件替换、
触发优雅重启;`-wal` 还在时直接拒绝(宁可失败也不让旧库 WAL 套到新库上);恢复后审计直写"恢复之后
还在的那份库"(不走通用 Auditor,否则记录只会落在被替换掉的旧文件上)。顺带修掉:裸服务端连续恢复
第二次会在拍副本处崩出一句看不懂的 500,现在回 503「上次恢复后尚未重启,请先重启服务端」。
- 安全副本保留策略:每类库保留最近 5 份,独立于 .db 的 20 份滚动清理。
- 图片管理:`DELETE /api/uploads/{name}`(被引用 409 并列出引用点)、`GET /api/uploads/orphans`、
`POST /api/uploads/orphans/delete`(只删列出的孤儿,宁漏勿误),全部 admin(与字体删除同档);
`deploy/backup.bat` 补上 uploads/ 归档(logs 不归档:可再生的诊断且会无上限增长)。
- 已彻底删除单位的恢复:`POST /api/teams/{id}/restore`(与 hideTeam 同档)。幂等;恢复不会把「停用」
变成「启用」;真机确认恢复前后榜单逐字节一致、change_log 最大值不变(数据一行没动,不需要重算)。
界面加「显示已彻底删除的单位」(仅 admin)与恢复按钮;非 admin 的删除确认框里写明"本账号无法自行恢复"。
- 审计日志按 `action` 过滤(子串模糊,输入里的 %/_/\ 转义成字面量)。
- 删除死表 recent_projects(新增 system 迁移 0006,未动已应用的迁移)。
- points_override 不再"设了没效果":写入侧 400 + 引擎侧守卫,提示改用「积分调整」。
- 联队份额空壳清理(UnionAttribution/UnionSplit/Entry.TeamShares/resolvedShares 等;TeamShare 类型保留,
因为 export/realtime/奖牌流水三处在生产路径上构造它)。
- 自定义大屏模板 M1:插值引擎(parseTemplate/renderTemplate),任何情况都不抛异常、未定义→空串+警告、
警告按 (code,path) 合并计数、无 eval/innerHTML,57 个新用例。
**两个真缺陷(前两个已随 0.1.6 出包)**
- 基础数据包导入「排序」列口径不一致:小项/组别留空保持原值,单位/大项留空被写成 0(plan 没从库里
现值起手)→ 四张表统一为「留空保持原值」。
- 同类第二处:布尔列「整列被删掉」被当成"取默认值",会把库里所有停用的单位/大项静默改回启用 →
改为「缺列 = 不动」(写侧与解析侧都收口,补了"缺列保持/空格子=是/显式否=停用"三组对照用例)。
**删死代码**
- createEntry / updateEntry / deleteEntry / entryEventID / checkCategoryInEvent / getEntry /
normalizeEntryStatus / ExportBaseDataXLSX(全仓 grep 零引用)。⚠️ 随之记入文档:「组别必须属于该小项」
这道校验现在只由导入路径"按小项+组别名解析"天然保证,将来若新增从请求体接收 categoryId 的接口,
必须自己补校验(库层没有复合外键)。
**文档**
- devdocs 全面同步(01/02/03/04/05/06/07/09/11/12/13/README;含身份口径、延迟结算、大屏单栏新版式、
新增路由与设置键、R18/R19 风险);docs 复核 + 第二轮(身份口径三个易写反的分支、图片删除、备份覆盖、
审计过滤、单位恢复、去掉硬编码版本号、上线检查清单 +5 项);debug/使用说明 6 份逐份 diff 一致。
测试:服务端 13 包(连跑两遍)、前端 406 例、Console 28 例 + 18 文件语法检查 —— 全绿。
版本号 dev_0.1.6 → dev_0.1.7。
192 lines
6.7 KiB
Go
192 lines
6.7 KiB
Go
package httpapi
|
||
|
||
// 本文件是各写接口的请求体契约;字段名与 web/src/shared/types.ts 对齐。
|
||
|
||
type loginInput struct {
|
||
Username string `json:"username"`
|
||
Password string `json:"password"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type saveDraftPayload struct {
|
||
EventID int64 `json:"eventId"`
|
||
CategoryID *int64 `json:"categoryId"`
|
||
Results []draftResultInput `json:"results"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type draftResultInput struct {
|
||
EntryID int64 `json:"entryId"`
|
||
Rank *int `json:"rank"`
|
||
ResultText *string `json:"resultText"`
|
||
ResultValue *float64 `json:"resultValue"`
|
||
Unit *string `json:"unit"`
|
||
IsTie bool `json:"isTie"`
|
||
Status string `json:"status"`
|
||
PointsOverride *float64 `json:"pointsOverride"`
|
||
Note *string `json:"note"`
|
||
}
|
||
|
||
type publishPayload struct {
|
||
EventID int64 `json:"eventId"`
|
||
CategoryID *int64 `json:"categoryId"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type voidResultPayload struct {
|
||
Reason string `json:"reason"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// entryHeatPayload 是批量设置批次的请求体:heat 为 null / 缺省表示清除批次(未分批)。
|
||
type entryHeatPayload struct {
|
||
EntryIDs []int64 `json:"entryIds"`
|
||
Heat *int64 `json:"heat"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// entryBatchPayload 是报名项批量操作的请求体:
|
||
// action 决定语义(scratch 退赛可恢复 / restore 恢复 / remove 物理移出报名),
|
||
// ids 是勾选的报名项(重复会自动去重),reason 可选、只进审计便于事后对账。
|
||
type entryBatchPayload struct {
|
||
Action string `json:"action"`
|
||
IDs []int64 `json:"ids"`
|
||
Reason string `json:"reason"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type adjustmentInput struct {
|
||
TeamID int64 `json:"teamId"`
|
||
Delta float64 `json:"delta"`
|
||
Category string `json:"category"`
|
||
Reason string `json:"reason"`
|
||
EventID *int64 `json:"eventId"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type recordBreakInput struct {
|
||
EventID int64 `json:"eventId"`
|
||
EntryID int64 `json:"entryId"`
|
||
Level string `json:"level"`
|
||
ResultValue float64 `json:"resultValue"`
|
||
ResultText string `json:"resultText"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// displayCommandInput 是大屏控制指令的请求体。
|
||
type displayCommandInput struct {
|
||
Action string `json:"action"`
|
||
Payload any `json:"payload"`
|
||
// Targets 是要定向控制的大屏实例 id(GET /api/display/instances 的 id);
|
||
// 留空表示广播给所有大屏——公告这类全屏指令走的就是广播。
|
||
Targets []string `json:"targets"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type voidAdjustmentPayload struct {
|
||
Reason string `json:"reason"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type settingsPatch struct {
|
||
Event *settingsEventPatch `json:"event"`
|
||
Display *settingsDisplayPatch `json:"display"`
|
||
Standings *settingsStandingsPatch `json:"standings"`
|
||
DefaultProfileID *int64 `json:"defaultProfileId"`
|
||
RequireApproval *bool `json:"requireApproval"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type settingsEventPatch struct {
|
||
Name *string `json:"name"`
|
||
Subtitle *string `json:"subtitle"`
|
||
Host *string `json:"host"`
|
||
LogoPath *string `json:"logoPath"`
|
||
PrimaryColor *string `json:"primaryColor"`
|
||
}
|
||
|
||
type settingsDisplayPatch struct {
|
||
Background *string `json:"background"`
|
||
Logo *string `json:"logo"`
|
||
Title *string `json:"title"`
|
||
Subtitle *string `json:"subtitle"`
|
||
PrimaryColor *string `json:"primaryColor"`
|
||
RotateSeconds *float64 `json:"rotateSeconds"`
|
||
// FontTitle / FontBody 是字体 id;null 表示不改动(清空要传空串)。
|
||
FontTitle *string `json:"fontTitle"`
|
||
FontBody *string `json:"fontBody"`
|
||
// LiveEvents 是「实时赛况」人工指定的当前项目 id(最多 2 个)。
|
||
// nil 表示不改动;传 [] 表示清空人工指定。
|
||
LiveEvents *[]int64 `json:"liveEvents"`
|
||
// LiveSource 是当前项目的判定方式(manual / auto);nil 表示不改动。
|
||
LiveSource *string `json:"liveSource"`
|
||
}
|
||
|
||
type settingsStandingsPatch struct {
|
||
IndividualToTeamWeight *float64 `json:"individualToTeamWeight"`
|
||
RankingMode *string `json:"rankingMode"`
|
||
ShowIndividualBoard *bool `json:"showIndividualBoard"`
|
||
ShowIndividualMedals *bool `json:"showIndividualMedals"`
|
||
// LiveOnPublish 是「分批发布时是否实时更新各单位总分与奖牌榜」;nil 表示不改动。
|
||
LiveOnPublish *bool `json:"liveOnPublish"`
|
||
}
|
||
|
||
// sessionSettingsPatch 是 PUT /api/system/session 的请求体:
|
||
// 只改「登录有效期(小时)」这一个系统级配置。
|
||
type sessionSettingsPatch struct {
|
||
SessionHours *int `json:"sessionHours"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
type categoriesPayload struct {
|
||
Categories []categoryDTO `json:"categories"`
|
||
}
|
||
|
||
// athletesBatchPayload 是运动员批量动作的请求体:action 决定停用还是彻底删除。
|
||
type athletesBatchPayload struct {
|
||
Action string `json:"action"`
|
||
IDs []int64 `json:"ids"`
|
||
// DryRun 只统计不改库:前端用它先看清「会删掉多少、重算哪些小项」再确认。
|
||
DryRun bool `json:"dryRun"`
|
||
}
|
||
|
||
type channelsPayload struct {
|
||
Channels []channelDTO `json:"channels"`
|
||
}
|
||
|
||
// ---------- 用户管理 ----------
|
||
|
||
type createUserInput struct {
|
||
Username string `json:"username"`
|
||
DisplayName string `json:"displayName"`
|
||
Role string `json:"role"`
|
||
Password string `json:"password"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// updateUserInput 只带需要改的字段:nil 表示保持原值。
|
||
type updateUserInput struct {
|
||
DisplayName *string `json:"displayName"`
|
||
Role *string `json:"role"`
|
||
Active *bool `json:"active"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// resetUserPasswordInput 的 password 留空时由服务端随机生成。
|
||
type resetUserPasswordInput struct {
|
||
Password string `json:"password"`
|
||
RequestID string `json:"requestId"`
|
||
}
|
||
|
||
// restoreBackupPayload 是 POST /api/backup/restore 的请求体。
|
||
//
|
||
// File 只接受备份目录里的裸文件名(不接受路径);Kind 是**要覆盖的那份库**
|
||
// (备份列表里的 dbKind)——安全副本的性质是 safety,但它同样属于某一份库,
|
||
// 拿它退回上一个时间点是正当操作,所以这里仍只认 system / event。
|
||
// 服务端再按文件名校验一遍(对不上直接拒绝):恢复错库的代价是两份数据一起完蛋。
|
||
type restoreBackupPayload struct {
|
||
File string `json:"file"`
|
||
Kind string `json:"kind"`
|
||
RequestID string `json:"requestId"`
|
||
}
|