feat: 人员年龄/号码布、图片上传、多显示器菜单、Console 两栏、exe 改名等一批改动

人员与报名:
- 人员唯一标识改为「证件号 或 号码布」二选一(0013/0014 迁移 + 导入匹配/回填/冲突)
- 人员新增「年龄」(0015 迁移):表单/列表/导入模板表头 11 列;冲突比较只在两边
  都有值且不同时才算差异,旧数据不误报
- 冲突决策键改为「证件号优先 → 号码布」(前端原先只用 idCard,号码布行会互相覆盖)
- 报名选择器支持无证件号但有号码布的运动员

大屏与 Console:
- 大屏控制新增「显示器」菜单:可给指定显示器重建全屏窗口;单屏/主显示器强制窗口化,
  限制在 Console 侧判定,不信任页面传参
- Console 服务管理窗口改左右两栏,运行日志独占右栏满高;窗口默认尺寸 1280×860
- 大屏右上角时钟精确到秒(同时把刷新间隔 15s 改为 1s)

图片与配置:
- 新增图片上传 POST /api/uploads(recorder+,png/jpg/webp/gif,≤5MiB)与匿名静态
  访问 GET /uploads/{name}(防目录穿越);文件落赛事项目目录 uploads/,随赛事包
  一起导出/导入
- 「赛事信息」新增赛事会标 / 大屏会标 / 大屏背景图三个上传项
- 参赛单位 logo 输入框右侧加上传按钮

关于页与构建:
- 构建时间打通:version.BuildTime + /api/system/about 返回 + build.bat 注入
  (用 PowerShell 取时间戳,避免本地化 %DATE% 的非 ASCII 星期)

安装包:
- 主程序改名 Scoreboard_Console.exe(win.executableName;productName/显示名不变)
- 记住上次安装目录:自定义 NSIS 宏优先沿用自存路径(卸载不清除),并在 customInit
  里补写回 electron-builder 的键,避免安装模式页把 $INSTDIR 打回默认

自检:服务端 12 包全绿;前端 vue-tsc + build 通过、node --test 58/58;console 12 文件通过。
This commit is contained in:
AskaEth committed 2026-09-10 09:05:06 +08:00
1 parent 24fff8dd08
commit 6483acf9ad
50 files changed
+2980 -395

No files matched your search

+46
View File
@@ -9,6 +9,7 @@ import (
"time"
"git.yeij.top/AskaEth/Scoreboard/server/internal/httpapi"
"git.yeij.top/AskaEth/Scoreboard/server/internal/version"
)
// waitClosed 等待关闭回调触发;没触发就报错。
@@ -133,6 +134,51 @@ func TestShutdownByToken(t *testing.T) {
}
}
// GET /api/system/about:版本包里的 buildTime 原样返回;未注入时是空串(前端据此显示「未注入」)。
// 顺带锁住权限:guest 依旧读不到(该端点靠它取内部署名)。
func TestSystemAboutReturnsBuildTime(t *testing.T) {
env := newTestEnv(t)
env.createUser("recorder", "recorder-pass", "recorder")
guest, _ := env.client()
if status, _ := env.do(guest, http.MethodGet, "/api/system/about", nil, false); status != http.StatusForbidden {
t.Fatalf("guest 读 about 应 403,得到 %d", status)
}
client, _ := env.client()
env.login(client, "recorder", "recorder-pass")
// 模拟 ldflags 注入:直接改包级变量,用例结束后恢复,避免影响其他用例。
original := version.BuildTime
t.Cleanup(func() { version.BuildTime = original })
version.BuildTime = "2026-09-10 08:50:12 +08:00"
status, env2 := env.do(client, http.MethodGet, "/api/system/about", nil, false)
if status != http.StatusOK || !env2.OK {
t.Fatalf("recorder 读 about 应 200,得到 %d %+v", status, env2.Error)
}
var about map[string]any
if err := json.Unmarshal(env2.Data, &about); err != nil {
t.Fatalf("解析 about: %v (%s)", err, env2.Data)
}
if about["buildTime"] != "2026-09-10 08:50:12 +08:00" {
t.Fatalf("buildTime 应原样返回注入值,得到 %#v", about["buildTime"])
}
if _, ok := about["internalSignature"]; !ok {
t.Fatalf("about 仍应带 internalSignature: %v", about)
}
// 未注入时字段仍在、值为空串,前端才能稳定判断「未注入」而不是解析失败。
version.BuildTime = ""
_, env2 = env.do(client, http.MethodGet, "/api/system/about", nil, false)
if err := json.Unmarshal(env2.Data, &about); err != nil {
t.Fatalf("解析 about: %v", err)
}
if got, ok := about["buildTime"]; !ok || got != "" {
t.Fatalf("未注入时 buildTime 应为空串,得到 %#v(存在=%v)", got, ok)
}
}
// GET /api/rules/switches:recorder 可读、guest 403,只返回布尔开关。
func TestRuleSwitchesReadableByRecorder(t *testing.T) {
env := newTestEnv(t)