Files
Scoreboard/server/internal/httpapi/payload.go
T
AskaEth fc96d17660 feat: 字体可配并随赛事包打包 + 大屏调试标识收敛 + 报名表 CSV 编码兼容
三块改动,均为现场反馈:

一、字体配置与随包打包(此前「没有入口可以配置字体」,导出包也不带字体)
- 服务端新增 httpapi/fonts.go 与 4 条路由:GET /api/fonts(recorder)、
  POST /api/fonts(admin,multipart file)、DELETE /api/fonts/{name}(admin)、
  GET /uploads/fonts/{name}(匿名——大屏 @font-face 不带会话)
- 上传限 ttf/otf/woff/woff2、单个 20 MiB;文件名经 safeFontName 收敛且需与
  fontContentType 往返一致,挡住路径穿越;重名 409、删除被引用字体 409
- 新增设置键 display.font_title / display.font_body(DTO fontTitle/fontBody,
  随快照下发给大屏),写入时校验取值合法
- 导出:接上一直没被赋值的 IncludeFonts/FontsDir;包内落点用 uploads/fonts/
  (运行时只读这里,落顶层 fonts/ 等于没人读)
- 大屏按配置动态注册字体并挂 --display-font-title/body,加载失败静默回落默认字体
- 管理端「赛事信息」新增字体区块:标题/正文字体下拉、上传、字体库、实时预览

二、大屏显示收敛
- 数据版本号角标 vN 彻底移除(原先只有窗口化窗口会显示,两处显隐口径不一致)
- 窗口化窗口按 F11 全屏后自绘标题栏不让位:根因是 Console 用 win.setFullScreen()
  做窗口级全屏,而页面用 document.fullscreenElement 判断,后者恒为 null;
  新增 display-preload.js 下发窗口级全屏状态,页面侧 shell.ts 把两级全屏取或,
  无 preload 时(浏览器直连 /display)回落到文档级监听

三、报名表 CSV 编码兼容(用户报「解析中文字符遇到编码问题」)
- 服务端不再把原始字节直接喂给 csv.Reader:先摘 UTF-8 BOM,再认 UTF-16 LE/BE
  BOM,其余非合法 UTF-8 按 GB18030 解(GBK 是其子集)
- 修复前症状:GBK 文件(Excel 另存为 CSV 的默认编码)中文变乱码;带 BOM 的文件
  (Excel「CSV UTF-8」,也正是我们自己模板的格式)把 BOM 拼进第一列表头,
  报「报名表缺少列 event_code」——连「下载模板→填→传回来」都走不通
- golang.org/x/text 由 indirect 提升为直接依赖,未新增外部依赖

测试:服务端 12 包全绿(新增 fonts_test.go / fonts_internal_test.go /
handlers_import_encoding_test.go);前端 vue-tsc 通过、pnpm build 通过、157 例全过;
Console 语法检查 17 个文件通过。docs/ 与 devdocs/ 同步更新。
2026-09-10 17:45:11 +08:00

137 lines
4.1 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"`
}
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"`
}
type displayCommandInput struct {
Action string `json:"action"`
Payload any `json:"payload"`
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"`
}
type settingsStandingsPatch struct {
IndividualToTeamWeight *float64 `json:"individualToTeamWeight"`
RankingMode *string `json:"rankingMode"`
ShowIndividualBoard *bool `json:"showIndividualBoard"`
ShowIndividualMedals *bool `json:"showIndividualMedals"`
}
type categoriesPayload struct {
Categories []categoryDTO `json:"categories"`
}
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"`
}