P0(9 条全修): - 多通道项目奖牌重复计数:domain 层按 (EventID,CategoryID,EntryID/Subject) 去重, 通道优先级 athlete→team→id - 「只排名不计分」项目(scored:false)不再进奖牌榜,REST/WS/导出口径统一 - aggregate 流水 entry_id 恒 NULL 导致重算后分数翻倍:作废范围改按 event+category 定位 - 已发布成绩仍可被「存草稿」覆盖并清空 rank:服务端 409 STATE_INVALID + 前端禁编辑 - voidResult / 规则导入重算后名次不回写:接住 Ranked 调 applyComputedRanks - 赛事包覆盖导入可能「旧项目已删、新项目没成」:先改名成功再清理,失败回滚 - 导入赛事包可覆盖正在运行的赛事目录:name==当前项目直接 400 - Console spawn 失败后守护器永久卡死:清 child/pid + 调度重启 + stop 超时兜底 - /ws 快照向非 admin 下发 admin-only 配置:按角色裁剪 settings P1(计分口径 / 契约 / 现场): - standings.tieBreak 三处接线;profiles[].recordBonusMode 生效;破纪录校验 RecordEligible 与 entry 归属;破纪录登记校验已发布成绩 - export.loadRanked 与主路径对齐;ListBackups reason 解析;CLI backup 前缀修正 (原来只增不减会撑爆磁盘);单位积分明细/导出统一权重折算;停用单位展示 - 规则导入原子化(重算并入同一事务)+ 返回 compatible/affectedEventCount/ recalculatedLedgerRows;登录审计 user_id 修正 - WS 策略性断开用 Close(code,reason);HelloMessage 补 page/instanceId; 实例注册表清理;settings/单位/项目写操作补 change_log 触发大屏刷新 - 大屏:全量快照不再被版本守卫丢弃;个人积分榜刷新;公告返回页/轮播;行数截断; REST 竞态;1008 停止重连;握手超时;时区补 Z;401 仅 AUTH_REQUIRED 才登出 - 管理端:published 行禁编辑;saveDraft 补 unit(1:06.33 显示);前端预览改用服务端 points;空成绩行不再恒「已修改」;修订详情按 id 拉取;切项目请求序号与 dirty 保护; 发布后刷新项目状态;NAV/路由按角色过滤;recorder 不再打 admin-only 接口 - Console:大屏窗口销毁后重建;大屏 URL 复用 host 归一化;backup.bat 改纯 ASCII (已在 Meowhome 实跑验证) 自检:服务端 gofmt/build/vet 通过、12 包测试全绿;前端 vue-tsc + build 通过、 node --test 24/24;console check-syntax 12 文件通过。
147 lines
10 KiB
Markdown
147 lines
10 KiB
Markdown
# Scoreboard Console
|
||
|
||
赛事积分榜系统的桌面宿主(Electron)。它只做六件事:
|
||
|
||
1. 拉起并守护同目录(或配置指定)的 `scoreboard.exe`,把它的 stdout/stderr 接管到管理窗口与日志文件;
|
||
2. 用 `screen.getAllDisplays()` 枚举显示器,把大屏页面投到扩展屏;
|
||
3. `powerSaveBlocker.start('prevent-display-sleep')` 防熄屏;
|
||
4. 提供启动/停止/状态/日志的管理 GUI 和托盘菜单;
|
||
5. 打包后随系统启动,现场断电重启后无需人工干预;
|
||
6. 在服务管理窗口展示服务端在线的大屏实例(`GET /api/display/instances`)。
|
||
|
||
规范来源:`devdocs/06-大屏端设计.md` 第 6 节、`devdocs/10-部署与现场方案.md` 第 4 节。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
console/
|
||
├── package.json # main 指向 main/index.js;electron-builder 配置
|
||
├── config.json # 运行配置(端口 / exe 路径 / 选屏策略)
|
||
├── main/
|
||
│ ├── index.js # 应用入口:窗口 / 托盘 / IPC / 生命周期
|
||
│ ├── config.js # 配置加载与合并、exe 路径解析
|
||
│ ├── service.js # 服务端子进程守护:spawn / 日志转发 / 健康检查 / 自动重启
|
||
│ ├── instances.js # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
|
||
│ ├── account.js # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
|
||
│ ├── displays.js # 大屏窗口管理:选屏、kiosk、排布、24h 重载
|
||
│ ├── log-bus.js # 日志环形缓冲 + 落文件 + 广播
|
||
│ └── tray-icon.js # 托盘图标(base64 内嵌,无外部资源文件)
|
||
├── scripts/
|
||
│ └── check-syntax.js # pnpm check:跨平台递归 node --check
|
||
├── extra/ # 打包前由 deploy/build.bat 暂存 使用说明.txt(extraFiles 指向它)
|
||
└── renderer/
|
||
├── index.html # 管理窗口(含自绘标题栏)
|
||
├── styles.css
|
||
├── app.js
|
||
├── preload.js # 管理窗口的 contextBridge 白名单
|
||
└── shell-preload.js # 服务端管理面板窗口的 window.desktopShell 白名单
|
||
```
|
||
|
||
## 运行方式
|
||
|
||
```bash
|
||
cd console
|
||
pnpm install # 首次需要下载 Electron
|
||
pnpm start # 等价于 electron .
|
||
```
|
||
|
||
开发时把 `scoreboard.exe` 放在以下任一位置都能被找到(`config.json` 里的相对路径会按顺序探测):
|
||
|
||
1. `console/`
|
||
2. 当前工作目录
|
||
3. 仓库根目录
|
||
4. `dist/`
|
||
|
||
现场(部署包)形态:
|
||
|
||
```
|
||
scoreboard-windows-amd64/
|
||
├── Scoreboard Console.exe
|
||
├── resources/app.asar # 内含 main/ renderer/ config.json
|
||
├── config.json # exe 旁边的可编辑配置
|
||
├── scoreboard.exe
|
||
├── config.yaml
|
||
├── rules/
|
||
├── data/
|
||
└── logs/console.log # 运行时生成
|
||
```
|
||
|
||
打包后 `config.json` 读取顺序:`SCOREBOARD_CONSOLE_CONFIG` 环境变量 → exe 同目录的 `config.json`。
|
||
|
||
## 行为说明
|
||
|
||
| 场景 | 行为 |
|
||
| --- | --- |
|
||
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏 |
|
||
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,窗口 `setIgnoreMouseEvents(true)`,光标由页面 CSS 隐藏 |
|
||
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
|
||
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
|
||
| 防熄屏 | Console 启动即生效,退出时释放;前台后台都算 |
|
||
| 服务端就绪 | 首次 `/api/health` 通过后开大屏窗口;等待超过 `display.waitForServerMs` 也先开,避免现场只见桌面 |
|
||
| 服务端崩溃 | 按 `restartDelayMs` 递增退避自动重启(`maxRestarts=0` 为无限);持续健康 `healthyResetMs` 后崩溃计数清零;恢复健康后自动重载大屏 |
|
||
| 停止服务 | 先 `POST server.shutdownPath` 并带 `X-Shutdown-Token`(启动时用 `crypto.randomBytes(32)` 生成,经环境变量 `SCOREBOARD_SHUTDOWN_TOKEN` 只交给该子进程)请求服务端优雅关闭;受理后最多等 `shutdownTimeoutMs`,超时或接口不可用(404/连接失败)再强杀 |
|
||
| 已连接大屏实例 | 管理窗口打开时每 5 秒读一次服务端 `GET /api/display/instances`(复用管理面板的 `scoreboard_session` Cookie,需记录员及以上),显示实例数、页面名与在线状态;窗口隐藏即停表,未登录/服务未启动时显示占位文案 |
|
||
| 关管理窗口 | 只隐藏到托盘,服务端继续运行;退出必须走托盘「退出」 |
|
||
| 开机自启 | 打包后在 Windows/macOS 上注册(`startup.openAtLogin`,默认开),开发态不注册 |
|
||
| 窗口外观 | 管理窗口与服务端管理面板窗口都是 `frame: false` 无边框,标题栏与最小化/最大化/关闭三键由页面自绘(拖拽区 `-webkit-app-region: drag`,按钮 `no-drag`,双击标题栏切最大化,关闭键 hover 变红) |
|
||
| 服务端管理面板 | 用独立无边框 Electron 窗口加载 `http://127.0.0.1:<port>/admin`(`shell-preload.js` 暴露 `window.desktopShell`),不再调系统浏览器;面板页在浏览器里直接打开时标题栏自动不渲染 |
|
||
| 管理员账号 | 主进程 `spawn` 同目录 `scoreboard.exe passwd -config <同一份配置> -user <当前用户名> -new-username <新用户名> -password <新密码>`(留空 = 不改 / 随机生成)。本机进程、不经 HTTP,因此免登录验证;用户名/密码含空格、制表符或换行会被主进程拒绝,密码不进日志。`passwd` 会作废该用户全部会话,UI 提交前二次确认并提示 |
|
||
| 日志 | 内存环形缓冲(`log.maxLines`)+ 文件 `logs/console.log`(超 `maxFileBytes` 轮转一份 `.1`,轮转前会删掉旧 `.1` 以兼容 Windows) |
|
||
|
||
## 配置项(`config.json`)
|
||
|
||
| 键 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `server.exe` | `scoreboard.exe` | 相对路径按上文顺序探测,也可写绝对路径 |
|
||
| `server.args` | `["-config","config.yaml"]` | 传给服务端的参数 |
|
||
| `server.host` / `server.port` | `127.0.0.1` / `19540` | 健康检查、关闭握手与管理面板地址(`0.0.0.0` 会自动换成回环地址探测) |
|
||
| `server.autoStart` | `true` | Console 启动后是否立刻拉起服务端 |
|
||
| `server.healthIntervalMs` | `5000` | 健康检查间隔 |
|
||
| `server.restartDelayMs` | `3000` | 重启基础退避,每次翻倍,最多 5 倍 |
|
||
| `server.maxRestarts` | `0` | 0 = 无限重启 |
|
||
| `server.shutdownPath` | `/api/shutdown` | 优雅关闭握手接口(需服务端实现并校验 `X-Shutdown-Token`),不可用时回退强杀 |
|
||
| `server.shutdownTimeoutMs` | `5000` | 服务端受理关闭请求后等待其自行退出的上限,超过则强杀 |
|
||
| `server.healthyResetMs` | `60000` | 连续健康多久后清零崩溃计数 |
|
||
| `display.selection` | `all-secondary` | `all-secondary` / `all` / `specified` |
|
||
| `display.displays` | `[]` | `specified` 时生效,填 display id 或 0 起序号 |
|
||
| `display.pages` | `["standings","medals","individual","results"]` | 按屏序轮转锁定;只填大屏支持的页名 |
|
||
| `display.pageParam` | `page` | 页面 URL 参数名 |
|
||
| `display.kiosk` | `true` | 多屏时是否 kiosk(单屏恒窗口化) |
|
||
| `display.ignoreMouse` | `true` | 多屏窗口是否忽略鼠标 |
|
||
| `display.reloadEveryHours` | `24` | 定时重载兜底,0 = 关闭 |
|
||
| `display.waitForServerMs` | `20000` | 服务端未就绪时开大屏的兜底等待 |
|
||
| `display.backgroundColor` | `#0b0f1a` | 窗口底色,减少加载白闪 |
|
||
| `startup.openAtLogin` | `true` | 打包后随系统启动(开发态不注册) |
|
||
| `admin.width` / `admin.height` | `1040` / `760` | 管理窗口初始尺寸(服务端管理面板窗口按 `width` 不小于 1100 取用) |
|
||
| `log.maxLines` / `log.file` / `log.maxFileBytes` | `2000` / `logs/console.log` / `4194304` | 日志缓冲与文件 |
|
||
|
||
配置缺项自动用内置默认值,文件不存在或 JSON 语法错误时回落到全默认值,并在管理窗口顶部给出提示。
|
||
|
||
## 打包(electron-builder)
|
||
|
||
`package.json` 里已写好 `build` 段:
|
||
|
||
```bash
|
||
cd console
|
||
pnpm install
|
||
pnpm check # 跨平台语法检查(scripts/check-syntax.js)
|
||
pnpm dist # electron-builder --win --x64 → ../dist/console/
|
||
pnpm pack # 只出目录,不生成安装包,便于现场调试
|
||
```
|
||
|
||
- 产物:NSIS 安装包 + zip(`Scoreboard Console-0.1.0-x64.*`)。
|
||
- `extraFiles` 会把 `../dist/scoreboard.exe`、`../config.yaml`、`config.json`(exe 旁的可编辑配置,缺失时 Console 会回落到内置默认值)与 `../rules` 拷到安装目录,最终形成第 4 节里的部署包形态。
|
||
- `使用说明.txt` 是可选的:`deploy/build.bat` 在打包前依次找 `docs/使用说明.txt`、`docs/使用手册.md`,命中就暂存到 `console/extra/使用说明.txt`(`extraFiles` 会把该目录内容平铺到部署包根目录),都没有就跳过并在构建日志里注明。
|
||
- 图标在 `resources/icon.ico`(`package.json` 的 `win.icon` 指向它)。
|
||
- 已在 Windows 构建机上通过 `deploy\build.bat` 实跑(含 `pnpm dist`,产出 NSIS 安装包 + zip);Linux 开发机未安装 Electron/electron-builder。
|
||
|
||
## 尚未实机验证
|
||
|
||
以下内容只做了静态语法检查,需要在 Windows + 真实多显示器环境跑一遍:
|
||
|
||
- 多屏 kiosk 定位与 `display-metrics-changed` 重排(改分辨率 / 缩放 / 插拔 HDMI)。
|
||
- `powerSaveBlocker` 在长时间后台下的实际效果。
|
||
- 托盘图标在 Windows 任务栏的显示效果(16px 缩放)。
|
||
- `scoreboard.exe` 真实拉起、日志转发、健康检查与崩溃重启的端到端链路。
|
||
- electron-builder 打包已通过 `deploy\build.bat` 实跑;NSIS 安装包在真机上的安装 / 卸载流程尚未验证。
|