Files
AskaEth 9c52dc4b38 feat(display): 大屏「赛事日程侧边栏」(手填日程 + 竖向进度轴 + 热开关 + 每屏覆盖);版本 dev_0.5.16
主人点名要的功能:大屏控制页加一个**可热开关**的侧边栏,在**指定的屏幕**上(不管它当前在哪一页)叠一个
**手填的赛事日程列表**,带**竖向进度条**;日程**不与登记的小项时间关联**、完全手填;竖向时间轴要好看,
**字多时字号自动缩放与换行**要做到位(不能过小、不能过大)。

设计口径(主人逐条拍板):放不下时**围绕当前项开窗**;进度条是**一条竖向时间轴 + 三态节点**;
**深色半透明 + 毛玻璃**;已完成的项**淡化保留**;备注**默认上屏 + 一个总开关**;
明细覆盖层 / 全屏公告期间**自动收起**、结束后自动回来;编辑权限**录入员及以上**;单屏覆盖按**实例 id**。

服务端(走大屏公告那条路 —— PUT /api/settings 是 admin-only,会与「录入员可改」冲突)
- 新增 `schedule` 动作 + 白名单;`POST /api/display/command` 同一个 handler:**先校验 → 再转发 → 最后落库**
  (校验排在转发前,非法配置不会在大屏上闪一帧)
- 落库到赛事库 settings 的 `display.schedule`(**不新建表、不写迁移**,随赛事包走);
  与公告共用那份「写设置键 + bumpVersion」的实现
- **读写不对称容错**:写侧严格 400(含中文提示与第几条),读侧坏数据一律静默当空 —— 老数据 / 手改库不会炸大屏
- 上限:40 条;time ≤20 / title ≤40 / note ≤60(按字符,清洗控制字符);宽度夹 15–40;覆盖 ≤50 条
- 口径收口三处:**时间允许为空**(与管理端同口径)、**showNote 缺键 = true**(区分「键不存在」与「显式 false」)、
  **`inherit` 不落库**(不再白占覆盖预算)

大屏端
- 新增 `ScheduleSidebar.vue` + `schedulePanel.ts`(纯函数:开窗 / 三态 / 整体进度 / 字号与行数 / 按显示宽度截断 /
  单屏覆盖 / 抑制 / 指纹去重)
- 字号是**舞台设计坐标**(1920×1080),靠 `.stage` 的 `transform: scale()` 整体缩放,**不做二次缩放**
  (wiring 断言禁止出现 `window.`/`innerWidth`/`vh`);复用既有的 `fitRowFontPx`(**16–56px 硬上下限**)与 `textEm`;
  固定点迭代 ≤4 轮;**贴下限时宁可行数少也不缩字**;标题 ≤3 行、备注 ≤2 行,超出截断
- 容器 `position:absolute; inset:0; z-index:50`(**不用 fixed** —— `.stage` 带 transform,是 fixed 后代的包含块);
  **独立于 detail 状态机**(goto 会关明细、明细会停轮播,侧边栏必须不受影响)
- 深色半透明 + `backdrop-filter`,并带 `@supports not (backdrop-filter: blur(1px))` 退化为更不透明的深底
- 关着时**不留任何 DOM**(逐字节等同于没有这个功能);`prefers-reduced-motion` 下不做动效

管理端
- 大屏控制页(左列、公告之后)新增「赛事日程」卡片:条目表格(时间/标题/备注/已完成 + 上移/下移/删除)、
  总开关、备注上屏、位置与宽度、**每块屏三态覆盖**(跟随 / 强制开 / 强制关;切回「跟随」会真删掉那条键)
- 保存走 `adminApi.displayCommand`(**不走 admin-only 的设置接口**);本地校验不过**一个请求都不发**
- 排序只有上移 / 下移(该页面禁止挂 pointerdown/mousedown 监听,有既有测试钉着)

测试
- 服务端 16 个包全绿;前端 **1174 例**全过(基线 1096,本批 +78:大屏端 44 + 管理端 34)
- 大屏端另有 **40 项 playwright 实机**(用 `page.routeWebSocket` 假造实时通道):开/关后 DOM 无残留、
  五个页面下都在、`lock=0` 真等轮播翻页且侧边栏不掉、明细与公告期间自动收起并自动回来、
  **1080p 与 4K 字号完全相同**、`reducedMotion` 下无动画;并钉住两条硬指标(真机列表高度 ≥ 纯函数预算、
  容量×行高 ≤ 真机高度)

文档:`docs/升级说明` 新增 `dev_0.5.16`;`使用手册` 9.2 + 17.6;`常见问题` Q108–Q111;`上线检查清单`/`部署指南`
各一条;`devdocs/05·07·13` 同步。

⚠️ 一条**既有边界**(本版故意没改,按主人「兼容性最大化」的要求):导出的名次口径是「通道名次优先」、
屏幕是「成绩行名次优先」,只在「先按决赛计分发布、之后又只更新名次发布一次」这种序列下分叉
(导出显示上次计分的名次)。正常赛程遇不到;遇到时对该组再「按决赛成绩发布」一次即恢复一致。
已写进《升级说明》④ 与《常见问题》Q111。
2026-09-17 20:04:50 +08:00

761 lines
68 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署指南
> 面向现场操作人员与管理员。本系统完全离线运行,不需要联网,不需要安装任何运行时。
---
## 1. 运行环境要求
| 项目 | 要求 |
| --- | --- |
| 服务端主机 | Windows 10 / 11,64 位(amd64) |
| 大屏 | 通过 HDMI 接入主机;显示模式设为**扩展**;推荐 16:9、1920×1080 或 3840×2160 |
| 网络 | 完全离线即可运行。若需要手机/平板/电视访问,用独立交换机或路由器组成局域网 |
| 磁盘 | 建议预留 ≥ 10GB |
| 运行时 | 无需安装 Go / Node / 数据库等任何运行时,前端已内嵌进可执行文件 |
| 权限 | 首次运行会在 `%LOCALAPPDATA%\ScoreboardConsole\` 下建数据目录并写入配置/规则(见 3.5),并在其下 `data\logs\` 写服务端日志(见第 10 节);Console 自己还在部署目录的 `logs\` 写 `console.log`,请确保这两处可写 |
> 服务端只构建 Windows amd64 一个版本。Linux / 其他平台不在交付范围内。
---
## 2. 获取安装包
在**构建机**(具备 Go、Node.js、pnpm、git 环境,并能访问依赖源)上执行:
```bat
deploy\build.bat
```
脚本依次完成:拉取代码 → 构建前端 → 构建服务端 → 打包桌面宿主。产物落在仓库的 `dist\` 目录:
| 产物 | 类型 | 说明 |
| --- | --- | --- |
| `dist\scoreboard.exe` | 服务端 | 前端已通过内嵌方式打进可执行文件,单独运行也能提供全部页面 |
| `dist\console\Scoreboard Console-<版本>-x64.exe` | 安装包 | NSIS 安装程序,双击按向导安装 |
| `dist\console\Scoreboard Console-<版本>-x64.zip` | 免安装包 | 解压即用 |
| `dist\console\win-unpacked\` | 目录 | 未压缩的运行目录,便于现场调试 |
> 版本号来自仓库根的 `VERSION` 文件:构建时由 `deploy\build.bat` 注入服务端二进制(`-ldflags -X main.version`,运行期不再读任何文件),并同步到 `console\package.json` 决定安装包/免安装包的文件名。**本文不写死版本号——以仓库根 `VERSION` 与 `dist\console\` 下的实际文件名为准**,运行期可在管理端「关于」页与 `GET /api/health` 里看到(没走构建脚本直接 `go build` 时兜底显示 `dev`,不编造版本号)。升级版本只改 `VERSION` 一处。
>
> 目标机器**不需要** Go / Node / pnpm,也不联网;上述工具只用于构建机。
>
> 安装包与免安装包解出来的根目录里,除程序与配置模板外还平铺着两份用户文档 `使用说明.txt`、`报名名单导入规范.txt`(由构建脚本从 `docs\` 暂存,见 3.3)。
---
## 3. 安装与首次启动
### 3.1 使用安装包
1. 双击 `Scoreboard Console-<版本>-x64.exe`(版本以 `dist\console\` 下实际文件名为准)。**安装程序一启动就会申请管理员权限**:Windows 会先弹一次 UAC(「是否允许此应用对你的设备进行更改?」),点「是」。**受限账户(不是管理员)的机器不会弹** —— 它拿不到「为所有用户」那一档,安装程序会自动降级为「仅为我安装」(见第 2 步)。
2. 按向导选**装法与安装目录**:
- **「为所有用户安装」**(新版默认):装到 `C:\Program Files\Scoreboard_Console`,并在这一趟**自动放行防火墙入站 19540**(防火墙里能看到规则 `Scoreboard Console (TCP 19540)`,卸载时自动删掉)。**升级时不用手工关掉旧版本** —— 安装程序会自己找到并关闭在跑的旧版本(含它拉起的 `scoreboard.exe` 服务端进程),见 3.4。
- **「仅为我安装」**:装到当前用户的目录(`%LOCALAPPDATA%\Programs\Scoreboard_Console`;受限账户会落到这一档);**不加防火墙规则**,局域网大屏要照 7.5 手工放行一次。
- **安装位置**:向导里**默认填这台机器上次装的目录**;切换上面两种装法时,目录输入框会跟着变成该模式的默认目录;**手工改过路径(或点过「浏览…」)之后再切模式也不会冲掉你填的** —— 想回到模式默认目录,把路径清空重新切一次模式。
3. 安装完成后,桌面会出现名为**「赛事积分榜」**的快捷方式(右键 →「属性」→ 看「目标」,可确认这次装到了哪个目录)。
4. 双击「赛事积分榜」启动。
> 赛事数据、配置与规则都存放在 `%LOCALAPPDATA%\ScoreboardConsole\`(不随安装目录走,见 3.4、3.5);服务端日志在数据目录的 `data\logs\` 下,Console 自己的 `console.log` 在**安装目录**的 `logs\` 下(见第 10 节),所以**安装目录要当前账户写得进去**。⚠️ 新版安装器默认装到 `C:\Program Files\Scoreboard_Console`;如果装完发现「运行日志」面板一直是空的、安装目录下没有 `logs\console.log`(多半就是写不进去),**改装到一个当前账户可写的普通目录**(例如 `D:\Scoreboard`)重装一次即可。
### 3.2 使用免安装包
1. 把 `Scoreboard Console-<版本>-x64.zip`(版本以 `dist\console\` 下实际文件名为准)解压到任意目录(建议使用纯英文路径)。
2. 双击目录里的 `Scoreboard_Console.exe` 启动。
### 3.3 首次启动会自动完成
- 拉起并守护服务端 `scoreboard.exe`;
- 在数据目录(默认 `%LOCALAPPDATA%\ScoreboardConsole\data`)下自动建库、建目录;
- 打开服务端管理面板窗口;
- 把大屏页面投到扩展屏(只接一块屏时开成窗口化窗口,按 F11 可切全屏;见第 7 节);
- 启用防熄屏;
- 注册开机自启登录项但**不启用**(任务管理器「启动」里可见、状态为「已禁用」);需要随系统启动时,在服务管理窗口的「开机自启」开关里打开(仅打包后的 Windows 版本生效)。
部署目录结构(安装或解压后):
```
安装目录\
├── Scoreboard_Console.exe # 桌面宿主,双击启动
├── resources\app.asar # Console 自身资源
├── scoreboard.exe # Go 服务端
├── scoreboard.png # 应用图标
├── config.json # Console 配置模板(首次启动复制到用户配置目录)
├── config.yaml # 服务端配置模板(同上)
├── rules\ # 规则包模板(同上)
├── 使用说明.txt # 用户手册,记事本直接打开
├── 报名名单导入规范.txt # 报名表填写规范,可直接转发给各单位填表人
└── logs\console.log # 首次运行生成:运行日志
```
> 根目录里那两份 `.txt` 与 `scoreboard.exe`、`config.yaml`、`config.json`、`scoreboard.png`、`rules\` 平铺在同一层,是构建时从 `docs\` 暂存进包的用户文档:`使用说明.txt` 即使用手册,`报名名单导入规范.txt` 可直接发给填表人(两者都按纯文本存放,用记事本打开即可,中文不会乱码)。改动了仓库里的 `docs\` 文档后需要**重新打包**才会更新到包内。
配置、规则与数据**都不在安装目录里**,各是独立的一处(详见 3.4、3.5):
```
%LOCALAPPDATA%\ScoreboardConsole\ # 用户数据目录(与安装目录无关)
├── data\ # 数据库、备份、上传的图片、服务端日志(data\logs\)
├── console\config.json # Console 自己的配置(实际生效的那份)
├── config.yaml # 服务端配置(实际生效的那份)
└── rules\ # 规则包(实际生效的那份)
```
> 服务端日志默认落在 `data\logs\` 下(`operations\` 放行类、`interceptions\` 拦截类,见第 10 节),所以它同样在安装目录之外、覆盖安装不会丢。
### 3.4 数据放在哪里(升级/卸载都不会删)
覆盖安装时安装程序会**先静默运行旧版卸载器**,而卸载器会把整个安装目录删掉,所以数据库、规则、用户、上传的图片与字体都不能放在安装目录里。**例外**:用 `安装包.exe /S` 做的**静默安装会跳过旧卸载器**(旧卸载器在无人值守的会话里可能弹一个没人能点的提权框)—— 但这只影响静默安装那一条路,手工卸载与交互式升级照样会删安装目录,所以数据仍然必须放在安装目录之外。现在的规则:
| 场景 | 数据目录 |
| --- | --- |
| 默认(`console\config.json` 的 `server.dataDir` 留空,该文件见 3.5) | `%LOCALAPPDATA%\ScoreboardConsole\data`,即 `C:\Users\<用户名>\AppData\Local\ScoreboardConsole\data` |
| 想放到别处,例如 D 盘 | 在 `%LOCALAPPDATA%\ScoreboardConsole\console\config.json` 里把 `server.dataDir` 改成 `"D:\\scoreboard-data"`(绝对路径),或相对安装目录的路径 |
| 直接双击 `scoreboard.exe`(不经 Console) | 服务端默认行为不变:用工作目录下的 `data\`,**这种用法在覆盖安装时仍会丢数据** |
- Console 启动时会把这个目录用 `-data` 参数交给服务端,界面上方「运行日志」里能看到 `数据目录:...` 这一行;
- **服务端日志也在数据目录下**:`<数据目录>\logs\` 里分 `operations\`(放行类)与 `interceptions\`(拦截类)两个子目录,按天 + 按大小轮转、自动清理旧文件,可用 `config.yaml` 的 `log:` 段改位置与保留份数(见第 10 节)。它在安装目录之外,覆盖安装不会丢;
- **恢复未完成标记**:数据目录根部的 `pending_restore.json` 只在「一键恢复」**中途异常中断**时留下(正常走完会被清掉),里面记着备份文件名、目标库、恢复前的安全副本与操作员。服务端下次启动会读它并在运行日志里给一条 WARN(含处置建议),**只读不删**——它是现场判断「这台机器上发生过一次没走完的恢复」的唯一线索。平时这个文件不存在,看到它就该按日志提示核对数据库;
- **上传资源随赛事项目走**:赛事会标、大屏背景图、单位 logo,以及「赛事信息 → 字体」里上传的**字体文件**,都在 `<数据目录>\event_projs\<项目名>\uploads\` 里(字体在 `uploads\fonts\`)。它们是文件而不是数据库记录,**不在 `backups\` 的 `.db` 快照里**(`.db` 快照与「一键恢复」都只动数据库,不含这些文件);备份 / 迁移时要连这个目录一起拷(见 8.5、8.6)。构建/开发机上的 `deploy\backup.bat` 会额外把每个项目的 `uploads\` 打成 `*-uploads-<时间戳>.zip`,但那份 zip 不在服务端的 20 份滚动保留里,要手工清理;
- **一次性迁移**:如果新数据目录还是空的,而旧的 `<安装目录>\data` 里有数据,Console 会在启动时把旧目录**整体改名**过去(同盘很快),日志里会写「已把旧数据从 X 迁移到 Y」。任何失败(跨盘、权限不足等)都只记日志,旧数据原地保留,不会被删;
- 如果两个目录里**都有数据**,Console 不做任何动作,只写一条警告日志,由现场人工决定保留哪一份;
- 迁移只在安装包版本(打包后)执行一次,`pnpm start` 之类的开发运行不会碰仓库里的 `data\`;
- 迁移的**旧目录只认 `<安装目录>\data`**。如果你已经把数据迁到默认位置、之后又想改成 D 盘等其他目录,Console **不会**自动搬运:请先停服务,把 `%LOCALAPPDATA%\ScoreboardConsole\data` 整个目录复制到新路径,再改 `server.dataDir` 并重启 Console。
> 升级到本版本后第一次启动,请打开 Console 的「运行日志」确认是否有迁移记录,并核对管理端里的赛事/账号数据是否完整。
>
> **升级前先看 [《升级说明》](./升级说明.md)**:里面按版本写了「这一版改了什么、升级后要补做什么、分数或奖牌为什么会变化」。本版(dev_0.1.8)有一条**需要人工补做的事**(「个人+团体」项目的团体名次)。
**升级后奖牌榜可能短暂是空的(正常,会自己补齐)**
本版给赛事库加了一条新迁移(**0020**),并新增了一张存"通道名次"的表与三个奖牌开关列。迁移**只建表、不回填**历史名次,所以**老库升级后、服务端做第一次重算之前,奖牌榜(金 / 银 / 铜)会是 0**——这不是数据丢了。
服务端**下一次启动时会自动做一次全量重算**把名次补齐(这次重算在开始对外提供服务之前跑完,所以大屏不会先亮出一帧全 0)。在 Console「运行日志」或 `<数据目录>\logs\operations\` 的当天日志里能看到这两行,**是正常的,不是报错**:
```
检测到通道名次为空但已有已发布成绩,正在执行一次性全量重算
一次性全量重算完成,通道名次与奖牌榜已补齐
```
- 通常**几秒内**跑完(成绩多时稍久,日志里会写处理了几个小项与耗时);跑完后刷新管理端 / 大屏,奖牌就回来了;
- 这是**一次性**的:表里已经有名次(说明重算过)之后不会再触发;
- 若日志里出现 `通道名次自愈失败,奖牌榜可能仍为空;可重新发布成绩或导入规则包触发重算`,说明这次重算没做完(不影响服务启动):**随便发布一次成绩**,或**重新导入一次规则包**(会全量重算),奖牌同样会回来。升级前如果做过备份,先确认榜单数字与升级前一致再开赛(见《上线检查清单》)。
- 升级命令与首次启动的完整流程见 3.4 开头;这条迁移是**加列 / 建表**,可重复执行,不会改动任何既有数据行。
### 3.5 配置与规则放在哪里(升级/卸载也不会丢)
`config.json`、`config.yaml`、`rules\` 以前跟着安装包落在安装目录里,而**安装目录会被卸载器整个删掉**(覆盖安装会先静默跑一遍旧卸载器),所以用户改过的配置和规则包会一起没。本版本起它们也搬到**用户数据目录**(与数据目录同一处,安装目录里那三份只当模板):
| 内容 | 实际生效的位置 | 管什么 |
| --- | --- | --- |
| Console 配置 | `%LOCALAPPDATA%\ScoreboardConsole\console\config.json` | 端口、选屏策略、开机自启等(见 6.2) |
| 服务端配置 | `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` | 监听地址/端口、备份间隔、日志级别与日志目录(见 6.1) |
| 规则目录 | `%LOCALAPPDATA%\ScoreboardConsole\rules\` | 规则包 YAML;管理面板「导出 → 规则包 → 选择 YAML」时从这里选 |
**怎么改**:直接编辑上表里的文件。`config.json` 与 `config.yaml` 都是 Console 启动时读一次,改完要**退出并重启 Console**(托盘 → 退出);服务管理窗口的「配置文件」一行显示的就是 `console\config.json` 的实际路径,可用来确认。`rules\` 只是规则包存放目录,往里放文件不需要重启,但规则要在管理面板「导出 → 规则包 → 选择 YAML → 预览差异 → 确认导入并全量重算」里导入后才生效(服务端不会自动加载)。安装目录里那三份是模板,改了不生效。
升级/覆盖安装时:
- **首次运行会复制**:用户目录里还没有、安装目录里有 → 原样复制一份过去,安装目录那份留着当模板;
- **已存在就绝不覆盖**:用户目录里已经有一份时一律用它,Console 既不覆盖也不删除,之后升级只换程序不换配置;
- Console 拉起服务端时传**绝对路径** `-config <用户目录>\config.yaml`,`passwd` 子命令用同一份(见 5.5),不会两边各读各的;
- 规则目录只在首次复制一次,之后你往里面放的规则包不会被模板顶掉;
- 某一处建不出来或不可写(权限、杀毒软件拦截等)时**只记日志并回落到安装目录里那份**,绝不因此启动失败;代价是那份改动仍会在覆盖安装时丢失,日志里会明确写出来;
- 开发运行(`pnpm start`)不接管这三项,仍用仓库里的 `console\config.json`、`config.yaml`、`rules\`;
- 不用 Console、直接双击 `scoreboard.exe` 时,服务端仍按工作目录下的 `config.yaml` 读配置,**不受这套保护**;
- 上传的图片与字体不在这三项里,位置见 3.4:它们随赛事项目存放在数据目录的 `event_projs\<项目名>\uploads\`(字体在 `uploads\fonts\`),同样在安装目录之外,覆盖安装不会动它们;**但它们不在 `backups\` 的数据库快照里**(「一键恢复」也只换数据库文件,不会带回图片),备份 / 迁移时要连这个目录一起拷(见 8.6)。
> 从老版本覆盖升级上来的第一次启动:安装目录里那三份在覆盖安装时**就已经被旧卸载器删过一遍**(新装进去的是新版模板),Console 只能把模板复制到用户目录,升级前改在安装目录里的配置需要重做一遍。**从本版本起**(也就是改动落在上表位置之后),配置与规则才会跨升级保留。
> 日志分两处,别找错:Console 自己的排查日志留在安装目录的 `logs\console.log`(每次运行都会重写,不值得搬,卸载后会消失);**服务端的运行 / 放行 / 拦截日志在数据目录的 `logs\` 下**(`operations\` 与 `interceptions\` 两类,随数据目录跨升级保留,见第 10 节)。
首次启动后看 Console「运行日志」里这几行核对:
| 日志 | 含义 |
| --- | --- |
| `已把 Console 配置文件 复制到 …` | 三项各自的首选复制记录,各一条,属正常 |
| `用户数据目录:<路径>` | 本次实际使用的用户数据目录 |
| `警告:准备 … 失败…` | 该项回落到安装目录里那份,按日志里的路径手工复制过去再重启 Console |
| `警告:拿不到 %LOCALAPPDATA%…` | 系统环境异常,三项都还在安装目录里,覆盖安装会丢改动 |
---
## 4. 服务管理窗口(Scoreboard Console)
启动后首先看到的是「Scoreboard Console · 启动管理」窗口。
### 4.1 窗口按钮
| 位置 | 按钮 / 项 | 作用 |
| --- | --- | --- |
| 服务端状态 | **启动服务** | 手动拉起服务端 |
| 服务端状态 | **停止服务** | 优雅停止服务端(会先请求服务端关闭,超时后强制结束) |
| 服务端状态 | **重启服务** | 停止后重新启动 |
| 服务端状态 | 状态行 | 已停止 / 启动中 / 运行中 / 停止中 / 异常退出 |
| 服务端状态 | 服务地址、进程 PID、自动重启、防熄屏 | 实时运行信息 |
| 服务端状态 | 服务端程序、配置文件 | 实际使用的可执行文件与配置路径 |
| 大屏窗口 | **重新排布** | 重新枚举显示器并重开大屏窗口 |
| 大屏窗口 | **打开服务端管理面板** | 用内置窗口打开 `http://127.0.0.1:<端口>/admin`,无需再开浏览器 |
| 运行日志 | **自动滚动** | 日志是否跟随最新一行 |
| 运行日志 | **清空** | 清空窗口内显示(不影响日志文件) |
| 已连接的大屏实例 | — | 显示在线大屏窗口的数量与各自页面,5 秒刷新;需先在管理面板登录过(记录员及以上)才能读到 |
| 管理员账号 | **重置账号** | 本机直接调用同目录的 `scoreboard.exe passwd`,自动带上用户目录里的 `config.yaml` 与数据目录,**不需要登录**;会作废该用户的全部登录会话(见 5.5) |
| 标题栏 | 最小化 / 最大化 / 关闭 | 关闭只是**隐藏到托盘**,服务端继续运行 |
### 4.2 托盘用法
- 托盘菜单显示当前服务端状态(如「服务端:运行中 · 就绪」);
- 菜单项:**启动服务**、**停止服务**、**重启服务**、**打开管理端**、**打开服务端管理面板**、**退出**;
- 双击托盘图标 = 打开启动管理窗口;
- **要真正退出程序,必须用托盘菜单的「退出」**;直接关窗口不会停服务端。
> 同一台机器只允许运行一个 Console 实例,重复启动会自动退出,避免抢占端口和大屏。
---
## 5. 首次登录
### 5.1 访问地址
| 用途 | 地址 |
| --- | --- |
| 本机管理面板 | `http://127.0.0.1:19540/admin` |
| 局域网管理面板 | `http://<主机IP>:19540/admin` |
| 大屏 | `http://<主机IP>:19540/display` |
| 导航页 | `http://<主机IP>:19540/` |
> 端口默认 **19540**,以 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` 为准(该文件在哪见 3.5)。
### 5.2 初始账号
- 用户名:`admin`
- 初始密码:**首次启动时随机生成,只打印一次**,数据库里只保存加密后的哈希,无法反查。
### 5.3 从日志里找初始密码
用 Console 启动时,服务端输出会同时显示在「运行日志」面板并写入 `logs\console.log`。初始密码所在的一段输出形如:
```
[server] 初始密码: xxxxxxxxxxxxxxxx
[server] 首次登录后请立即修改密码
[server] === INITIAL ADMIN CREATED (password above) ===
```
在部署目录下执行以下命令即可找到(搜索纯 ASCII 标记行,中文 `findstr` 在 chcp 936/65001 下不可靠):
```bat
findstr /C:"INITIAL ADMIN CREATED" logs\console.log
```
搜到这一行后,初始密码就在它上面几行(形如 `初始密码: xxxxxxxx`)。也可以直接打开 Console 的「运行日志」面板往上翻。
> 如果不用 Console、直接双击 `scoreboard.exe`,密码只会打印在终端窗口里,不会落文件。建议始终通过 Console 启动。
### 5.4 修改密码
- 首次登录后会自动跳到**「修改密码」**页,必须改密才能继续使用;
- 新密码至少 8 位,需输入两次确认;
- 修改成功后所有会话作废,需用新密码重新登录;
- 管理员也可以在「系统设置 → 用户管理 → 重置密码」为其他用户重置。
### 5.5 忘记密码 / 没记下初始密码
**首选:服务管理窗口的「管理员账号」卡片。** 在卡片里填「当前用户名」(默认 `admin`),「新用户名」「新密码」留空即「不改名 / 随机生成」,点「重置账号」,命令输出会直接显示在卡片里。这条路径不需要登录,Console 会自动用上用户目录里的 `config.yaml` 与数据目录。
**命令行方式**:在 Console 里先**停止服务**,然后在安装目录(`scoreboard.exe` 所在目录)执行:
```bat
scoreboard.exe passwd -config "%LOCALAPPDATA%\ScoreboardConsole\config.yaml" -data "%LOCALAPPDATA%\ScoreboardConsole\data"
```
> **`-config` 与 `-data` 都不能省**:`-config` 指到 Console 实际使用的用户配置(安装目录里那份只是模板,见 3.5);`-data` 不加的话服务端会去用工作目录下的 `data\`,改的是另一个库里的账号(那个库是空的,看起来像「密码重置成功但登录不上」)。数据目录改过配置的,把这里换成用户配置里 `server.dataDir` 实际生效的那个路径;实在不确定,可以先看 `logs\console.log` 里 `数据目录:` 那一行。
该命令会为用户 `admin` 生成一个新的随机密码并打印出来,同时要求下次登录强制改密。可选参数:
| 参数 | 说明 |
| --- | --- |
| `-user <用户名>` | 指定要重置的用户,默认 `admin` |
| `-password <新密码>` | 直接指定新密码;留空则随机生成并打印 |
| `-new-username <新用户名>` | 同时修改用户名 |
| `-data <数据目录>` | 数据目录;通过 Console 启动时必须带上同一个值(见第 3.4 节) |
---
## 6. 端口与配置修改
### 6.1 服务端配置 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml`
> 路径说明见 3.5;安装目录里的 `config.yaml` 只是模板,改它不生效。
| 项 | 当前默认值 | 说明 |
| --- | --- | --- |
| `server.host` | `0.0.0.0` | 监听地址;`0.0.0.0` 表示局域网内其他设备也能访问 |
| `server.port` | `19540` | HTTP 端口 |
| `dataDir` | `data` | 数据目录,相对服务端工作目录解析;**Console 启动时会用 `-data <绝对路径>` 覆盖它**,配置项本身只对直接运行 `scoreboard.exe` 时生效 |
| `log.level` | `info` | 控制台与 `operations\` 放行类日志的采集级别:`debug` / `info` / `warn` / `error`;拦截类日志固定按 `debug` 采集,不受它影响(见第 10 节) |
| `log.dir` | 空 | 日志根目录(其下分 `operations\` / `interceptions\`);留空 = `<数据目录>\logs` |
| `log.maxSizeMB` | `20` | 单个日志文件大小上限(MB);`<=0` 用内置默认 |
| `log.keepFiles` | `10` | 每类日志保留份数(含当前文件);`<=0` 用内置默认 |
| `backup.intervalMinutes` | `5` | 自动备份间隔(分钟),`<=0` 关闭自动备份 |
| `backup.keep` | `20` | 每个库滚动保留的备份份数 |
| `server.allowedOrigins` | `[]` | 额外放行的 WebSocket Origin 白名单;默认只放行同源与不带 Origin 的客户端 |
### 6.2 Console 配置 `%LOCALAPPDATA%\ScoreboardConsole\console\config.json`
> 路径说明见 3.5;安装目录里的 `config.json` 只是模板,改它不生效。服务管理窗口的「配置文件」一行显示的就是这份实际路径。
| 项 | 当前默认值 | 说明 |
| --- | --- | --- |
| `server.exe` | `scoreboard.exe` | 服务端可执行文件;相对路径会在部署目录(安装目录)、当前目录、上级目录、`dist\` 依次查找 |
| `server.args` | `["-config","config.yaml"]` | 传给服务端的启动参数;其中 `-config` 的**值**会被换成 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` 的绝对路径,其余参数(如 `-project`)按原样传(见 3.5) |
| `server.dataDir` | `""` | 数据目录;留空 = 用默认位置 `%LOCALAPPDATA%\ScoreboardConsole\data`。填了就按它来:绝对路径直接用,相对路径以安装目录为基准。Console 会把它作为 `-data <绝对路径>` 追加到启动参数后(见第 3.4 节) |
| `server.host` / `server.port` | `127.0.0.1` / `19540` | 健康检查与打开管理面板用的地址;配成 `0.0.0.0` 时本机探测自动改走回环地址 |
| `server.autoStart` | `true` | Console 启动后是否自动拉起服务端 |
| `server.healthIntervalMs` | `5000` | 健康检查间隔(毫秒) |
| `server.restartDelayMs` | `3000` | 崩溃重启的初始退避,之后逐次翻倍 |
| `server.maxRestarts` | `0` | `0` = 无限重启 |
| `server.shutdownTimeoutMs` | `5000` | 等待服务端优雅退出的上限 |
| `server.healthyResetMs` | `60000` | 连续健康多久后清零崩溃计数 |
| `display.selection` | `all-secondary` | 选屏策略,见第 7 节 |
| `display.displays` | `[]` | 仅 `specified` 策略生效,填显示器 id 或 0 起序号 |
| `display.pages` | `["standings","medals","individual","results"]` | 多屏时按屏序轮转锁定的页面;可加入 `live`(实时赛况)等页面值,见 7.4 |
| `display.pageParam` | `page` | 页面 URL 参数名 |
| `display.kiosk` | `true` | 多屏时是否 kiosk 全屏(单屏恒为窗口化) |
| `display.allowClickThrough` | `false` | 多屏 kiosk 窗口是否**允许点击穿透**。默认 `false` = 窗口吞掉鼠标点击:点击不会漏到大屏后面的窗口 / 桌面,被别的窗口盖住后点一下就能回到最前。显式写 `true` 才穿透,代价是**误触会点到后面别的东西**、大屏也点不活。本键是 `display.ignoreMouse` **改名**来的 |
| `display.reloadEveryHours` | `24` | 定时重载兜底,`0` = 关闭 |
| `display.waitForServerMs` | `20000` | 服务端未就绪时,最多等这么久也先把大屏开出来 |
| `display.backgroundColor` | `#0b0f1a` | 大屏窗口底色,减少加载白闪 |
| `startup.openAtLogin` | `false` | 默认注册但不启用(任务管理器可见、已禁用);在服务管理窗口「开机自启」开关里控制 |
| `admin.width` / `admin.height` | `1280` / `860` | 启动管理窗口初始尺寸(服务管理窗口与服务端管理面板共用) |
| `log.maxLines` | `2000` | 内存中保留的日志行数 |
| `log.file` | `logs/console.log` | 日志文件,相对部署目录(安装目录)解析 —— 日志不随配置搬到用户目录 |
| `log.maxFileBytes` | `4194304` | 单个日志文件上限,超出后轮转为 `.1` |
> 配置缺项会自动使用内置默认值;JSON 语法错误时回落到全默认值,并在窗口顶部提示。
>
> 配置文件里若还留着**已废弃的** `display.ignoreMouse`(旧版模板会写 `ignoreMouse: true`),这个键现在**完全不读**,启动时会在 `logs\console.log` 里记一条中文警告(只在启动记一次,不随大屏重排重复刷):旧键已废弃、现在默认不穿透、想穿透改用 `display.allowClickThrough: true`。旧键的值一律忽略,也**不会**被自动改写进你的配置文件。
### 6.3 修改端口
端口必须**两处一致**:
1. 关闭 Console(托盘 → 退出);
2. 修改 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` 的 `server.port`;
3. 修改 `%LOCALAPPDATA%\ScoreboardConsole\console\config.json` 的 `server.port`(保持一致,否则 Console 健康检查和管理面板地址会失效);
4. 重新启动 Console;
5. 之后管理面板地址变为 `http://<主机IP>:<新端口>/admin`。
---
## 7. 大屏接线
### 7.1 主方案:Console 驱动 HDMI
1. 用 HDMI 线连接主机与大屏;
2. 在 Windows「显示设置」里把显示模式设为**扩展**(不要「复制」);
3. 确认分辨率与缩放(推荐 1920×1080 或 3840×2160,缩放按现场效果调整);
4. 启动 Console,大屏窗口会自动打开。
### 7.2 选屏策略(`display.selection`)
| 取值 | 行为 |
| --- | --- |
| `all-secondary`(默认) | 除内置屏外的每块屏各开一个大屏窗口;若所有屏都报告为非内置屏(纯台式机),退回「排除主屏」 |
| `all` | 所有显示器都开大屏窗口 |
| `specified` | 只对 `display.displays` 里列出的显示器开窗;可填显示器 id 或 0 起序号 |
### 7.3 单屏:窗口化 + F11
- 只有一块屏时,大屏以窗口化方式打开(屏幕 80% 大小、居中),并显示自绘标题栏;
- 在窗口内按 **F11** 切换全屏 / 窗口:全屏时自绘标题栏与「实例 · 屏」标识一起隐藏、画面铺满整块屏;再按一次恢复;
- 单屏模式不跟随显示器变化重排;
- 窗口化且未全屏时右上角显示一行「实例 … · 屏 …」供现场核对(数据版本号角标已移除,见 7.7)。
### 7.4 多屏:每屏锁一页
- 除内置屏外的每块屏各开一个 kiosk 全屏窗口;
- 每块屏**锁定一个页面**,按 `display.pages` 的顺序轮转分配;
- 多屏 kiosk 不响应 F11,也不显示任何调试标识;窗口**默认吞掉鼠标点击、不穿透**——点击不会漏到大屏后面的窗口或桌面,被别的窗口盖住后点一下就能回到最前,只有显式配 `display.allowClickThrough: true` 才会让点击穿透(代价是误触会点到后面的东西、大屏也点不活);kiosk 窗口固定置顶在 `screen-saver` 层级,**压得住任务栏**,别的窗口拖到这块屏上也盖不住它;
- 可用的页面值:`standings`(总积分榜)、`medals`(单位奖牌榜)、`individual`(个人积分奖牌榜)、`results`(分项成绩)、`live`(实时赛况)、`announcement`(公告);管理端切页用的是同一批页面的规范名 `team-medals` / `athlete-standings` / `event-results`,两种写法大屏都认。把 `live` 加进 `display.pages`,就能把某块屏固定成「实时赛况」(`live` 也认 `live-results` / `实时赛况` / `实时` / `赛况` 这些别名,与其它页一样)。
- 这一页现在叫 **「单位奖牌榜」**(旧称「团体奖牌榜」容易和「团体项目的奖牌」混淆,已改名);**旧名 `团体奖牌榜` 仍作为别名被接受**,老配置里的 `display.pages`、大屏地址里 `page=团体奖牌榜` 这类老链接都照常生效,不需要改。
### 7.5 在管理端单独控制某块屏
管理端「大屏控制」页有**显示器**卡片,可以给指定显示器单独开窗或收窗。这张卡片只在**从服务管理窗口点「打开服务端管理面板」打开的那个窗口**里可用;用浏览器直接打开管理面板时,卡片会提示此功能不可用。
同一页右侧还有「**局域网大屏**」卡片:把本机可用的局域网地址拼成完整链接(可点开、可一键复制),**第一个地址带一个常驻二维码,其余每条也能点「二维码」按钮临时出码**(点一下显示、再点一次 / 按 Esc / 点空白处收起,同一时刻只留一个),别的设备扫码即可当大屏用。卡片需**录入员及以上**登录才看得到,用法见《使用手册》第 9 节。
- 列出本机每块显示器(名称、分辨率、缩放、是否主显示器、当前有没有大屏窗口、是全屏还是窗口化);
- 先在「窗口页面」里选页面,再对某块屏点「创建全屏窗口」或「创建窗口(窗口化)」;已经开着窗口的屏显示「聚焦窗口」与「关闭窗口」;
- **全屏只对「多显示器 + 非主显示器」开放**:只接一块屏、或选中的是主显示器时只能创建窗口化窗口(全屏会把桌面整个盖住又退不出来)。请求被降级时窗口照开,提示里会写明原因;
- 「重新排布」按 Console 配置的选屏策略重建全部窗口,不在策略内的窗口会被关掉。
副屏的全屏窗口被误关时,用这里的「创建全屏窗口」就能恢复,不必重启 Console。
**「局域网大屏」卡片:怎么拿到地址(两条路)**
1. **卡片自动检测**:「大屏控制 → 局域网大屏」卡片会列出主机上可用的地址,直接点开或扫码;**第一个地址(常驻二维码那一条)就是现场最可能要用的**,其余地址要多一个「二维码」按钮、点一下才出码;
2. **`ipconfig` 手填**:自动检测不到、或者列出来的不是你那张网卡时,在管理员 cmd / PowerShell 里跑 `ipconfig`,把对应网卡的 **IPv4 地址**抄进卡片下方的「手动添加地址」再点「添加」——手填的排在第一条、带「手动」徽标,记在浏览器本地,刷新不丢(最多 10 个,可「删除」)。
**三类地址的含义**(卡片按类别判定,只排除回环 / 未指定 / 组播 / 广播这四类对本机之外没有意义的地址):
| 类别 | 形如 | 现场含义 | 排序 |
| --- | --- | --- | --- |
| 内网地址 | `192.168.x.x`、`10.x.x.x`、`172.16–31.x.x` | 路由器 / 交换机分配或手工设的地址,最常用的现场形态 | 最前 |
| 全局地址 | 其它网段(运营商专线等) | 能用就照常列出,但多半要经中间设备才通,不一定平板能直连 | 居中 |
| **自动配置地址** | **`169.254.x.x`** | **本网段没有 DHCP**,地址是设备自己配的;在独立小网络(没路由器、设备直连或只接交换机)里是**正常用法**,同网段设备可互相访问 | 最后 |
另外两种来源会各带一句提示、**照常能用**:网卡状态读不到时提示「网卡状态读不到(可能是驱动或虚拟网卡),地址本身可用,打不开就换下面一条」;按系统默认路由推断出来时提示「这条地址是按系统默认路由推断出来的(网卡枚举里没有它),地址本身可用,打不开就换下面一条」。
**防火墙要放行入站 19540**(默认端口,改过端口就换成实际端口)。安装包会替你放行,分两种情况:
- 安装时选了**「为所有用户安装」**:**提权那一趟就顺手把入站 19540 放行了**(规则名 `Scoreboard Console (TCP 19540)`),装完不用再管;卸载时会自动把这条规则删掉。
- 安装时选了**「仅为我安装」**:这一趟**不加防火墙规则**(装到用户目录,安装程序只在「为所有用户安装」那一档才去改防火墙),**必须手工放行**。也可以重新跑一次安装程序、这次选「为所有用户安装」,让它自动放行。
> ⚠️ **新版安装器一启动就会申请管理员**(`RequestExecutionLevel highest`,见 3.1):**管理员账户**的机器上,双击安装包先弹一次 UAC,**无论最后选哪种装法都会弹这一次** —— 选「为所有用户安装」才顺手放行防火墙、装到 `C:\Program Files\…`;选「仅为我安装」只装到用户目录、不加规则。**受限账户**的机器不弹 UAC,会自动落到「仅为我安装」那一档,所以这种机器一定要手工放行。
手工兜底(给「仅为我安装」的机器,以及改过端口、装了才发现要放行的机器)——在**管理员** cmd 里跑:
```bat
netsh advfirewall firewall add rule name="Scoreboard 19540" dir=in action=allow protocol=TCP localport=19540
```
注意这条手工规则的名字(`Scoreboard 19540`)与安装程序自动加的那条(`Scoreboard Console (TCP 19540)`)**不一样**:卸载时安装程序只会删掉自己加的那条,手工加的这条要自己 `netsh advfirewall firewall delete rule name="Scoreboard 19540"` 删;两条同时存在只是重复放行,不影响使用。
临时关掉防火墙只能用来**验证**「是不是防火墙挡的」,验证完要恢复;正式开赛前一定用上面这条命令(或改用「为所有用户安装」重跑一次安装程序)放行,而不是一直关着防火墙。
**排障常识**:Windows 里「网线插着、网卡显示 Up」**不等于拿到了 IP**;`ipconfig` 里看到 `169.254.x.x` 说明**本网段没有 DHCP**(地址是设备自配的)——这在**独立小网络**里是正常用法,**不是故障**,同网段的平板 / 手机照样能访问。
### 7.6 公告
管理端「大屏控制 → 公告」推送后:
- 大屏先**全屏**显示这条公告,时长取「滚动时长」(5–120 秒,实际就是全屏展示的秒数),到时自动退回推送前的那一页;
- 公告正文随后留在**大屏底部的公告栏**(底栏固定的「公告」槽位)常驻显示,直到被下一条公告替换或点了「清除公告」才消失;
- 公告正文存在服务端设置里:断线重连、重开大屏窗口都会重新拿到它(新开的窗口会先全屏放 15 秒再落回底栏);点「清除公告」后底栏回到「暂无公告」。
### 7.7 大屏画面里的调试标识与单位 logo
- **调试标识**:只有**窗口化且没有全屏**的大屏窗口才会显示一行「实例 … · 屏 …」;**多屏 kiosk 全屏完全不带调试标识**,单屏窗口化窗口按 F11 全屏后它也会连同自绘标题栏一起隐藏。数据版本号角标 `v<n>` 已移除,任何情况下都不再显示(要核对某块屏是否跟上服务端,看管理端仪表盘「实时连接」里的「版本 N」)。要一个完全干净的展示画面,就用多屏 kiosk;
- **单位 logo**:参赛单位的 logo 会显示在总积分榜、单位奖牌榜、分项成绩表与个人积分榜上(个人榜显示在姓名前);没上传 logo 的单位不占位。logo 在「基础数据 → 参赛单位」里上传,随赛事项目包一起导出/导入(见 8.5)。
### 7.8 大屏窗口的自动行为
| 场景 | 行为 |
| --- | --- |
| 防熄屏 | Console 启动即生效,前后台都算 |
| 服务端就绪 | 首次健康检查通过后开大屏 |
| 服务端迟迟不就绪 | 等待超过 `waitForServerMs`(默认 20 秒)也先开,避免只见桌面 |
| 服务端崩溃 | 按退避策略自动重启,恢复后大屏自动重载 |
| 长时间运行 | 每 `reloadEveryHours`(默认 24 小时)自动重载一次 |
### 7.9 备选方案:安卓 TV / 设备自带浏览器
如果不用 Console 驱动 HDMI,也可以让大屏设备用自己的浏览器访问:
```
http://<主机IP>:19540/display
```
- 这个地址不用手敲:管理端「大屏控制 → 局域网大屏」卡片里有**可复制的完整链接**与**二维码**(**第一条是常驻二维码,其余每条点「二维码」按钮也能临时出码**),别的设备扫码或在浏览器里粘贴即可打开(该卡片需录入员及以上登录才看得到);
- 需要主机设置固定 IP,且设备与主机在同一局域网;
- **主机防火墙要放行入站 19540**(默认端口):**「为所有用户安装」会自动放行**(那一档提权时装好规则),不用再管;**「仅为我安装」不加规则、必须手工放行一次**(命令与说明见 7.5)。装的时候选哪一档,取决于 3.1 里那几行 —— 新版安装器一启动就申请管理员,但只有「为所有用户」那一趟才顺手改防火墙;
- 安卓电视一般常亮,无需额外防熄屏;
- 浏览器方案**不受** Console 的 kiosk、防熄屏、选屏控制,字体与动画表现以实际设备为准;
- 现场以 Console + HDMI 为主方案,浏览器访问作为备用。
---
## 8. 备份与恢复
### 8.1 备份目录
所有备份都在**数据目录**下的 `backups\` 里,主体是 `.db` 快照;用 `deploy\backup.bat` 时还会多出 `*-uploads-*.zip`(上传图片与字体)。默认数据目录是 `%LOCALAPPDATA%\ScoreboardConsole\data`,所以备份在:
```
%LOCALAPPDATA%\ScoreboardConsole\data\backups\
```
数据目录改过 `server.dataDir` 的,按改后的路径找(同目录下的 `backups\`)。该目录不进版本库。
### 8.2 如何备份
| 方式 | 操作 | 产物 |
| --- | --- | --- |
| 自动 | 服务端启动后按 `backup.intervalMinutes`(默认 5 分钟) | `system-auto-<时间戳>.db` 与 `event-auto-<时间戳>.db` |
| 管理端 | 「系统设置 → 备份 → 立即备份」 | `system-manual-<时间戳>.db` 与 `event-manual-<时间戳>.db` |
| 命令行 | 在安装目录执行 `scoreboard.exe backup -config "%LOCALAPPDATA%\ScoreboardConsole\config.yaml" -data "%LOCALAPPDATA%\ScoreboardConsole\data"` | `system-manual-<时间戳>.db` 与 `event-manual-<时间戳>.db` |
| 快捷脚本 | `deploy\backup.bat`(原因记 `manual`)或 `deploy\backup.bat publish`(原因记 `publish`) | 同上(`publish` 时原因为 `publish`);**额外**把每个赛事项目的 `uploads\`(图片 + 上传字体)打成 `<项目名>-uploads-<时间戳>.zip` |
| 发布前 | 每次发布成绩时自动 | `event-publish-<时间戳>.db` |
- 备份使用 SQLite 的 `VACUUM INTO`,服务运行中也能安全执行,不需要停服务,也不依赖外部 `sqlite3.exe`;
- 自动、手动、命令行三种方式都按 `backup.keep`(默认 20 份)滚动保留,`system` 库与赛事库各自计算,超出的旧备份自动删除;
- ⚠️ **`*-uploads-*.zip` 不在 20 份滚动保留范围内**(滚动只认 `.db`):它不会被自动清理,需要手工删旧文件(建议每个项目至少留最新一份)。`deploy\backup.bat` 不归档日志(`logs\`),排查材料要单独拷;空的项目 `uploads\` 会跳过,不产空包;
- 同一时刻只允许一个备份在进行,自动备份与手动、发布前备份不会互相踩;
- `deploy\backup.bat` 会自动用和 Console 相同的配置与数据目录(配置优先 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml`、数据优先 `%LOCALAPPDATA%\ScoreboardConsole\data`,即用户数据目录里那两份;都不存在时回退到脚本上级目录 `..\config.yaml` 与 `..\data`),并分别作为 `-config` / `-data` 传给服务端,所以从哪里运行都不会备份到别的库;
- 现场建议:开幕前、每次发布高峰前、每天结束时各手动备份一次(自动备份只是兜底)。
### 8.3 如何恢复
**首选:管理端「一键恢复」(仅管理员)。** 到「系统设置 → 备份」,在备份列表里找到要退回到的那一份(列表会显示时间、原因、大小;`pre-restore-…` 是恢复前自动留的安全副本),点该行的**「恢复」**:
1. 先过危险确认框(会写明要覆盖的是系统库还是赛事库、会丢弃备份时间点之后的改动);
2. 服务端先**给当前库拍一份「恢复前安全副本」**(`pre-restore-<库>-<时间戳>.db`,不参与 20 份滚动清理,选错时间点还能用它退回;**每类库只保留最近 5 份**,更旧的会在后续恢复时被自动清理),再替换库文件,然后**优雅退出并由 Console 自动拉起**;
3. 页面显示「正在重启服务端…」并自动等待刷新(最多 60 秒;超时就打开服务管理窗口确认状态后手动刷新);
4. **恢复是覆盖式的**:备份时间点之后的所有改动都会丢。恢复系统库还会连**账号与会话**一起换掉,重启后需要用那个时间点的账号重新登录;
5. ⚠️ **恢复不会带回上传的图片与字体**(它们在 `uploads\` 里是文件,不在 `.db` 内);需要一起回退时见 8.6。
**没有 Console 或恢复不了时的兜底:停机手工替换文件。**
1. 在 Console 里**停止服务**,避免恢复过程中继续写入;
2. 把当前的 `<数据目录>\event_projs\<项目名>\event.db` 改名留档,确认恢复失败还能退回;
3. 选一份目标备份复制为 `event.db`(覆盖原文件);
4. 同时删除同目录下的 `event.db-wal` 与 `event.db-shm`;
5. 重新启动服务,核对积分榜与最近一次发布记录。
> `<数据目录>\system.db` 保存账号与会话信息,一般不需要回退;如需一并回退,替换它的方法与 `event.db` 相同(同时删除 `system.db-wal` / `system.db-shm`)。
>
> 现场若还有备机,优先切换备机比原地恢复更快。
### 8.4 整机迁移
用户数据目录(`%LOCALAPPDATA%\ScoreboardConsole\`,里面是数据 + 配置 + 规则)与**安装目录**(`Scoreboard_Console.exe`、`scoreboard.exe` 等)一起拷到另一台同版本 Windows 机器:
- 用户数据目录:整个 `%LOCALAPPDATA%\ScoreboardConsole\` 拷过去即可(含 `data\`、`console\config.json`、`config.yaml`、`rules\`;同一用户名下路径一致,最省事)——这样配置、规则和赛事数据原样接管,无需重新配置;
- 安装目录:按同样路径放好,或直接用新机器上的安装包装一遍(安装目录里那三份只是模板,拷不拷都行);
- 启动 Console 即可;若数据目录放到了别处,记得同步改 `console\config.json` 的 `server.dataDir`(在用户数据目录里那份)。
### 8.5 赛事项目包导出 / 导入
服务端**一次只打开一个赛事项目**,页面上看到的数据全部来自标记为「当前」的那一项,其余项目只是躺在数据目录里的数据。导入赛事包**不会自动切换**当前赛事,导入后要再点一次「切换为当前赛事」才生效(见下面的步骤)。
| 操作 | 位置 | 说明 |
| --- | --- | --- |
| 导出 | 「导出 → 赛事项目包 → 导出 zip」 | 包内含 `meta.yaml`、`event.db`、`uploads/`、`rules.yaml`;整包拷走即可在另一台机器复现 |
| 导入 | 「导出 → 赛事项目包 → 选择 .zip → 导入」 | **仅管理员**可操作;可填「重命名」,勾选「覆盖同名项目」会先二次确认 |
| 切换 | 「导出 → 赛事项目包 → 赛事项目列表 → 切换为当前赛事」 | 只有管理员可操作;会让服务端重启一次 |
包内含四类内容:
- `meta.yaml`:包格式标识、项目名、赛事名称/副标题/主办方、导出时间,以及每个文件的 SHA-256 清单(导入时逐个校验,对不上就拒绝);
- `event.db`:赛事库快照(用 `VACUUM INTO` 生成,导出时服务端可以继续写入),成绩、基础数据、积分规则都在里面;
- `uploads/`:**全部上传资源**——赛事会标、大屏会标、大屏背景图、各参赛单位 logo,以及**上传的字体**(放在 `uploads/fonts/` 下,见使用手册 14.3)。导入时会连文件一起落到新项目目录的 `uploads/`,切换过去后图片直接显示、字体直接可用,不用重新上传;
- `rules.yaml`:当前规则包的一份 YAML 副本,便于单独复用到别的赛事;规则本身存在赛事库里,导入后即刻生效,这份只是副本。
导出界面同一页的「结果导出」标签可以把最终结果导成 xlsx:**总表**(总积分榜 + 单位奖牌榜 + 个人积分奖牌榜,一个工作簿)、**全部**(总表 + 全部分项)、**全部(单页)**(所有成绩平铺在一张表)、**分项成绩表**(按小项)、**单位积分明细**(按单位)各自可单独导出打印,另有可筛选(小项 / 组别 / 单位 / 批次)的**报名名单**;「积分榜」页三个榜单的卡片头还各有明细导出(**导出总明细** / **导出奖牌明细** / **导出个人明细**,见使用手册 8.3)。基础数据(单位 / 大项 / 小项 / 组别)另有单独的基础数据包可导出 / 导入,见 8.7。
导入与切换:
1. 导入把包解到 `<数据目录>\event_projs\<项目名>\`(项目名可用「重命名」覆盖)。**导入只是把数据落盘,不会切换当前赛事**,所以导入完成后页面会提示「已导入为 xxx,但它还不是当前赛事」;导入期间请勿停止服务端进程;
2. 在导入结果框里点「立即切换并重启服务端」,或稍后在**赛事项目列表**里点该项目的「切换为当前赛事」(列表会显示每个项目的项目名、更新时间、占用大小,当前打开的那项带「当前」徽标,缺 `event.db` 的项目不可切换);
3. 切换 = 服务端把项目名写进 `<数据目录>\active_project.txt` 后优雅退出,由 Console 自动拉起并打开新赛事(页面会显示「正在重启服务端…」,恢复后自动刷新,最多等 60 秒)。**所以切换需要 Console 在运行**;如果是不经 Console、直接双击 `scoreboard.exe` 跑的,切换后要自己把服务端重新启动一次;
4. 之后即使不带任何启动参数,服务端也仍然打开这个赛事(记忆文件生效);换了别的赛事再切一次即可。
- **导出前先看一眼赛事名称**:名称里含 `/ \ : * ? " < > |`、超过 64 个字、或以点 / 空格结尾时**导不出来**——导出包的目录名取自赛事名称。现在这种非法名会返回一句明确的提示(不是「服务端异常」),按提示把「赛事信息 → 赛事名称」改掉再导即可;
- **切换后怎么确认真的切过去了**:看服务端日志里有没有 `msg=当前赛事项目 project=<项目名> source="记忆文件 active_project.txt"` 这一行。看到它才算切过去,再去核对榜单、成绩、单位 logo、大屏背景图与字体(图片有长缓存,必要时在大屏窗口按一次强制刷新);
- 不能覆盖**正在运行**的赛事项目:对当前赛事的同名包,导入会被直接拒绝(先切到别的赛事再导);
- 勾选「覆盖同名项目」时,旧目录会先改名保留、新项目落盘成功后才删除它;
- 参赛单位 logo、会标、背景图与上传的字体都在 `uploads\` 里,所以**备份 / 迁移时要连 `event_projs\<项目名>\uploads\` 一起拷**(见 8.6);
- **赛事包上传上限是 512MB**(名单 CSV 与基础数据包那一档是 32MB,这里刻意放宽):赛事包是唯一带二进制资源的包——上传字体单个就允许 20MB(且可以传多个),再加上背景图、会标与各单位 logo,「两个字体 + 一张背景图」的正常包很容易超过 32MB,按 32MB 卡会让正常包导不进来。真的超过 512MB 时导入会被拒绝并提示「解析上传失败」,先到「系统设置 → 赛事信息」删掉用不到的图片 / 字体,再重新导出一份包;
- **不要用 `-project` 启动参数来切赛事**:显式传 `-project` 时,服务端会顺手把它写回「上次打开的赛事」记忆文件,于是以后每次启动都优先打开这个项目——在页面里切完、重启后又被拉回去。只有在「刻意把一台机器固定成只开某一个项目」时才这么用:在 Console 的用户配置 `config.json` 里给 `server.args` 加 `"-project","<项目名>"` 后重启 Console,或直接 `scoreboard.exe -config "%LOCALAPPDATA%\ScoreboardConsole\config.yaml" -project <项目名> -data "%LOCALAPPDATA%\ScoreboardConsole\data"` 启动(见 6.2)。
### 8.6 手工备份 / 迁移整个数据目录
快照备份之外,数据库文件、上传的图片与字体、备份都可以整体拷走:
1. 在 Console 里**停止服务**(托盘 → 停止服务),避免拷贝过程中继续写入;
2. 把整个数据目录 `%LOCALAPPDATA%\ScoreboardConsole\data` 复制到 U 盘或其他盘(里面含 `system.db`、`event_projs\`(数据库 + `uploads\` 图片与字体)、`backups\`);
3. 恢复时把整个目录拷回去,然后:
- 拷回**原路径**:直接启动 Console 即可;
- 拷到**别的路径**:先建好目标目录并放入数据,再在用户配置 `console\config.json` 里把 `server.dataDir` 指向它,重启 Console;
4. 恢复完成后在管理端核对赛事、账号、上传图片与字体是否齐全。
> 只拷 `backups\` 里的 `.db` 快照也能救回成绩数据,但上传的背景图/会标/单位 logo **与自定义字体**在 `event_projs\<项目名>\uploads\` 里(字体在 `uploads\fonts\`),不拷这个目录会丢这些文件,大屏上表现为图片不显示、字体回落成默认字体。`deploy\backup.bat` 会自动把每个项目的 `uploads\` 打成 `*-uploads-*.zip` 放在 `backups\` 里(见 8.2),可以用它替代手工拷贝——但那个 zip **不参与 20 份滚动清理**,解压后请把项目目录里的 `uploads\` 覆盖回去,并定期手工删旧的 zip。
### 8.7 基础数据包导出 / 导入(单位 / 大项 / 小项 / 组别)
赛前批量维护基础数据,或换一台机器时把基础数据整体搬过去,用「导出 → 基础数据」页签:
| 操作 | 位置 | 说明 |
| --- | --- | --- |
| 导出 | 「导出 → 基础数据 → 导出 xlsx」 | 一个工作簿四张工作表:单位 / 大项 / 小项 / 组别(组别表带所属小项编码) |
| 导入 | 「导出 → 基础数据 → 选择 .xlsx → 预览差异 → 确认写入」 | **仅管理员**;只认 `.xlsx`(四张表装不进一个 CSV) |
- **按编码匹配,存在就更新、不存在就新建;绝不删除**:文件里没有的行不会被删,也不会被停用;
- **两步走**:先「预览差异」(新增 / 更新 / 无变化 / 错误 + 逐行变化,这一步不写库),确认后再点「**确认写入**」;**有错的行会跳过,合法的行照常写入**,行级错误带**真实 Excel 行号**(如 `单位 第 3 行:单位编码必填`);
- **列不在表里 = 不动该字段**(**布尔列也一样**:缺整列不等于「取默认值」,否则把「启用」列删掉再导回,会把所有停用的单位/大项静默改成启用);**列在表里但格子空**:文本列与年龄上下限清空,权重按 1、布尔列按「是」,**排序留空保持原值**(**四张表口径一致**,单位 / 大项留空同样不会被改写);
- **枚举列例外:列在表里但格子空 = 不改动**(不是「取默认值」)。小项的类型 / 结果模式 / 结果类型 / 比较 / 状态 / 平均口径,以及组别的性别,留空都沿用原值;只有**新建的行**才用默认值(新建组别时性别留空 = 不限)。这是为了防手滑——误清一格不会把已发布小项打回「待录入」、也不会把「团体」改成「个人」;
- 表头行在第 4 行(系统导出件,上面是标题与副标题)或第 1 行(手工整理的表)都能识别;枚举列要填英文规范值(如 `individual` / `time` / `published`),布尔列填「是 / 否」;
- 被「彻底删除」的单位不在导出件里,导入也**不会**把它翻出来:命中它的编码那一行会**整行跳过**(不更新它的任何字段),并在导入结果里报 `单位 第 N 行:单位 XX 已被彻底删除,本行跳过(导入不会把隐藏的单位翻出来);要放回列表请先恢复该单位`,其余合法的行照常写入。要让这个单位回来,先到「基础数据 → 参赛单位」用「恢复」把它放回列表(见使用手册 10.1);
- **这个包只含基础数据**,不含成绩、报名与规则;整场赛事的搬迁仍用赛事项目包(见 8.5)。赛前导出一份留底,现场改乱了可以导回对照。
---
## 9. 故障排查
### 9.1 端口被占用,服务端起不来
1. 查看占用进程(Windows 命令提示符):
```bat
netstat -ano | findstr :19540
```
2. 根据最后一列的 PID 结束进程:
```bat
taskkill /PID <PID> /F
```
3. 或按第 6.3 节把端口改到其他值(两处同时改)。
> 同一台机器不要启动两个 Console,否则会互相抢占端口和大屏。
### 9.2 大屏不亮 / 只有桌面
按顺序检查:
1. HDMI 线是否插好,大屏电源是否打开;
2. Windows 显示设置是否为**扩展**模式;
3. Console「大屏窗口」卡片是否显示已打开;点「重新排布」;
4. 在服务管理窗口点「打开服务端管理面板」,到「大屏控制 → 显示器」里看该块屏的状态,点它的「创建全屏窗口」重新开出来(副屏全屏窗口被误关时最有效,见 7.5);
5. 确认 `display.selection` 选屏策略是否漏掉了目标屏;
6. 服务端未就绪时大屏最多等 20 秒才打开,稍等即可;
7. 查看 Console 运行日志里的「大屏排布完成」「大屏加载」记录;
8. 单屏窗口化时按 **F11** 可切全屏。
> 「大屏控制」页的显示器卡片只在**从服务管理窗口点「打开服务端管理面板」打开的那个窗口**里可用;用浏览器直接打开管理面板看不到这组按钮(页面会给出说明)。
### 9.3 管理面板打不开
1. 确认 Console 里服务端状态为「运行中」;
2. 本机访问 `http://127.0.0.1:19540/admin`,其他设备访问 `http://<主机IP>:19540/admin`;
3. 确认用户配置 `config.yaml` 与 `console\config.json` 的端口一致(都在 `%LOCALAPPDATA%\ScoreboardConsole\` 下,见 3.5);
4. 检查 Windows 防火墙是否拦截了该端口;
5. 用托盘菜单的「打开服务端管理面板」直接打开。
### 9.4 服务反复重启
- 打开 `logs\console.log`,搜索 `[server!]`,这是服务端错误输出;
- 检查数据目录 `%LOCALAPPDATA%\ScoreboardConsole\data` 是否可写、磁盘是否已满;
- 检查用户配置 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` 的语法与端口是否合法。
### 9.5 页面显示「示例数据」
- 表示前端连不上后端或接口未就绪,此时录入、发布等操作**不会写入真实赛事**;
- 确认服务端状态为「运行中」、实时连接显示「实时在线」;
- 恢复连接后刷新页面即可切换到真实数据。
### 9.6 大屏时间不对
- 系统没有外网 NTP,**以主机时间为准**;
- 大屏顶部时钟精确到秒、每秒刷新,改完 Windows 系统时间与时区后立刻跟随;
- 建议在开幕前校准主机时间。
### 9.7 安卓 TV 备选方案打不开
1. 确认电视与主机在同一局域网,主机 IP 为固定 IP;
2. 电视浏览器地址填 `http://<主机IP>:19540/display`;
3. 确认用户配置 `%LOCALAPPDATA%\ScoreboardConsole\config.yaml` 的 `server.host` 为 `0.0.0.0`;
4. 部分电视浏览器对 CSS 动画、字体支持有限,显示效果以实际设备为准。
### 9.8 忘记管理员密码
见第 5.5 节,使用 `scoreboard.exe passwd -config "%LOCALAPPDATA%\ScoreboardConsole\config.yaml" -data "%LOCALAPPDATA%\ScoreboardConsole\data"` 重置。
### 9.9 升级后数据/配置不见了 / 日志里有迁移警告
覆盖安装时安装程序会先跑旧卸载器删掉安装目录,所以本版本起数据放在 `%LOCALAPPDATA%\ScoreboardConsole\data`、配置与规则放在 `%LOCALAPPDATA%\ScoreboardConsole\` 下的另外三处(见 3.4、3.5)。首次启动后先看 Console「运行日志」或 `logs\console.log`:
| 日志 | 含义 |
| --- | --- |
| `数据目录:<路径>` | 本次实际使用的数据目录,先确认它是不是你期望那个 |
| `用户数据目录:<路径>` | 本次实际使用的用户数据目录(配置与规则都在它下面) |
| `已把 <X> 复制到 <Y>` | 首次运行把安装目录里的配置/规则复制到用户目录,属正常 |
| `已把旧数据从 X 迁移到 Y` | 旧数据已整体搬过去,属正常 |
| `警告:...都有数据...` | 两个目录都有数据,Console 没有动任何一边;请人工把要保留的那份拷到 `数据目录:` 指向的位置 |
| `警告:迁移旧数据失败...` | 跨盘或权限问题;旧数据仍在日志里写的 `X`,手工拷过去再重启即可 |
| `警告:准备 <X> 失败...` | 配置/规则那一项回落到安装目录里那份,改动仍会在覆盖安装时丢;按日志里的路径手工复制到用户目录再重启 |
| `警告:数据目录仍在安装目录内` | `server.dataDir` 配置成了安装目录里的路径,覆盖安装会丢数据,请改到安装目录之外 |
若安装目录里的 `data\` 已被删掉,优先用 `backups\` 里最新的 `.db` 快照按第 8.3 节恢复;配置与规则从老版本升上来时只能由 Console 复制新版模板过去(原因见 3.5 的说明),升级后确认一遍端口等设置即可。
---
## 10. 服务端日志与拦截复盘
服务端会把日志写到**数据目录**下的 `logs\`(默认 `%LOCALAPPDATA%\ScoreboardConsole\data\logs\`),按用途分成**两类**目录,各自按天 + 按大小轮转:
```
<数据目录>\logs\
├── operations\ # 放行类:放行的写操作 + 常规运行日志(启动、迁移…)
└── interceptions\ # 拦截类:我们主动拦截的报错 + 服务端异常
```
> 这与 Console 自己的 `logs\console.log`(在安装目录里,只装 Console 与服务端的控制台输出)不是一回事:要复盘「某次操作为什么被拒绝」,看的是数据目录 `logs\interceptions\`。
### 10.1 两类日志各记什么
| 目录 | 内容 | 采集级别 |
| --- | --- | --- |
| `operations\` | **放行类**:放行的写操作(`操作已放行`,含 action / 实体 / 操作人 / requestId)+ 常规运行日志(启动、数据库迁移…) | 按 `log.level` 过滤(默认 `info`) |
| `interceptions\` | **拦截类**:我们主动拦截的报错(WARN,含中文原因 + 拦截点 `文件:行 函数名` + method/path + 操作人/角色 + requestId)+ 拦截上下文(DEBUG,含 detail 与来源 IP)+ **服务端异常(ERROR)** | **固定按 `debug` 采集,不受 `log.level` 影响** |
拦截类固定采集是刻意的:默认级别是 `info`,跟着过滤就看不到现场最需要的那份详细拦截日志。中间件层的拦截(未登录 401 / 无权限 403 / CSRF 校验失败 / 未改初始密码 / 操作过于频繁 429)同样进这里,一样带中文原因与拦截点。
### 10.2 文件命名与轮转
- 文件名按天:`operations-YYYYMMDD.log`、`interceptions-YYYYMMDD.log`;同一天写满一个文件后接着写 `.1`、`.2`…,例如 `interceptions-20260911.1.log`;
- **保留份数含当前文件**,超出后自动删最旧的(每开一个新文件都会清理一遍);
- 单文件大小上限 `log.maxSizeMB`(默认 20 MB),每类保留份数 `log.keepFiles`(默认 10)。两类合计最多约 `2 × keepFiles × maxSizeMB`,默认约 400 MB,正常跑一整天不会把盘写满;
- 万一日志目录不可写(权限、盘满),服务端只把告警打一次,之后降级为只写控制台,**不影响业务请求**。
### 10.3 配置(`config.yaml` 的 `log:` 段)
| 项 | 默认值 | 说明 |
| --- | --- | --- |
| `log.level` | `info` | 控制台与 `operations\` 的采集级别:`debug` / `info` / `warn` / `error`;**拦截类不受它影响** |
| `log.dir` | 空 | 日志根目录,其下分 `operations\` 与 `interceptions\`;留空 = `<数据目录>\logs` |
| `log.maxSizeMB` | `20` | 单个日志文件大小上限(MB);`<=0` 用内置默认 |
| `log.keepFiles` | `10` | 每类保留份数(含当前文件);`<=0` 用内置默认 |
`dir` / `maxSizeMB` / `keepFiles` 三个键都有默认值,不配也能跑;仓库根的 `config.yaml` 里已带注释,照抄即可。改完要重启 Console(配置只在启动时读一次,见 3.5)。
### 10.4 现场复盘:某次操作被拒了怎么看
1. 打开 `<数据目录>\logs\interceptions\`,找当天的 `interceptions-YYYYMMDD.log`(有续写文件时按 `.1`、`.2` 往后看);
2. 搜 `请求被拦截`(WARN)那一行,它带:
- `reason`:中文原因(与页面上看到的提示同源);
- `origin`:**拦截点**,形如 `internal/httpapi/repo_events.go:231 normalizeAvgMode`——直接告诉你是哪个函数的哪一行拦下的;
- `code` / `method` / `path`:错误码与请求地址;
- `actor`(`用户名#id`,未登录记 `guest`)/ `role` / `requestId`;
3. 想定位得更准:在浏览器开发者工具的 **Network** 面板点开那条失败请求,复制响应头 `X-Request-Id`,在日志文件里搜这个值;同值的 DEBUG 行「拦截上下文」会再补上 `detail` 与来源 IP;
4. 想对照同一时间有没有成功的操作,去 `operations\` 里搜同一个 `requestId`(放行日志带 action / 实体 / 操作人)。
> 导入类操作的行级报错也留痕:`导入行被拦截`(DEBUG)会记第几行、原因与拦截点。日志不记请求体,证件号这类个人信息不会因为日志多一处泄露面;`detail` 字段与页面响应同源。
> 服务端自身异常(5xx)也在 `interceptions\` 里,是 `请求失败`(ERROR)并同样带 `origin`。
---
## 11. 上线检查清单
- [ ] 服务端开机自启、崩溃自动拉起已验证
- [ ] 「运行日志」里的 `数据目录:` 指向 `%LOCALAPPDATA%\ScoreboardConsole\data`(不是安装目录),且没有迁移警告
- [ ] 「运行日志」里的 `用户数据目录:` 指向 `%LOCALAPPDATA%\ScoreboardConsole`,且没有「准备 … 失败」警告(配置与规则也在用户目录里,见 3.5)
- [ ] 数据目录的 `backups\` 可写,已生成至少一份备份
- [ ] **服务端日志目录可写**:数据目录下已出现 `logs\operations\` 与 `logs\interceptions\` 两个目录(见第 10 节);知道操作被拒时去 `interceptions\` 搜原因与拦截点
- [ ] 已把数据目录整个拷一份到 U 盘(含 `event_projs\<项目名>\uploads\` 里的图片与字体)
- [ ] **当前赛事正确**:「导出 → 赛事项目包 → 赛事项目列表」里带「当前」徽标的是本次要用的那个项目(刚导入的包要切换过去才算生效,见 8.5)
- [ ] 大屏 HDMI 输出定位正确、分辨率正确、字体正常
- [ ] **显示器菜单可用**:在服务管理窗口打开的「大屏控制」页里能看到每块屏并「创建全屏窗口」,副屏被误关时能重开(见 7.5)
- [ ] **单位 logo 已上传且在榜上显示**(总积分榜 / 单位奖牌榜 / 分项成绩表 / 个人积分榜,见 7.7)
- [ ] **大屏字体已选好并核对**:「系统设置 → 赛事信息 → 字体」里选了标题字体与正文字体,大屏上显示正常;上传的字体在数据目录 `event_projs\<项目名>\uploads\fonts\` 里,导出赛事项目包会一并带走(见 8.5)
- [ ] **公告可用**:推送一条测试公告,全屏到时后底栏仍能看到正文,点「清除公告」后底栏回到「暂无公告」(见 7.6)
- [ ] **赛事日程侧边栏(本版新增,用不用都过一遍开关)**:到「大屏控制 → 赛事日程」(左列、公告卡下面)加两条日程 → 保存 → 大屏**当前页上**叠出侧边栏(默认右侧 22 %)、读数 `已完成 0 / 共 2`,**大屏不换页、轮播不停**;勾一条「已完成」再保存,着色与读数跟着变;打开积分明细或推一条公告时侧边栏**自动收起**、关掉后**自己回来**;**不用就在开赛前把总开关关掉**(关掉后大屏与没有这个功能时一致)。⚠️ 每次保存都会让全场大屏全量重拉一次,是**低频动作**(见《使用手册》9.2 / 17.6)
- [ ] 管理端各角色账号可登录,权限正确
- [ ] 主机 IP 固定,管理端 / 大屏地址已打印或做成二维码贴在操作台
- [ ] 防熄屏、屏保、休眠、系统通知已关闭
- [ ] 主机时间已校准
- [ ] 磁盘剩余空间充足
- [ ] 安卓 TV / 浏览器备选通道实测可打开页面