diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02b0391 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# 依赖 +node_modules/ +.pnpm-store/ + +# 构建产物 +dist/ +build/ +*.tsbuildinfo + +# 运行时数据(数据库、备份、上传的 logo)——绝不入库 +data/ +!data/.gitkeep + +# 日志 +logs/ +*.log +npm-debug.log* +pnpm-debug.log* + +# 环境变量与密钥 +.env +.env.* +!.env.example + +# 编辑器 / 系统 +.vscode/* +!.vscode/extensions.json +.idea/ +.DS_Store +Thumbs.db +*.swp + +# 临时文件 +*.tmp +.tmp/ diff --git a/README.md b/README.md index 5b1f927..5fa6af3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ -# Scoreboard +# Scoreboard · 运动会积分榜系统 -一个简易实用的赛事积分榜系统 \ No newline at end of file +一套局域网内部署的运动会成绩与积分系统:**一个服务端 + 两个界面**。 + +| 界面 | 地址 | 用途 | +| --- | --- | --- | +| 实时积分大屏 | `/display` | 只读、无人值守;成绩发布后 1 秒内更新 | +| 裁判长管理面板 | `/admin` | 登录后录入、审核、发布成绩,控制大屏 | + +后台一改数据,前台大屏实时跟着变;草稿不上屏,发布才生效,误操作可撤销、可追溯。 + +## 当前状态 + +📋 **设计阶段** —— 开发文档已成型,等待 D1–D13 决策确认,尚未开始编码。 + +## 文档 + +开发文档集见 [`devdocs/`](./devdocs/README.md): + +| 编号 | 文档 | +| --- | --- | +| 01 | [需求与范围](./devdocs/01-需求与范围.md) | +| 02 | [积分与排名规则](./devdocs/02-积分与排名规则.md) ★ 核心 | +| 03 | [技术选型与架构](./devdocs/03-技术选型与架构.md) | +| 04 | [数据模型](./devdocs/04-数据模型.md) | +| 05 | [接口与实时协议](./devdocs/05-接口与实时协议.md) | +| 06 | [大屏端设计](./devdocs/06-大屏端设计.md) | +| 07 | [管理面板设计](./devdocs/07-管理面板设计.md) | +| 08 | [安全与可靠性](./devdocs/08-安全与可靠性.md) | +| 09 | [测试策略](./devdocs/09-测试策略.md) | +| 10 | [部署与现场方案](./devdocs/10-部署与现场方案.md) | +| 11 | [开发计划与里程碑](./devdocs/11-开发计划与里程碑.md) | +| 12 | [风险登记册](./devdocs/12-风险登记册.md) | + +## 拟定技术栈 + +Node.js 22 + TypeScript + Fastify + Socket.IO + SQLite + Vue 3 + Vite + +## 许可证 + +MIT diff --git a/devdocs/01-需求与范围.md b/devdocs/01-需求与范围.md new file mode 100644 index 0000000..ada7e09 --- /dev/null +++ b/devdocs/01-需求与范围.md @@ -0,0 +1,103 @@ +# 01 · 需求与范围 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[02-积分与排名规则](./02-积分与排名规则.md) + +--- + +## 1. 项目定义 + +一套**局域网内部署**的运动会成绩与积分系统:一个服务端进程,同时对外提供**实时积分大屏**(只读、无人值守)和**裁判长管理面板**(登录、录入、审核、发布)两个前端界面;后台一改数据,前台大屏在 1 秒内完成更新。 + +### 1.1 目标 + +| 编号 | 目标 | 验收标准 | +| --- | --- | --- | +| G1 | 成绩录入到上屏的延迟可感知地「即时」 | 发布后 ≤1s 大屏刷新完成 | +| G2 | 积分计算零差错 | 积分引擎单元测试覆盖全部规则分支,用例数 ≥60 | +| G3 | 误操作可追溯、可撤销 | 每次写操作留审计日志;成绩/调整可撤销且可回滚 | +| G4 | 现场不宕机、断电不丢数据 | 进程崩溃自动拉起;数据库每 5 分钟自动备份 + 发布前备份 | +| G5 | 大屏无人值守可连续运行 | 断网自恢复、分辨率自适应、7×24 无内存泄漏 | + +### 1.2 非目标(明确不做,避免范围蔓延) + +- ❌ 公网多租户 SaaS、多赛事并行管理(本期是**单赛事**)。 +- ❌ 在线报名系统(报名表由主办方线下收集,系统支持导入)。 +- ❌ 计时/测距设备硬件对接(除非 D6 确认需要,否则只留 CSV/Excel 导入接口)。 +- ❌ 移动 App(手机浏览器访问是附赠能力,不是交付项)。 +- ❌ 复杂的多级审批流(本期最多两级:录入 → 裁判长发布)。 + +### 1.3 名词表 + +| 术语 | 含义 | +| --- | --- | +| 单位 / 代表团 | 参赛主体,积分榜上的行,如「第一分公司」 | +| 大项 | 项目的分类,如「田径」「球类」「趣味项目」 | +| 小项 | 具体比赛项目,如「男子100米」「拔河」,**积分计算的最小单位** | +| 报名项 / entry | 「某单位派某人/某队参加某小项」这一条记录 | +| 成绩 / result | 某个报名项在比赛中取得的名次与成绩值 | +| 积分流水 / ledger | 单位得分的原子记录,总分由流水求和得出,**唯一的积分真相来源** | +| 发布 | 把草稿态成绩正式生效并推送上屏的动作 | +| 纪录 | 历史最好成绩,可被本次赛会打破并获得加分 | +| 版本号 / version | 服务端单调递增的变更序号,实时同步与断线补发的依据 | + +--- + +## 2. 角色与职责 + +| 角色 | 使用界面 | 能做什么 | +| --- | --- | --- | +| 大屏观众 | 大屏端 | 只能看 | +| 录入员 `recorder` | 管理端 | 录入成绩草稿、查看自己录入的内容 | +| 裁判长 `chief` | 管理端 | 录入、修改、审核、发布、撤销、登记破纪录、加减分、控制大屏 | +| 管理员 `admin` | 管理端 | 全部权限 + 用户管理 + 赛事配置 + 备份恢复 | + +权限的落地方式(会话、路由级/服务级双重校验)见 [08-安全与可靠性](./08-安全与可靠性.md)。 + +--- + +## 3. 赛事时间线(系统的使用节奏) + +``` +赛前(T-7 ~ T-1) + 建赛事 → 导入单位 → 建大项/小项 → 配置积分模板 → 导入报名表 + → 录入/导入历届纪录 → 联调彩排 + +赛中(比赛日) + 项目开始 → 裁判录入成绩(草稿)→ 裁判长核对 → 发布 → 大屏实时更新 + → 破纪录登记 → 公告发布 + +赛后(T+1) + 导出成绩册/积分表 → 归档 → 备份留档 +``` + +--- + +## 4. 核心业务流程 + +### 4.1 成绩录入 → 上屏 + +```mermaid +flowchart LR + A[裁判长选择小项] --> B[系统列出该项目全部报名项] + B --> C[录入名次/成绩/状态] + C --> D[系统实时校验并预览积分变化] + D --> E{提交} + E -->|存草稿| F[草稿态·不上屏] + E -->|发布| G[写入积分流水] + G --> H[重算总积分榜] + H --> I[生成变更事件 version++] + I --> J[WebSocket 推送] + J --> K[大屏应用增量·播放动效] + F --> L[稍后审核发布] +``` + +### 4.2 关键约束 + +1. **草稿绝不上屏**:服务端只向大屏推送 `state='published'` 的数据。 +2. **发布是批量事务**:一个项目的整组成绩一次发布,不允许出现「发布了 3 人、还剩 5 人」的中间态。 +3. **发布后修改要走撤销**:作废旧积分流水 → 重新发布,全程留审计。 +4. **积分只追加不修改**:总分永远是 `SUM(未作废流水)`,任何数字都能回答「这分怎么来的」。 + +> 规则的完整定义见 [02-积分与排名规则](./02-积分与排名规则.md);流程对应的接口见 [05-接口与实时协议](./05-接口与实时协议.md)。 diff --git a/devdocs/02-积分与排名规则.md b/devdocs/02-积分与排名规则.md new file mode 100644 index 0000000..bc06104 --- /dev/null +++ b/devdocs/02-积分与排名规则.md @@ -0,0 +1,144 @@ +# 02 · 积分与排名规则 ★ + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[04-数据模型](./04-数据模型.md)、[09-测试策略](./09-测试策略.md) +> **本篇是系统的心脏**:规则一变,数据模型与积分引擎都要跟着改,所以必须先定稿。 + +--- + +## 1. 总分构成 + +``` +单位总分 = Σ 成绩积分 + Σ 破纪录加分 + Σ 人工调整 +``` + +对应积分流水表 `score_ledger` 的三种来源类型:`result` / `record_bonus` / `adjustment`。 + +--- + +## 2. 成绩积分 + +``` +成绩积分 = 名次积分表[名次] × 小项权重(multiplier) +``` + +- 名次积分表由「积分模板」定义,可被大项引用,也可被单个小项覆盖。 +- 未取得名次(未完赛、未录取、弃权、DQ)→ 0 分。 +- 小项权重 `multiplier`:普通项目 1,团体/接力/球类项目 2(见 D2)。 + +### 2.1 默认名次积分表 + +| 名次 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 积分 | 9 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | + +### 2.2 并列名次(三种口径,系统可配置) + +| 模式 | 例:两人并列第 2(积分表 9/7/6/5) | 说明 | +| --- | --- | --- | +| `skip` 顺延 ✅默认 | 两人各 7 分,无第 3 名,下一名是第 4 名得 5 分 | 国内运动会最常见 | +| `average` 平均 | 两人各 (7+6)/2 = 6.5 分,下一名第 4 名得 5 分 | 部分规程采用 | +| `higher` 就高 | 两人各 7 分,下一名第 3 名得 6 分 | 少见 | + +### 2.3 特殊情形 + +| 情形 | 处理 | +| --- | --- | +| 报名不足 8 人 | 按实际名次取分,空缺名次分值作废 | +| 弃权 / DNS / DSQ | 0 分,且不占名次(名次顺延) | +| 团体项目积分归属 | 计入所属单位总分;个人积分榜**不计入**成员个人(待 D3 确认是否要给成员各记一份) | +| 一人多项参赛 | 各项独立计分,个人总分累加 | +| 积分手工覆盖 | 允许,但必须填理由并记入审计 | + +--- + +## 3. 破纪录加分 + +| 项 | 规则 | +| --- | --- | +| 加分档位 | 赛会纪录 +2 / 市级 +5 / 省级 +8 | +| 同时破多级 | **取最高档,不累加**(可配置为累加,见 `record_bonus_mode`) | +| 是否随权重翻倍 | **不翻倍**:破纪录加分与成绩积分相互独立 | +| 确认流程 | 系统自动提示「疑似破纪录」→ 裁判长确认 → 生成加分流水 → 更新纪录库 | +| 撤销 | 支持作废该条破纪录记录,加分流水一并作废 | + +--- + +## 4. 人工调整(扣分 / 补分) + +- 字段:单位、分值(可正可负)、类别(处罚 / 补分 / 更正)、理由(必填)、操作人。 +- 支持作废(不物理删除),作废后总分自动重算。 +- 典型场景:违反纪律扣分、跨单位联队积分拆分、漏录补分。 + +--- + +## 5. 排名规则 + +### 5.1 总积分榜排序键 + +``` +总分 DESC → 金牌数 DESC → 银牌数 DESC → 铜牌数 DESC → 单位编码 ASC +``` + +### 5.2 奖牌判定 + +- 名次 = 1 → 金,= 2 → 银,= 3 → 铜。 +- 并列第 1 → 两人各记 1 金,无银牌(与名次顺延口径一致)。 +- 团体/球类项目是否计入奖牌榜 → 见 D3(建议计入)。 + +### 5.3 并列展示 + +- 名次相同则显示同一名次(如两个「2」),不强行拆开。 +- 同分不同名次时按 tie-break 拆开,并在界面标注 tie-break 依据(便于解释给参赛单位)。 + +--- + +## 6. 积分流水原则(实现红线) + +1. 所有积分变化**只追加、不修改**;修改 = 作废旧流水 + 追加新流水。 +2. 总分永远是 `SUM(未作废流水)`,**禁止**使用「总分字段 += 分数」这类原地累加。 +3. 每条流水必须可溯源到:成绩 / 破纪录 / 人工调整。 +4. 排名缓存(`standings_cache`)永远可由流水重建,缓存只是加速,不是真相。 + +--- + +## 7. 待确认(🔴) + +| 编号 | 问题 | 建议默认 | +| --- | --- | --- | +| D2 | 积分表分值 / 是否双倍 / 破纪录加分档位 | 见第 2、3 节 | +| D3 | 并列奖牌、个人积分榜、团体成员是否各记个人分 | 见第 5 节 | +| — | 是否存在「同一小项每单位限报 2 人」这类报名限制 | 建议由报名导入时校验,不进积分引擎 | + +--- + +## 附录 · 积分计算示例 + +**示例 1:男子100米,8 人参赛,无并列** + +| 名次 | 单位 | 成绩 | 积分 | +| --- | --- | --- | --- | +| 1 | 第一分公司 | 11.58 | 9 | +| 2 | 机关本部 | 11.92 | 7 | +| 3 | 第二分公司 | 12.01 | 6 | +| … | … | … | … | +| 8 | 第六分公司 | 13.40 | 1 | + +**示例 2:两人并列第 2(skip 模式)** + +| 名次 | 单位 | 成绩 | 积分 | +| --- | --- | --- | --- | +| 1 | A | 11.58 | 9 | +| 2 | B | 11.92 | 7 | +| 2 | C | 11.92 | 7 | +| 4 | D | 12.01 | 5 | +| 5 | E | 12.10 | 4 | + +**示例 3:4×100米接力(双倍)+ 破赛会纪录** + +| 项 | 分值 | +| --- | --- | +| 第 1 名成绩分 | 9 × 2 = 18 | +| 破赛会纪录加分 | +2(不翻倍) | +| 该单位合计 | 20 | diff --git a/devdocs/03-技术选型与架构.md b/devdocs/03-技术选型与架构.md new file mode 100644 index 0000000..a86202e --- /dev/null +++ b/devdocs/03-技术选型与架构.md @@ -0,0 +1,169 @@ +# 03 · 技术选型与架构 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[04-数据模型](./04-数据模型.md)、[10-部署与现场方案](./10-部署与现场方案.md) + +--- + +## 1. 技术选型 + +### 1.1 候选方案对比 + +| 维度 | A. Node + TS + Fastify + Socket.IO | B. Go 单二进制 | C. Python + FastAPI | +| --- | --- | --- | --- | +| 实时推送 | Socket.IO 成熟(房间/重连/降级) | 需自己写重连与降级 | WebSocket 原生,重连自己写 | +| 前端生态 | 最强(Vue / Element Plus / 动效库) | 前端仍要 Node 构建 | 同左 | +| 现场部署 | 需装 Node 运行时 | **单文件,最优** | 需装 Python + 依赖 | +| 原生依赖风险 | better-sqlite3 需预编译二进制 | 纯 Go SQLite,无 cgo | 无 | +| 开发效率 | 高 | 中 | 中 | +| 单赛事规模(≤300 人、≤50 小项) | 绰绰有余 | 绰绰有余 | 绰绰有余 | + +### 1.2 ✅ 建议结论 + +**主方案 A:Node.js 22 + TypeScript + Fastify + Socket.IO + SQLite + Vue 3 + Vite + Pinia。** + +理由: + +1. 本项目 90% 的复杂度在**两个前端界面 + 实时同步**,Node 生态在这块最省事。 +2. 数据规模极小(几百行成绩),SQLite 同步读写完全够用,`better-sqlite3` 性能过剩。 +3. 单一语言贯穿前后端,类型可共享(前后端共用 `shared/types`),减少字段对不上的低级错误。 +4. 部署难点(运行时)可用「绿色部署包」化解:Node 运行时 + 构建产物 + 启动脚本打包,现场解压双击即可,见 [10-部署与现场方案](./10-部署与现场方案.md)。 + +**降级条件**:若现场主机无法运行 Node(极端受限环境),切方案 B。**切换成本主要在服务端,前端可完全复用**,因此架构上把「领域逻辑」与「运行时」隔离,便于将来移植。 + +### 1.3 关键依赖(初稿,编码时锁版本) + +**服务端** + +| 依赖 | 用途 | 备注 | +| --- | --- | --- | +| `fastify` | HTTP 框架 | 比 Express 快、TS 友好、插件体系清晰 | +| `@fastify/static` | 托管前端构建产物 | 单端口对外 | +| `@fastify/cookie` + `@fastify/csrf-protection` | 会话与 CSRF | 若用 cookie 会话 | +| `socket.io` | 实时推送 | 房间、自动重连 | +| `better-sqlite3` | SQLite 驱动 | 同步 API,简单可靠;备选 `node:sqlite` | +| `zod` | 入参校验 | 与前端共享 schema | +| `argon2` / `bcrypt` | 密码哈希 | 优先 argon2id | +| `exceljs` | Excel 导出 | 导出成绩册 | +| `pino` | 日志 | Fastify 默认 | +| `vitest` | 单元测试 | 与 Vite 同族 | + +**前端** + +| 依赖 | 用途 | +| --- | --- | +| `vue` 3 + `vite` + `typescript` | 基础 | +| `pinia` | 状态管理 | +| `vue-router` | 管理端路由 | +| `element-plus` | 管理端组件库(表格/表单/对话框) | +| `socket.io-client` | 实时客户端 | +| `dayjs` | 时间处理 | +| `@vueuse/core` | 常用组合式工具 | + +> 大屏端**不用组件库**,手写 CSS(设计令牌待视觉定稿,见 D13),保证视觉可控与渲染性能。 + +--- + +## 2. 系统架构 + +### 2.1 部署拓扑 + +``` + ┌──────────────────────────────┐ + │ 现场主机(唯一服务端) │ + │ │ + 大屏机 A ──HTTP──▶│ Node 进程 │ + (Chrome kiosk) │ ├─ HTTP API (/api) │ + │ ├─ 静态资源 (/display /admin)│ + 大屏机 B ──HTTP──▶│ ├─ Socket.IO (/socket.io) │ + │ └─ SQLite (data/scoreboard.db)│ + 平板/笔记本 ─HTTP─▶│ │ + (管理面板) └──────────────────────────────┘ + │ + └── data/backups/ 自动备份 +``` + +- **单进程、单端口**,不引入 Nginx / Redis / 消息队列——现场少一个组件就少一个故障点。 +- 大屏机与管理端都是浏览器,不需要额外安装客户端。 + +### 2.2 服务端模块划分 + +``` +server/src/ +├── main.ts 启动、装配、优雅退出 +├── config.ts 配置(端口/路径/赛事默认值),支持环境变量覆盖 +├── db/ +│ ├── client.ts 连接、PRAGMA(WAL/foreign_keys/busy_timeout) +│ ├── migrations/ 版本化迁移(001_init.sql …) +│ └── seed.ts 首次运行初始化(默认管理员、默认积分模板) +├── domain/ ★纯逻辑,无 IO,最需要测试 +│ ├── scoring/ 名次→积分、并列处理、权重 +│ ├── standings/ 总分聚合、排序、tie-break、奖牌计数 +│ ├── records/ 纪录比较与破纪录判定 +│ └── ledger/ 积分流水的追加/作废/重算 +├── services/ 事务编排(domain + db) +├── routes/ HTTP 接口层(薄) +├── realtime/ Socket.IO:房间、快照、增量、心跳 +├── middleware/ 认证、权限、审计、错误处理、限流 +└── util/ +``` + +**原则**:`domain/` 不 import 任何 fastify / socket.io / sqlite,输入输出都是普通对象。这样积分规则能被彻底单测,也方便将来移植到 Go。 + +### 2.3 数据流(一次成绩发布) + +``` +HTTP POST /api/results/publish + → 校验权限 + 校验入参(zod) + → 开启事务 + ├─ 写入/更新 results + ├─ 作废旧积分流水,追加新流水 + ├─ 重算受影响单位的总分与名次 + ├─ 写 change_log(version++) + └─ 写 audit_log + → 事务提交 + → realtime.broadcast(version, changes) + ├─ room "display" → 大屏 + └─ room "admin" → 其他管理端(多人协作时保持同步) +``` + +--- + +## 3. 目录结构 + +``` +Scoreboard/ +├── devdocs/ # 本文档集 +├── server/ # Node + TS 服务端 +│ ├── src/{db,domain,services,routes,realtime,middleware,util} +│ ├── tests/ +│ └── package.json +├── web/ # Vue 3 + Vite +│ ├── src/ +│ │ ├── shared/ # 类型、API 客户端、Socket 客户端、设计令牌 +│ │ ├── display/ # 大屏入口 +│ │ └── admin/ # 管理端入口 +│ ├── display.html +│ ├── admin.html +│ └── index.html # 导航页 +├── deploy/ # systemd / 启动脚本 / 打包脚本 +├── data/ # 运行时数据(gitignore) +├── package.json # pnpm workspace 根 +├── pnpm-workspace.yaml +└── README.md +``` + +--- + +## 4. 编码规范 + +| 项 | 约定 | +| --- | --- | +| TypeScript | `strict: true`;前后端共享类型放 `web/src/shared` 或独立 `shared/` 包 | +| 命名 | 数据库表/字段 `snake_case`,TS 变量 `camelCase`,组件 `PascalCase` | +| 提交信息 | Conventional Commits(`feat:` / `fix:` / `docs:` / `test:` / `chore:`) | +| 分支 | `main` 保护,功能分支 `feat/xxx` | +| 代码风格 | Prettier + ESLint,提交前 lint-staged 校验 | +| 分层红线 | `domain/` 禁止引入 IO 依赖(见 2.2) | +| 注释 | 只在解释「为什么」时写;不写复述代码的注释 | diff --git a/devdocs/04-数据模型.md b/devdocs/04-数据模型.md new file mode 100644 index 0000000..091999a --- /dev/null +++ b/devdocs/04-数据模型.md @@ -0,0 +1,248 @@ +# 04 · 数据模型 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[02-积分与排名规则](./02-积分与排名规则.md)、[05-接口与实时协议](./05-接口与实时协议.md) + +--- + +## 1. 实体关系概览 + +``` +settings(赛事配置) + +teams(单位) ──┬── athletes(运动员) ──┐ + │ ├── entries(报名项) ── results(成绩) +sports(大项) ── events(小项) ────────┘ │ + │ ├── record_breaks(破纪录) + └── scoring_profiles(积分模板)┘ + │ +score_ledger(积分流水) ── 汇总 ──▶ standings(排名视图/缓存) +point_adjustments(人工调整) ──────────┘ + +users / sessions / audit_logs / change_log +``` + +--- + +## 2. 表结构 + +> SQLite 是动态类型,字段类型按习惯书写,真正的约束靠 `CHECK`、`UNIQUE`、外键与索引保证。 + +### settings — 赛事配置 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| key | TEXT PK | 如 `event.name`、`display.rotate_seconds` | +| value | TEXT | JSON 或纯文本 | +| updated_at | TEXT | ISO8601 | + +关键配置项:赛事名称、副标题、主办方、Logo 路径、主题色、大屏轮播间隔、排名模式、默认积分模板、是否启用审核流。 + +### teams — 参赛单位 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | INTEGER PK | AUTOINCREMENT | | +| code | TEXT | UNIQUE NOT NULL | 短代码,用于排序与导出 | +| name | TEXT | NOT NULL | 全称 | +| short_name | TEXT | | 大屏显示用 | +| logo_path | TEXT | | 相对 `data/uploads/` | +| color | TEXT | | 主题色,用于图表 | +| sort_order | INTEGER | DEFAULT 0 | 自定义排序 | +| active | INTEGER | DEFAULT 1 | 软删除 | + +### athletes — 运动员 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | INTEGER PK | | | +| name | TEXT | NOT NULL | | +| gender | TEXT | CHECK in ('男','女') | | +| team_id | INTEGER | FK teams | | +| employee_no | TEXT | | 工号/学号,可空 | +| active | INTEGER | DEFAULT 1 | | + +### sports — 大项 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| code / name / short_name | TEXT | | +| scoring_profile_id | INTEGER FK | 该大项默认积分模板 | +| sort_order / active | INTEGER | | + +### events — 小项 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| sport_id | INTEGER FK | | +| code | TEXT UNIQUE | | +| name | TEXT | 如「男子100米」 | +| gender | TEXT | 男/女/混合/不限 | +| event_type | TEXT | individual / team / relay / match(球类) | +| scoring_profile_id | INTEGER FK NULL | 覆盖大项模板 | +| multiplier | REAL DEFAULT 1 | 权重,双倍项目填 2 | +| record_eligible | INTEGER DEFAULT 1 | 是否参与纪录榜 | +| status | TEXT | pending / ongoing / finished / published | +| scheduled_at | TEXT | 计划时间 | +| venue | TEXT | 场地 | +| sort_order / remark | | | + +### scoring_profiles — 积分模板 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| code | TEXT UNIQUE | | +| name | TEXT | 如「田径前8名」 | +| tie_mode | TEXT | skip / average / higher,默认 skip | +| record_bonus_mode | TEXT | max / sum,默认 max | + +### scoring_profile_rules — 模板明细 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| profile_id | INTEGER FK | 联合主键 | +| rank | INTEGER | 名次 | +| points | REAL | 分值 | + +### entries — 报名项 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| event_id | INTEGER FK | | +| team_id | INTEGER FK | | +| athlete_id | INTEGER FK NULL | 个人项目 | +| member_names | TEXT NULL | 团体/接力成员,JSON 数组 | +| bib | TEXT | 号码/道次 | +| seed | TEXT | 种子/预赛成绩 | +| status | TEXT | registered / scratched / dq / dns | +| remark | TEXT | | +| UNIQUE(event_id, team_id, athlete_id) | | 防重复报名 | + +### results — 成绩 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| event_id | INTEGER FK | 冗余,便于查询 | +| entry_id | INTEGER FK UNIQUE | 一条报名项一条成绩 | +| rank | INTEGER NULL | 名次,可空 | +| result_text | TEXT | 展示用,如「1:06.33」「6.12米」 | +| result_value | REAL NULL | 比较用(秒/米/分),用于排序与破纪录判断 | +| unit | TEXT | s / m / pts / 次 | +| is_tie | INTEGER | 是否并列 | +| points_override | REAL NULL | 手工覆盖积分(需理由) | +| status | TEXT | ok / dq / dns / dnf / withdrawn | +| state | TEXT | draft / published ★ | +| recorded_by / recorded_at | | | +| verified_by / verified_at | | | +| note | TEXT | | + +### records — 纪录库 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| event_id | INTEGER FK | | +| level | TEXT | meet / city / province / national | +| result_value | REAL | | +| result_text | TEXT | | +| holder_name / holder_team_id | | | +| achieved_at | TEXT | | +| is_current | INTEGER | 是否当前有效纪录 | +| source | TEXT | history(历届导入)/ current(本届产生) | + +### record_breaks — 破纪录登记 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| event_id / entry_id | | | +| level | TEXT | | +| result_value / result_text | | | +| bonus_points | REAL | | +| state | TEXT | pending / approved / rejected | +| approved_by / approved_at | | | + +### score_ledger — 积分流水(核心) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| team_id | INTEGER FK | 归属单位 | +| athlete_id | INTEGER NULL | 归属个人(个人榜用) | +| event_id / entry_id / result_id | NULL | 溯源 | +| source_type | TEXT | result / record_bonus / adjustment | +| source_id | INTEGER | 对应来源记录 id | +| points | REAL | 可正可负 | +| voided | INTEGER DEFAULT 0 | 作废标记 | +| voided_by / voided_at / void_reason | | | +| created_by / created_at | | | + +### point_adjustments — 人工调整 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | INTEGER PK | | +| team_id | INTEGER FK | | +| delta | REAL | 正负均可 | +| category | TEXT | penalty / bonus / correction | +| reason | TEXT NOT NULL | 必填 | +| event_id | INTEGER NULL | 可选关联 | +| voided / created_by / created_at | | | + +### users / sessions / audit_logs / change_log + +| 表 | 关键字段 | +| --- | --- | +| users | id, username UNIQUE, password_hash, display_name, role, active, last_login_at | +| sessions | id, user_id, token_hash, expires_at, ip, user_agent | +| audit_logs | id, user_id, action, entity_type, entity_id, before_json, after_json, ip, request_id, created_at | +| change_log | id(version), entity_type, entity_id, action, payload_json, actor_id, created_at | + +--- + +## 3. 排名如何算(性能与一致性) + +数据量:单位 ≤50、小项 ≤100、报名项 ≤2000。**每次变更后全量重算**耗时可忽略(毫秒级),因此: + +- 不引入复杂增量算法,直接 `SELECT ... FROM score_ledger WHERE voided=0 GROUP BY team_id` 聚合。 +- 排名结果可物化到 `standings_cache` 供大屏快速读取;缓存失效时以流水为准重算。 +- 一致性策略:**流水是唯一真相,缓存永远可重建**。任何「总分不对」的问题,重算一遍即可自愈。 + +--- + +## 4. 索引与约束 + +**索引** + +| 索引 | 用途 | +| --- | --- | +| `entries(event_id)` | 按项目取报名项 | +| `entries(team_id)` | 按单位取报名项 | +| `results(event_id, state)` | 大屏取已发布成绩 | +| `score_ledger(team_id, voided)` | 总分聚合 | +| `score_ledger(source_type, source_id)` | 按来源作废流水 | +| `change_log(id)` | 增量补发 | + +**约束** + +- `results.entry_id UNIQUE` +- `scoring_profile_rules(profile_id, rank)` 联合主键 +- `PRAGMA foreign_keys=ON` + +**删除策略** + +- 业务数据一律**软删除 / 作废**,不做物理删除。 + +--- + +## 5. 迁移与初始化 + +- 迁移文件 `db/migrations/NNN_描述.sql`,按序号执行,记录在 `schema_migrations` 表。 +- 首次启动:建库 → 跑迁移 → 写入种子数据(默认管理员 + 默认积分模板 + 默认赛事配置)。 +- 默认管理员密码随机生成,打印到控制台,首次登录强制修改。 diff --git a/devdocs/05-接口与实时协议.md b/devdocs/05-接口与实时协议.md new file mode 100644 index 0000000..a6b90dc --- /dev/null +++ b/devdocs/05-接口与实时协议.md @@ -0,0 +1,180 @@ +# 05 · 接口与实时协议 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[04-数据模型](./04-数据模型.md)、[06-大屏端设计](./06-大屏端设计.md) + +--- + +# 第一部分 · 实时同步协议 + +## 1. 通道 + +- **Socket.IO**,路径 `/socket.io`,与 HTTP 同端口。 +- 房间:`display`(所有大屏)、`admin`(所有管理端)、`admin:`(定向通知,如「你的草稿已被发布」)。 +- 传输:优先 WebSocket,失败自动降级 polling(Socket.IO 内建)。 + +## 2. 版本号 + 快照/增量(保证断线不错乱) + +``` +服务端维护单调递增 version(等于 change_log.id 最大值) + +客户端连接 + └─ emit "hello" { role, lastVersion } + ├─ lastVersion 为空 或 太旧(增量超阈值)→ "snapshot" { version, data } + └─ 可增量 → "catchup" { fromVersion, changes:[...], version } +服务端有变更 + └─ emit "change" { version, changes:[...] } +客户端应用后 + └─ emit "ack" { version } // 服务端据此识别慢客户端 +服务端每 10s + └─ emit "heartbeat" { version, serverTime } + 客户端发现 version 落后 → emit "resync" +``` + +## 3. 事件清单 + +| 事件 | 方向 | 载荷 | +| --- | --- | --- | +| `hello` | C→S | `{ role, lastVersion?, displayKey? }` | +| `snapshot` | S→C | `{ version, settings, teams, events, standings, records, latestResults }` | +| `catchup` | S→C | `{ fromVersion, version, changes[] }` | +| `change` | S→C | `{ version, changes[] }` | +| `resync` | C→S | `{}` | +| `ack` | C→S | `{ version }` | +| `heartbeat` | S→C | `{ version, serverTime }` | +| `display:command` | S→C | `{ action: 'goto'\|'lock'\|'unlock'\|'reload'\|'announce', payload }` | + +`changes[]` 元素形如: + +```jsonc +{ "entity": "result", "id": 42, "action": "upsert", "data": { } } +``` + +`action` 取值:`upsert` / `void` / `replace`(`replace` 用于整体替换的列表,如积分榜)。 + +## 4. 断线与降级 + +| 场景 | 行为 | +| --- | --- | +| 大屏断网 | 继续显示 localStorage 最后一次快照,右上角显示「⚠ 连接中断 · 数据可能不是最新」 | +| 大屏重连 | 按 `lastVersion` 增量补发;补不上就整包快照 | +| 管理端断网 | 顶部横幅提示,禁止提交写操作(避免双写) | +| 服务端重启 | 客户端自动重连,版本号从数据库恢复(不重置) | +| 多管理端同时改同一项目 | 服务端事务串行化;后提交者收到冲突提示,前端刷新最新数据 | + +## 5. 幂等与顺序 + +- 客户端按 `version` 去重:重复或乱序的变更直接丢弃。 +- 服务端写操作带 `requestId`,重复提交返回首次结果。 + +--- + +# 第二部分 · REST API + +## 6. 约定 + +- 前缀 `/api`,JSON,UTF-8。 +- 认证:`HttpOnly` Cookie 会话 + CSRF Token(管理端);大屏端免认证只读。 +- 统一响应:`{ ok: true, data }` / `{ ok: false, error: { code, message, detail? } }`。 +- 分页:`?page=&size=`,返回 `{ items, total, page, size }`。 + +## 7. 接口清单(初稿) + +### 认证 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/auth/login` | 登录 | +| POST | `/api/auth/logout` | 登出 | +| GET | `/api/auth/me` | 当前用户 | + +### 基础配置 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET/PUT | `/api/settings` | 赛事配置 | +| GET/POST/PUT/DELETE | `/api/teams[/:id]` | 单位 CRUD | +| GET/POST/PUT/DELETE | `/api/sports[/:id]` | 大项 CRUD | +| GET/POST/PUT/DELETE | `/api/events[/:id]` | 小项 CRUD | +| GET/POST/PUT | `/api/scoring-profiles[/:id]` | 积分模板 | +| GET/POST/PUT | `/api/athletes[/:id]` | 运动员 | +| POST | `/api/entries/import` | 导入报名表 | + +### 成绩(核心) + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/events/:id/entries` | 该项目全部报名项 + 现有成绩 | +| PUT | `/api/results/draft` | 批量保存草稿(一次一个项目的整组) | +| POST | `/api/results/publish` | 发布(触发积分重算与推送) | +| POST | `/api/results/unpublish` | 撤销发布 | +| POST | `/api/results/:id/void` | 作废单条成绩 | +| GET | `/api/results?eventId=&state=` | 查询 | + +### 积分与排名 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/standings` | 总积分榜(含奖牌数) | +| GET | `/api/standings/teams/:id/breakdown` | 某单位积分明细(可解释每一分) | +| GET | `/api/standings/athletes` | 个人积分榜 | +| GET/POST | `/api/adjustments[/:id/void]` | 人工加减分 | + +### 纪录 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/api/records` | 纪录榜 | +| POST | `/api/record-breaks` | 登记破纪录 | +| POST | `/api/record-breaks/:id/approve` | 确认加分 | + +### 大屏控制与导出 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | `/api/display/command` | 切页 / 锁定 / 刷新 / 公告 | +| GET | `/api/export/standings.xlsx` | 导出总积分榜 | +| GET | `/api/export/breakdown.xlsx` | 导出单位积分明细 | +| GET | `/api/export/results.xlsx` | 导出成绩册 | +| GET | `/api/audit-logs` | 审计日志 | +| POST | `/api/backup` | 立即备份 | +| GET | `/api/health` | 健康检查(供运维/守护脚本) | + +## 8. 错误码 + +| code | 含义 | +| --- | --- | +| `AUTH_REQUIRED` / `AUTH_FAILED` / `FORBIDDEN` | 认证与权限 | +| `VALIDATION_FAILED` | 入参不合法(附字段级 detail) | +| `CONFLICT` | 并发修改冲突 | +| `STATE_INVALID` | 状态不允许该操作(如未发布就想撤销发布) | +| `RATE_LIMITED` | 触发限流 | +| `INTERNAL` | 服务端异常 | + +--- + +# 附录 · 实时事件样例 + +```jsonc +// 服务端 → 大屏 +{ + "event": "change", + "data": { + "version": 128, + "changes": [ + { + "entity": "result", + "id": 512, + "action": "upsert", + "data": { "eventId": 7, "entryId": 88, "rank": 1, "resultText": "11.58", "state": "published" } + }, + { + "entity": "standings", + "action": "replace", + "data": [ { "teamId": 1, "totalPoints": 186, "gold": 5, "rank": 1 } ] + } + ] + } +} +``` diff --git a/devdocs/06-大屏端设计.md b/devdocs/06-大屏端设计.md new file mode 100644 index 0000000..768b517 --- /dev/null +++ b/devdocs/06-大屏端设计.md @@ -0,0 +1,105 @@ +# 06 · 大屏端设计 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[05-接口与实时协议](./05-接口与实时协议.md)、[10-部署与现场方案](./10-部署与现场方案.md) +> ⚠️ 视觉方案**尚未定稿**(见 D13),第 1 节的令牌为占位值,待设计稿确定后替换。 + +--- + +## 1. 视觉规范(占位,待定) + +| 令牌 | 占位值 | 说明 | +| --- | --- | --- | +| 设计分辨率 | 3840 × 1920(16:9) | 按比例缩放适配任意窗口;1920×1080 同样支持 | +| 背景 | `#0b1e3a` | 深色底,投影环境下对比度好 | +| 主金色 | `#ffd75e` | 分数、冠军 | +| 强调青色 | `#4fd8ff` | 标题、次要强调 | +| 正文 | `#e8f1ff`,次要 `#9db4d4` | | +| 面板 | 半透明白 5% + 1–2px 蓝边 + 16px 圆角 | | +| 字体 | 思源黑体 / Noto Sans CJK SC | **本地打包,不依赖 CDN** | + +> 以上仅为「深色底 + 高对比数据表格」的通用赛事大屏基线。D13 拍板后,本节整体替换为最终视觉规范(色板、字号阶梯、间距、圆角、阴影)。 + +## 2. 缩放适配(与视觉无关,可先落地) + +以设计分辨率布局、整体 `transform: scale()` 适配任意窗口: + +```js +const s = Math.min(window.innerWidth / 3840, window.innerHeight / 1920); +stage.style.transform = `scale(${s})`; +stage.style.left = (window.innerWidth - 3840 * s) / 2 + "px"; +stage.style.top = (window.innerHeight - 1920 * s) / 2 + "px"; +``` + +好处:一套 CSS 适配所有分辨率,不用为 1080p / 4K 写两份。 + +## 3. 页面与轮播 + +| 页 | 内容 | 默认 | +| --- | --- | --- | +| P1 | 总积分榜(全单位,含名次升降箭头) | 轮播页 | +| P2 | 奖牌榜 | 轮播页 | +| P3 | 纪录榜(历史最好成绩) | 轮播页 | +| P4 | 最新成绩 / 刚刚产生名次的项目 | 轮播页 | +| P5 | 单项成绩榜(当前正在进行的项目) | 轮播页 | +| P6 | 公告 / 图文 | 手动触发 | + +- 轮播:默认 20s/页,可后台配置。 +- 后台可**锁定**当前页(比赛高潮时不让它跳走)。 +- 每页支持「无数据」占位态,避免开赛前空白。 + +> 页面清单需在 D7 确认。 + +## 4. 动效(克制、不炫技) + +| 场景 | 动效 | +| --- | --- | +| 名次上升/下降 | 行高亮 + ↑↓ 箭头,2s 后淡出 | +| 分数变化 | 数字滚动递增/递减 | +| 新成绩上屏 | 该行从左滑入 + 短暂金色描边 | +| 破纪录 | 整屏横幅 + 项目/姓名/成绩,停留 5s | +| 公告 | 底部滚动条 | + +> 动效原则:**不影响可读性**。所有动画在「减少动态效果」系统偏好下自动关闭。 + +## 5. 分辨率与性能 + +- 表格行数多时启用「自动滚动」或分页,不用虚拟滚动(行数少,虚拟滚动反而复杂)。 +- 单页 DOM 节点控制在 2000 以内。 +- 避免大面积 `box-shadow` / `backdrop-filter`(4K 下 GPU 压力大)。 +- 动画优先 CSS transition,避免 JS 定时器累积泄漏。 +- 每 24h 自动 `location.reload()` 兜底。 + +## 6. 无人值守自恢复 + +| 项 | 做法 | +| --- | --- | +| 断线 | 显示「⚠ 连接中断 · 数据可能不是最新」,继续展示缓存快照 | +| 重连 | 自动按 `lastVersion` 补增量,补不上取全量快照 | +| 崩溃 | 浏览器 kiosk 模式 + 系统开机自启;可选看门狗脚本 | +| 兜底 | 每 24h 自刷新;页面无内存增长 | + +## 7. 移动端(若 D9 选 A) + +- 同一套大屏页在手机上自适应(窄屏改为单列卡片)。 +- 只读,不做交互编辑。 + +--- + +## 附录 · 设计令牌(占位) + +```css +:root { + /* 占位值,待 D13 视觉定稿后替换 */ + --bg: #0b1e3a; + --panel: rgba(255, 255, 255, 0.05); + --panel-border: rgba(120, 180, 255, 0.25); + --gold: #ffd75e; + --cyan: #4fd8ff; + --text: #e8f1ff; + --text-dim: #9db4d4; + --design-w: 3840px; + --design-h: 1920px; +} +``` diff --git a/devdocs/07-管理面板设计.md b/devdocs/07-管理面板设计.md new file mode 100644 index 0000000..4efc40c --- /dev/null +++ b/devdocs/07-管理面板设计.md @@ -0,0 +1,101 @@ +# 07 · 管理面板设计 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[02-积分与排名规则](./02-积分与排名规则.md)、[05-接口与实时协议](./05-接口与实时协议.md) +> 使用者:裁判长、录入员、管理员。 + +--- + +## 1. 信息架构 + +``` +登录 +├── 仪表盘 当前赛程、待发布项目、连接状态、快捷入口 +├── 成绩录入 ★ 选择项目 → 录入整组 → 预览积分 → 存草稿/发布 +├── 成绩审核 草稿列表 → 对比 → 发布 / 退回 +├── 破纪录 疑似破纪录列表 → 确认 → 加分 +├── 积分调整 人工加减分(必填理由) +├── 积分榜 实时总分/奖牌/个人榜,含明细下钻 +├── 大屏控制 切页 / 锁定 / 刷新 / 公告 +├── 基础数据 单位 / 大项 / 小项 / 积分模板 / 运动员 / 报名导入 +├── 导出 积分榜 / 明细 / 成绩册 +├── 日志 审计日志(谁在什么时候改了什么) +└── 系统 用户管理 / 备份恢复 / 赛事信息 +``` + +--- + +## 2. 成绩录入页(开发重点)★ + +**布局**:左侧项目列表(按大项分组、显示状态)→ 右侧录入表格。 + +**表格列** + +| 道次/号码 | 单位 | 运动员/队 | 成绩输入 | 名次 | 状态 | 积分(自动) | 备注 | +| --- | --- | --- | --- | --- | --- | --- | --- | + +**关键交互** + +1. **实时预览**:每改一个格子,右侧实时显示「若现在发布,各单位积分将如何变化」(+9、-2 的差值),发布前就能看出错。 +2. **自动排序**:填完成绩后按 `result_value` 自动排名(时间越短越好 / 距离越远越好,按项目类型自动判断)。 +3. **键盘流**:Tab/Enter 在格子间移动,`Ctrl+Enter` 保存草稿,`Alt+Enter` 发布。录 8 个人名次不超过 30 秒。 +4. **Excel 粘贴**:支持从 Excel 复制「单位 + 成绩」两列直接粘贴。 +5. **防呆校验**(提交前拦截): + +| 校验 | 提示 | +| --- | --- | +| 名次重复且未标并列 | 红字警告,要求确认 | +| 成绩值与纪录差距过大(如 100 米 5.00 秒) | 提示疑似录入错误 | +| 必填项缺失(名次或状态) | 阻止发布 | +| 该项目已发布,再次提交 | 提示需先撤销发布 | +| 发布确认 | 弹窗显示「本次将影响 N 个单位,最大分差变化 X 分」 | + +--- + +## 3. 审核与发布 + +- 草稿态数据**绝不上屏**(服务端过滤 `state='published'`)。 +- 发布是**批量事务**:一个项目的整组成绩一次发布,避免「发布了 3 个人、还有 5 个人没发」的中间态。 +- 撤销发布:作废该组积分流水,大屏回退,全程留审计。 +- 审核流开关:D4 选 A 时启用;若选 B(一级录入即上屏),本页退化为「已发布成绩列表 + 快速纠错」。 + +--- + +## 4. 大屏控制 + +| 功能 | 说明 | +| --- | --- | +| 切页 | 立即跳转到指定页面 | +| 锁定 / 解锁 | 停止自动轮播,停在当前页 | +| 强制刷新 | 让所有大屏重新拉取快照(用于排障) | +| 公告 | 推送一条滚动公告,可设时长 | +| 在线看板 | 显示每块大屏的在线状态、最后心跳时间、版本号(哪块屏掉线一眼可见) | + +--- + +## 5. 导出 + +- Excel 模板预先设计好(表头、合并单元格、字体),导出即可直接打印。 +- 导出内容:总积分榜、各单位积分明细、全项目成绩册。 + +--- + +## 6. 仪表盘 + +| 卡片 | 内容 | +| --- | --- | +| 当前赛程 | 今天计划进行的小项、进度(已发布 / 总数) | +| 待办 | 草稿待发布数、疑似破纪录待确认数 | +| 连接状态 | 大屏在线数 / 总数,管理端在线数 | +| 实时积分 Top 3 | 当前领先单位 | +| 快捷入口 | 「继续录入」跳回上次未完成的项目 | + +--- + +## 7. 交互通用规范 + +- **危险操作二次确认**:发布、撤销发布、作废成绩、删除单位。 +- **不可逆操作给出后果描述**:如「撤销发布将回退 5 个单位共 23 分」。 +- **乐观更新 + 失败回滚**:提交时按钮进入 loading,失败时明确报错并保留用户输入,不丢数据。 +- **所有列表可筛选/搜索**,成绩录入页默认聚焦上次编辑的项目。 diff --git a/devdocs/08-安全与可靠性.md b/devdocs/08-安全与可靠性.md new file mode 100644 index 0000000..07713ed --- /dev/null +++ b/devdocs/08-安全与可靠性.md @@ -0,0 +1,89 @@ +# 08 · 安全与可靠性 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[10-部署与现场方案](./10-部署与现场方案.md) + +--- + +# 第一部分 · 安全与权限 + +## 1. 认证 + +| 项 | 方案 | +| --- | --- | +| 密码存储 | argon2id(次选 bcrypt cost ≥ 12) | +| 会话 | HttpOnly + SameSite=Lax Cookie,12h 过期,服务端可强制下线 | +| CSRF | 双提交 Cookie / `@fastify/csrf-protection` | +| 默认账号 | 首次启动生成随机强密码并打印到控制台,强制首次登录修改 | +| 限流 | 登录 5 次/分钟/IP,写操作 30 次/分钟/用户 | + +## 2. 授权 + +| 角色 | 权限 | +| --- | --- | +| `admin` | 全部 + 用户管理 + 赛事配置 + 备份恢复 | +| `chief` | 录入、修改、审核、发布、撤销、登记破纪录、加减分、控制大屏 | +| `recorder` | 仅录入草稿、查看自己录入的内容 | +| `viewer` | 只读 | + +- **路由级 + 服务级双重校验**:不信任前端隐藏按钮,服务端每个写接口独立校验权限。 +- 大屏端只读、免登录;可选 `display_key` 参数防止无关人员误开。 + +## 3. 审计 + +- 所有写操作记录:操作人、时间、IP、`request_id`、`before_json`、`after_json`。 +- 审计日志**只读、不可删**(仅管理员可导出/归档)。 +- 关键动作单独标记:发布、撤销发布、作废成绩、人工加减分、用户管理。 + +## 4. 传输安全 + +- 内网 HTTP 即可。 +- 若 D9 选「外网可访问」:必须 HTTPS + 反向代理 + 访问口令,并单独做一次安全评审。 + +## 5. 其他 + +- 备份文件放 `data/backups/`,不随 git 提交;可选加密。 +- 上传的 logo 做类型与大小校验,存储路径不暴露原始文件名。 +- 生产环境关闭详细错误堆栈,仅返回 `error.code`。 + +--- + +# 第二部分 · 可靠性、备份与容灾 + +## 6. 数据可靠性 + +| 项 | 做法 | +| --- | --- | +| SQLite 模式 | `WAL` + `synchronous=NORMAL`(现场断电风险高则用 `FULL`,性能足够) | +| 自动备份 | 每 5 分钟一次 + **每次发布前**一次 | +| 备份方式 | `VACUUM INTO` 生成一致性快照(比直接复制文件安全) | +| 保留策略 | 滚动保留最近 20 份 | +| 全量导出 | 一键导出全量 JSON;一键恢复 | + +## 7. 进程可靠性 + +| 平台 | 方案 | +| --- | --- | +| Linux | `systemd` 服务,`Restart=always`,日志进 journald | +| Windows | 包装成服务(`nssm`)或计划任务 + 开机自启 | +| 健康检查 | `/api/health`,异常自动重启 | + +## 8. 现场容灾预案 + +| 故障 | 预案 | +| --- | --- | +| 服务端主机死机 | 备机启动同一份部署包,恢复最新备份,改 IP 或切换路由器 DHCP 保留 | +| 数据库损坏 | 用最近备份恢复(丢失 ≤5 分钟数据),必要时手工补录 | +| 大屏机死机 | 备用笔记本打开同一 URL 即可顶替 | +| 网络中断 | 大屏继续显示缓存数据;恢复后自动补增量 | +| 断电 | 建议主机接 UPS;恢复后 systemd 自动拉起 | +| 误发布 | 撤销发布 → 修正 → 重新发布,全程 ≤1 分钟 | + +**彩排要求**:正式比赛前至少做一次「全流程 + 断网 + 断电 + 恢复」演练。 + +## 9. 监控与告警 + +- `/api/health` 返回:进程存活、数据库可读写、版本号、最近一次备份时间。 +- 管理面板「系统」页展示:数据库大小、备份列表、磁盘剩余空间。 +- 磁盘剩余空间低于阈值时在管理端告警(现场最容易踩的坑之一)。 diff --git a/devdocs/09-测试策略.md b/devdocs/09-测试策略.md new file mode 100644 index 0000000..ae7cd68 --- /dev/null +++ b/devdocs/09-测试策略.md @@ -0,0 +1,100 @@ +# 09 · 测试策略 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[02-积分与排名规则](./02-积分与排名规则.md) + +--- + +## 1. 测试分层 + +| 层次 | 范围 | 工具 | 要求 | +| --- | --- | --- | --- | +| 单元测试 ★ | 积分引擎、排名、tie-break、并列、双倍、破纪录、撤销重算 | Vitest | 覆盖所有规则分支,≥60 用例,**必过** | +| 集成测试 | API + 权限 + 事务 + 审计 | Vitest + Fastify inject | 关键路径全覆盖 | +| 实时测试 | 快照/增量/重连/乱序/重复/心跳 | Vitest + socket.io-client | 模拟断线重连 | +| E2E | 录入 → 发布 → 大屏更新 | Playwright | 至少 3 条主流程 | +| 压测 | 50 大屏 + 5 管理端并发 | autocannon / 自写脚本 | 延迟 <100ms,无内存增长 | +| 彩排 | 真实数据全流程 | 人工 | 赛前必做 | + +--- + +## 2. 积分引擎测试用例矩阵 + +> 引擎是纯函数,输入「成绩集合 + 积分模板」,输出「流水 + 排名」。以下用例必须在 M2 阶段全部通过。 + +| # | 用例 | 期望 | +| --- | --- | --- | +| 1 | 8 人参赛正常排名 | 9/7/6/5/4/3/2/1 | +| 2 | 两人并列第 2(skip) | 7/7/-/5/4/3/2/1 | +| 3 | 两人并列第 2(average) | 6.5/6.5/-/5/4/3/2/1 | +| 4 | 三人并列第 3(skip) | 6/6/6/-/-/3/2/1 | +| 5 | 团体项目双倍 | 18/14/12/… | +| 6 | 破纪录 + 双倍 | 成绩分双倍,破纪录加分**不**双倍 | +| 7 | 并列第一的奖牌计数 | 2 金 0 银 1 铜 | +| 8 | 发布后撤销 | 流水作废,总分回退 | +| 9 | 修改已发布成绩 | 旧流水作废 + 新流水,审计可查 | +| 10 | 弃权者不占名次 | 名次顺延 | +| 11 | 报名不足 8 人 | 按实际名次取分 | +| 12 | 人工扣分后排名变化 | tie-break 正确重排 | +| 13 | 同分 tie-break | 金牌 → 银牌 → 铜牌 → 编码 | +| 14 | 破纪录多级别 | 取最高档(max 模式) | +| 15 | 破纪录多级别(sum 模式) | 累加 | +| 16 | 一条成绩归属两个单位(联队) | 按配置拆分或整体归属 | +| 17 | 重复发布同一组 | 幂等,不产生重复流水 | +| 18 | 乱序应用增量 | 最终状态一致 | + +--- + +## 3. 集成测试重点 + +| 场景 | 断言 | +| --- | --- | +| 未登录调写接口 | 401 / `AUTH_REQUIRED` | +| `recorder` 尝试发布 | 403 / `FORBIDDEN` | +| 发布触发推送 | 大屏客户端收到 `change` 且 version 递增 | +| 撤销发布 | 流水作废、大屏回退、审计有记录 | +| 并发修改同一项目 | 后者收到 `CONFLICT` | +| 重复提交同一 `requestId` | 返回首次结果,不重复写 | + +--- + +## 4. 实时同步测试 + +- 连接 → `hello` → 收到 `snapshot`。 +- 断开 10 秒期间产生 3 次变更 → 重连收到 `catchup` 且最终状态一致。 +- 重复投递同一 `change` → 客户端忽略。 +- 服务端重启 → 客户端重连后 version 不回退。 +- 心跳超时 → 客户端显示离线角标。 + +--- + +## 5. E2E 主流程 + +1. 登录 → 选择项目 → 录入成绩 → 存草稿 → 发布 → 大屏出现新成绩与积分变化。 +2. 撤销发布 → 大屏回退 → 重新录入并发布。 +3. 登记破纪录 → 确认 → 大屏播放破纪录动效 → 积分榜加分正确。 + +--- + +## 6. 压测指标 + +| 指标 | 目标 | +| --- | --- | +| 大屏客户端数 | 50 并发长连接 | +| 管理端 | 5 并发操作 | +| 发布接口 P95 | < 100ms | +| 推送延迟 P95 | < 500ms | +| 连续运行 24h | 内存增长 < 10% | + +--- + +## 7. 彩排清单(赛前必做) + +- [ ] 导入真实单位 / 项目 / 报名数据 +- [ ] 全流程录入 + 发布 + 大屏验证 +- [ ] 断网 1 分钟后恢复,验证增量补发 +- [ ] 拔电重启,验证自动拉起与数据完整 +- [ ] 从备份恢复演练 +- [ ] 大屏在真实分辨率与投影下检查可读性 +- [ ] 裁判长实操培训(30 分钟) diff --git a/devdocs/10-部署与现场方案.md b/devdocs/10-部署与现场方案.md new file mode 100644 index 0000000..6448e93 --- /dev/null +++ b/devdocs/10-部署与现场方案.md @@ -0,0 +1,86 @@ +# 10 · 部署与现场方案 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[03-技术选型与架构](./03-技术选型与架构.md)、[08-安全与可靠性](./08-安全与可靠性.md) +> ⚠️ 本篇依赖 D8(现场环境)与 D10(技术栈)的确认结果。 + +--- + +## 1. 运行形态 + +- 单进程 Node 服务,监听 `0.0.0.0:8080`。 +- 静态资源由服务端直接托管: + +| 地址 | 用途 | +| --- | --- | +| `http://<现场IP>:8080/` | 导航页(两个入口) | +| `http://<现场IP>:8080/display` | 大屏 | +| `http://<现场IP>:8080/admin` | 管理面板 | + +- 数据目录 `data/`(数据库、备份、上传的 logo)与代码分离,升级不丢数据。 + +## 2. 配置 + +- `.env` 或 `config.json`:端口、数据目录、会话密钥、赛事默认值、大屏 key。 +- 首次启动自动:建库 → 迁移 → 建默认管理员 → 打印初始密码。 + +## 3. 现场部署包(绿色包) + +``` +scoreboard-v1.0.0-win-x64.zip +├── runtime/node.exe (Linux 下为 runtime/bin/node) +├── app/ (服务端构建产物 + 前端 dist) +├── data/ (空目录,首次运行自动初始化) +├── start.bat / start.sh +└── 使用说明.txt +``` + +现场操作:**解压 → 双击 `start.bat` → 浏览器打开提示的地址**。 + +> 不依赖现场装 Node、不依赖外网、不依赖包管理器。这是把「Node 需要运行时」这个缺点消掉的方案,也是选型 A 能站住的关键。 + +## 4. 大屏机配置(kiosk 模式) + +```bash +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" --kiosk --incognito \ + --disable-pinch --noerrdialogs --disable-session-crashed-bubble \ + --autoplay-policy=no-user-gesture-required \ + http://192.168.x.x:8080/display + +# Linux +chromium --kiosk --incognito --disable-pinch --noerrdialogs http://192.168.x.x:8080/display +``` + +现场额外设置: + +- 关闭屏保 / 休眠 / 自动更新。 +- 设置开机自启。 +- 关闭系统通知弹窗(否则会盖在大屏上)。 + +## 5. 网络 + +- 建议独立路由器 / 交换机,不与办公网混用。 +- 服务端主机设固定 IP(或 DHCP 保留)。 +- 有线优先;无线投屏只作为备份手段。 +- 若 D9 选公网访问:加反向代理 + HTTPS + 访问口令,并做安全评审(见 [08-安全与可靠性](./08-安全与可靠性.md))。 + +## 6. 备用与切换 + +| 项 | 方案 | +| --- | --- | +| 备机 | 同架构备用机 + 同一份部署包 + 最新备份,10 分钟内可接管 | +| 备用大屏 | 一台预装浏览器的笔记本,打开同一 URL 即顶替 | +| 备用网络 | 一个备用路由器 / 热点,SSID 与 IP 段预先规划 | +| 备用电源 | 主机接 UPS | + +## 7. 上线检查清单 + +- [ ] 服务端开机自启 + 崩溃自拉起已验证 +- [ ] 数据库自动备份目录可写、有历史文件 +- [ ] 大屏机 kiosk 自启、分辨率正确、字体已本地化 +- [ ] 管理端各角色账号可登录、权限正确 +- [ ] 现场 IP 固定,二维码 / 地址已打印贴在操作台 +- [ ] 断网 / 断电演练通过 +- [ ] 磁盘剩余空间充足(建议 ≥10GB) diff --git a/devdocs/11-开发计划与里程碑.md b/devdocs/11-开发计划与里程碑.md new file mode 100644 index 0000000..cb7bd6f --- /dev/null +++ b/devdocs/11-开发计划与里程碑.md @@ -0,0 +1,51 @@ +# 11 · 开发计划与里程碑 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) · 相关:[09-测试策略](./09-测试策略.md) + +--- + +## 1. 里程碑 + +| 里程碑 | 内容 | 交付物 | 预估 | +| --- | --- | --- | --- | +| **M0** | 需求确认、文档定稿、仓库骨架 | 文档集 v1.0 + 目录结构 + CI | 1 天 | +| **M1** | 数据库 schema + 迁移 + 种子数据 | 可初始化的空库 + 默认模板 | 1–2 天 | +| **M2** | **积分引擎 + 单元测试** | `domain/scoring` 全绿 | 2–3 天 | +| **M3** | 服务端 API + 认证 + 审计 | 接口可调通(含接口文档) | 3–4 天 | +| **M4** | 管理面板:基础数据 + 成绩录入 | 能完整录入并发布 | 4–5 天 | +| **M5** | 大屏端:页面 + 动效 + 缩放适配 | 可投屏展示 | 3–4 天 | +| **M6** | 实时同步 + 重连 + 压测 | 多屏同步验证通过 | 2–3 天 | +| **M7** | 导出 / 备份 / 部署包 | 绿色部署包 | 2 天 | +| **M8** | 彩排 + 缺陷修复 + 正式运行支持 | 现场可用 | 2 天 | + +> 预估为纯开发时间,不含等待确认与现场联调。 + +## 2. 关键路径与顺序 + +``` +M0 → M1 → M2 → M3 → M4 → M6 → M7 → M8 + M5 可与 M4 并行 +``` + +**M2 必须先于 M3/M4**:积分规则是整个系统的心脏,规则错了界面做得再漂亮也白搭。先把引擎用测试钉死,后面所有界面都建立在确定的地基上。 + +## 3. 各里程碑的验收标准 + +| 里程碑 | 验收方式 | +| --- | --- | +| M1 | `pnpm db:migrate` 能建出完整库;种子数据含默认管理员与积分模板 | +| M2 | 09 篇的 18 条积分用例全部通过 | +| M3 | 用 REST 客户端跑通「登录 → 建项目 → 录成绩 → 发布 → 查积分榜」 | +| M4 | 裁判长能在 30 秒内录完 8 人成绩并发布 | +| M5 | 真实分辨率投屏,坐 10 米外能看清前三名 | +| M6 | 50 个客户端并发下发布延迟 <100ms | +| M7 | 在一台干净的机器上解压部署包,双击即能用 | +| M8 | 彩排清单全部打勾 | + +## 4. 风险与缓冲 + +- 业务规则确认(D1–D13)是最大不确定项,**确认越晚,返工越多**。 +- 建议在 M2 之后安排一次「规则复核」,用真实报名数据跑一遍积分表,让裁判长确认无误再往下做界面。 +- 现场联调(真实分辨率、真实网络、真实设备)单独留时间,不要挤进 M8。 diff --git a/devdocs/12-风险登记册.md b/devdocs/12-风险登记册.md new file mode 100644 index 0000000..25cff0b --- /dev/null +++ b/devdocs/12-风险登记册.md @@ -0,0 +1,36 @@ +# 12 · 风险登记册 + +> 所属:运动会积分榜系统 · 开发文档集 +> 版本:v0.1(讨论稿) · 2026-09-09 +> 上游:[README](./README.md) + +--- + +## 1. 风险清单 + +| # | 风险 | 影响 | 概率 | 对策 | +| --- | --- | --- | --- | --- | +| R1 | 积分规则临时变更 | 高 | 中 | 积分模板可配置;规则改动**先改 02 篇文档再改代码** | +| R2 | 现场录入笔误上屏 | 高 | 高 | 两级发布 + 实时预览 + 撤销机制 | +| R3 | 现场网络不稳定 | 高 | 中 | 大屏离线缓存 + 自动重连 + 有线优先 | +| R4 | 服务端主机故障 | 高 | 低 | 备机 + 备份 + 5 分钟恢复预案 | +| R5 | 大屏分辨率 / 浏览器差异 | 中 | 中 | 设计稿缩放 + 多分辨率实测 | +| R6 | 操作人员不熟练 | 中 | 高 | 键盘流 + 防呆 + 赛前 30 分钟培训 | +| R7 | Node 原生模块在现场编译失败 | 中 | 低 | 用预编译二进制 + 绿色包;备选纯 Go 方案 | +| R8 | 数据量 / 并发超出预期 | 低 | 低 | 规模上限明确;SQLite 余量充足 | +| R9 | 需求蔓延(想加直播、投票等) | 中 | 中 | 非目标清单已锁定,新需求走变更流程 | +| R10 | 视觉方案迟迟未定(D13) | 中 | 中 | 先按通用风格实现,视觉定稿后只换样式层,不动逻辑 | + +## 2. 风险等级说明 + +| 等级 | 含义 | +| --- | --- | +| 高 | 一旦发生,会直接影响比赛当天的可用性或数据正确性 | +| 中 | 会影响体验或进度,但有替代方案 | +| 低 | 影响有限,可事后处理 | + +## 3. 需要重点盯的三件事 + +1. **R1 规则变更**——这是唯一能让已完成工作大面积返工的风险。规则一旦确认,改动必须走「先文档、后代码」。 +2. **R2 误录入**——这是发生概率最高的风险。两级发布 + 实时积分预览是核心对策,不能省。 +3. **R4 主机故障**——这是后果最严重的风险。备机 + 备份 + 演练,三样缺一不可。 diff --git a/devdocs/README.md b/devdocs/README.md new file mode 100644 index 0000000..d0f3ec7 --- /dev/null +++ b/devdocs/README.md @@ -0,0 +1,198 @@ +# 运动会积分榜系统 · 开发文档集 + +| 项 | 内容 | +| --- | --- | +| 项目 | 一拖二运动会积分榜:一个服务端 + 实时积分大屏 + 裁判长管理面板 | +| 仓库 | `ssh://git@git.yeij.top:2222/AskaEth/Scoreboard.git` | +| 作者 | AskaEth | +| 文档版本 | v0.1(讨论稿) · 2026-09-09 | +| 状态 | **待确认**:第 D1–D13 项未拍板前,不进入编码 | + +> 本目录取代了早期的单文件《开发规划.md》。内容按模块拆开,改哪块就动哪份,避免一份文档越滚越长。 + +--- + +## 文档地图 + +| 编号 | 文档 | 内容 | 主要读者 | +| --- | --- | --- | --- | +| — | [README](./README.md) | 索引、决策清单、阅读顺序 | 全部 | +| 01 | [需求与范围](./01-需求与范围.md) | 目标/非目标、名词表、角色、赛事时间线、核心流程 | 主办方、产品 | +| 02 | [积分与排名规则](./02-积分与排名规则.md) | 积分表、并列、双倍、破纪录、tie-break | **裁判长**、开发 | +| 03 | [技术选型与架构](./03-技术选型与架构.md) | 选型对比、模块划分、数据流、目录结构、编码规范 | 开发 | +| 04 | [数据模型](./04-数据模型.md) | 表结构、约束、索引、积分流水原则 | 开发 | +| 05 | [接口与实时协议](./05-接口与实时协议.md) | REST API、Socket.IO 事件、断线重连 | 开发 | +| 06 | [大屏端设计](./06-大屏端设计.md) | 页面、轮播、动效、分辨率、无人值守 | 设计、开发 | +| 07 | [管理面板设计](./07-管理面板设计.md) | 信息架构、成绩录入、审核发布、导出 | 裁判长、开发 | +| 08 | [安全与可靠性](./08-安全与可靠性.md) | 权限、审计、备份、容灾预案 | 运维、开发 | +| 09 | [测试策略](./09-测试策略.md) | 测试分层、积分引擎用例矩阵、彩排 | 开发、测试 | +| 10 | [部署与现场方案](./10-部署与现场方案.md) | 绿色部署包、kiosk 模式、现场网络 | 现场运维 | +| 11 | [开发计划与里程碑](./11-开发计划与里程碑.md) | M0–M8、关键路径、交付物 | 全部 | +| 12 | [风险登记册](./12-风险登记册.md) | 风险、影响、概率、对策 | 全部 | + +## 阅读顺序 + +``` +01 需求 → 02 规则 → 03 架构 → 04 数据模型 → 05 接口 → 06/07 界面 → 08 可靠性 → 09 测试 → 10 部署 → 11 计划 → 12 风险 +``` + +**规则先行**:02 篇是整个系统的心脏,规则定不下来,数据模型和积分引擎都不能动工。 + +## 文档维护约定 + +| 改动内容 | 要同步更新的文档 | +| --- | --- | +| 积分规则、排名规则 | 02(必须)→ 04(表结构)→ 09(用例) | +| 新增业务角色 | 01 → 08 | +| 新增接口 / 实时事件 | 05 → 04 | +| 大屏页面增减 | 06 → 05(事件载荷) | +| 现场环境变化 | 10 → 03(选型)→ 08(容灾) | +| 决策清单拍板 | README(勾掉 🔴)→ 对应模块 | + +--- + +## 决策清单(🔴 待主人拍板) + +> 「✅建议」是推荐默认值。主人可以只回复「按建议」,或逐条改。 + +### D1 赛事规模与结构 + +| 项 | ✅建议默认 | +| --- | --- | +| 参赛单位数 | 7 个(初估,以实际报名为准) | +| 大项数 | 3 类:团队项目 / 个人项目 / 球类项目 | +| 小项数 | 26 个(8 团队 + 12 个人 + 6 球类) | +| 运动员数量级 | ≤300 人 | +| 是否多年办赛 | 是,需要保留历届数据(为纪录榜服务) | + +🔴 请确认:实际参赛单位数量与项目结构?是否会有多个「组别」(如职工组/青年组/女子组)? + +### D2 积分规则 + +| 项 | ✅建议默认 | +| --- | --- | +| 名次积分表 | 前 8 名:9 / 7 / 6 / 5 / 4 / 3 / 2 / 1 | +| 并列名次 | **名次顺延**:并列第 2 → 两人各得第 2 名分值,无第 3 名,下一名是第 4 名 | +| 团体/接力项目 | **双倍积分**(`multiplier = 2`) | +| 球类项目 | 双倍积分 | +| 报名不足 8 人 | 按实际名次取分,空缺名次分值作废 | +| 弃权 / DNS / DSQ | 0 分,且不占名次(名次顺延) | +| 破纪录加分 | 赛会纪录 +2、市级 +5、省级 +8(取最高一条,不累加) | +| 扣分 / 处罚 | 支持人工调整,必须填理由,记入审计 | + +🔴 请确认:积分表分值、是否双倍、破纪录加分档位与规则。详见 [02-积分与排名规则](./02-积分与排名规则.md)。 + +### D3 排名规则 + +| 项 | ✅建议默认 | +| --- | --- | +| 主榜 | 总积分榜(总分降序) | +| 同分 tie-break | 金牌数 → 银牌数 → 铜牌数 → 单位编码 | +| 是否要奖牌榜 | 要(金/银/铜计数) | +| 是否要个人积分榜 | 要(个人总分榜,用于大屏展示) | +| 并列奖牌计数 | 两人并列第 1 → 各记 1 金,无银牌(即名次顺延) | + +### D4 审核流程 + +| 方案 | 说明 | +| --- | --- | +| A(✅建议) | 两级:裁判录入草稿 → 裁判长确认发布 → 上屏。发布前可随意改,发布后修改需「撤销发布」 | +| B | 一级:录入即上屏。简单但风险高,录错一个数全场都看见 | +| C | 三级:录入 → 复核 → 发布 | + +🔴 现场只有一位裁判长操作的话,选 A 还是 B?我强烈建议 A,理由:录入必然有笔误,A 的成本只是多一次点击。 + +### D5 用户与权限 + +| 项 | ✅建议默认 | +| --- | --- | +| 账号数 | 3–5 个:1 个管理员 + 1–2 个裁判长 + 若干录入员 | +| 角色 | `admin`(全部)/ `chief`(录入+审核+发布+调整)/ `recorder`(仅录入草稿)/ `viewer`(只读) | +| 大屏端 | 不需要登录(只读),可选 `display_key` 防止外人误开 | +| 会话时长 | 12 小时(覆盖整个比赛日) | + +🔴 确认角色划分是否够用。 + +### D6 成绩来源 + +| 方案 | 说明 | +| --- | --- | +| A(✅建议) | 纯手工录入 + 支持从 Excel/CSV 批量粘贴导入 | +| B | 对接计时设备(需设备型号与协议,本期不承诺) | +| C | 线下 Excel 整理后整表导入 | + +🔴 现场实际怎么产生成绩?如果是手写记录后录入,A 的「快速录入模式」是开发重点。 + +### D7 大屏页面 + +| 页 | 内容 | ✅建议 | +| --- | --- | --- | +| P1 | 总积分榜(全单位,含名次升降箭头) | 轮播页 | +| P2 | 奖牌榜 | 轮播页 | +| P3 | 纪录榜(历史最好成绩) | 轮播页 | +| P4 | 最新成绩 / 刚刚产生名次的项目 | 轮播页 | +| P5 | 单项成绩榜(当前正在进行的项目) | 轮播页 | +| P6 | 公告 / 图文 | 手动触发 | +| — | 是否自动轮播 | 是,每页 20s,可后台锁定某页 | + +🔴 确认页面清单与轮播策略(自动 / 固定 / 手动)。详见 [06-大屏端设计](./06-大屏端设计.md)。 + +### D8 现场部署环境 + +🔴 需要主人提供: + +1. 现场主机**操作系统与架构**(Windows / Linux;x86_64 / arm64)? +2. 主机是笔记本、迷你主机还是服务器?有几台(是否有备机)? +3. 大屏**数量、尺寸、分辨率**(设计分辨率待定,建议 3840×1920 或 1920×1080)? +4. 现场网络:有没有外网?是否独立交换机?大屏机用什么连接(HDMI / 无线投屏)? +5. 录入人员用什么设备(笔记本 / 平板)?平板是什么系统? + +这组信息直接决定部署方案(见 [10-部署与现场方案](./10-部署与现场方案.md))。 + +### D9 是否需要手机看榜 + +| 方案 | 说明 | +| --- | --- | +| A(✅建议) | 局域网内手机可看(大屏页自适应),不开公网 | +| B | 需要外网可访问(要额外考虑域名、HTTPS、安全加固) | +| C | 完全不需要 | + +### D10 技术栈 + +| 方案 | 说明 | +| --- | --- | +| A(✅建议) | Node.js 22 + TypeScript + Fastify + Socket.IO + SQLite + Vue 3 + Vite | +| B | Go 1.26 单二进制(chi + websocket + 纯 Go SQLite + embed 前端) | + +详见 [03-技术选型与架构](./03-技术选型与架构.md)。🔴 如果现场机器装不了 Node 运行时,就选 B。 + +### D11 历史数据 + +🔴 是否需要导入**历届纪录**(往年纪录榜)作为纪录基准?数据现在是什么格式(Excel / 纸质 / 无)? + +### D12 导出与打印 + +| 项 | ✅建议默认 | +| --- | --- | +| 导出总积分榜 Excel | 要 | +| 导出各单位积分明细(哪个项目拿了几分) | 要 | +| 导出成绩册(全部小项成绩) | 要 | +| 现场打印 | 不做打印排版,导出 Excel 后自行打印 | + +### D13 大屏视觉方案 + +| 方案 | 说明 | +| --- | --- | +| A | 由主人/设计方提供新的视觉稿(PSD / Figma / 参考图),我按稿实现 | +| B(✅建议) | 先按「深色底 + 高对比数据表格」的通用赛事大屏风格做一版,用真实数据投屏后再微调 | +| C | 指定某套现成主题 | + +🔴 确认视觉方向,以及是否有现成设计稿。**目前无定稿视觉**,因此 [06-大屏端设计](./06-大屏端设计.md) 中的视觉规范标注为「占位,待定」。 + +--- + +## 下一步 + +1. 主人确认 **D1–D13**(可以只回「按建议」,或逐条改)。 +2. 我按确认结果把文档集从 v0.1 修到 v1.0,并把 02 篇的规则整理成可执行的《积分规则细则》。 +3. 定稿后进入 M1(数据库骨架),见 [11-开发计划与里程碑](./11-开发计划与里程碑.md)。