diff --git a/.gitignore b/.gitignore index 02b0391..17aa624 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,35 @@ -# 依赖 -node_modules/ -.pnpm-store/ - # 构建产物 dist/ -build/ -*.tsbuildinfo +*.exe +*.test -# 运行时数据(数据库、备份、上传的 logo)——绝不入库 +# 运行时数据(数据库、备份、上传资源)——绝不入库 data/ !data/.gitkeep +# 规则包(可能含敏感信息)——只保留本格式说明 +rules/* +!rules/README.md + +# 开发文档(内部设计文档,不进仓库) +devdocs/ + +# 导出的赛事包 / xlsx(含敏感信息) +exports/ + # 日志 logs/ *.log -npm-debug.log* -pnpm-debug.log* # 环境变量与密钥 .env .env.* !.env.example +# 前端 +web/node_modules/ +web/dist/ + # 编辑器 / 系统 .vscode/* !.vscode/extensions.json diff --git a/README.md b/README.md index 5fa6af3..f264248 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,40 @@ -# Scoreboard · 运动会积分榜系统 +# Scoreboard · 赛事积分榜系统 -一套局域网内部署的运动会成绩与积分系统:**一个服务端 + 两个界面**。 +一套局域网内部署的赛事成绩与积分系统:**一个服务端 + 两个界面**,后台一改数据,前台大屏实时跟着变。 | 界面 | 地址 | 用途 | | --- | --- | --- | | 实时积分大屏 | `/display` | 只读、无人值守;成绩发布后 1 秒内更新 | -| 裁判长管理面板 | `/admin` | 登录后录入、审核、发布成绩,控制大屏 | +| 管理面板 | `/admin` | 录入、发布成绩,控制大屏;未登录可只读查看全部赛事数据 | -后台一改数据,前台大屏实时跟着变;草稿不上屏,发布才生效,误操作可撤销、可追溯。 +## 功能概览 -## 当前状态 +- **四个榜**:总积分榜、团体奖牌榜、个人积分奖牌榜、分项成绩表 +- **录入即上屏**:成绩录入 → 发布 → 大屏即时更新 +- **可追溯、可撤销**:草稿不上屏;发布后纠错走「撤销发布 → 改 → 重新发布」,全程留痕 +- **变更历史**:仿 Git 图谱,一眼看清成绩是怎么变成现在这样的 +- **现场公开**:积分明细可下钻,争议时一键推送到大屏 +- **规则可配置**:积分规则以规则包(YAML)描述,可导入导出、跨赛事复用 +- **赛事独立存档**:每个赛事一个独立项目目录,可整包导出/导入 +- **结果导出**:Excel 总表与分项可单独导出 -📋 **设计阶段** —— 开发文档已成型,等待 D1–D13 决策确认,尚未开始编码。 +## 快速开始 + +1. 解压部署包 +2. 双击 `scoreboard.exe`(Linux 下执行 `./scoreboard`) +3. 浏览器打开终端提示的地址 + +``` +大屏 http://<主机IP>:8080/display +管理面板 http://<主机IP>:8080/admin +``` + +> 完全离线运行,不依赖外网,不需要安装任何运行时。 ## 文档 -开发文档集见 [`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 +- 使用说明、操作手册:随正式版发布补充 +- 规则包格式:见 [rules/README.md](./rules/README.md) ## 许可证 diff --git a/devdocs/01-需求与范围.md b/devdocs/01-需求与范围.md deleted file mode 100644 index ada7e09..0000000 --- a/devdocs/01-需求与范围.md +++ /dev/null @@ -1,103 +0,0 @@ -# 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 deleted file mode 100644 index bc06104..0000000 --- a/devdocs/02-积分与排名规则.md +++ /dev/null @@ -1,144 +0,0 @@ -# 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 deleted file mode 100644 index a86202e..0000000 --- a/devdocs/03-技术选型与架构.md +++ /dev/null @@ -1,169 +0,0 @@ -# 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 deleted file mode 100644 index 091999a..0000000 --- a/devdocs/04-数据模型.md +++ /dev/null @@ -1,248 +0,0 @@ -# 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 deleted file mode 100644 index a6b90dc..0000000 --- a/devdocs/05-接口与实时协议.md +++ /dev/null @@ -1,180 +0,0 @@ -# 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 deleted file mode 100644 index 768b517..0000000 --- a/devdocs/06-大屏端设计.md +++ /dev/null @@ -1,105 +0,0 @@ -# 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 deleted file mode 100644 index 4efc40c..0000000 --- a/devdocs/07-管理面板设计.md +++ /dev/null @@ -1,101 +0,0 @@ -# 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 deleted file mode 100644 index 07713ed..0000000 --- a/devdocs/08-安全与可靠性.md +++ /dev/null @@ -1,89 +0,0 @@ -# 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 deleted file mode 100644 index ae7cd68..0000000 --- a/devdocs/09-测试策略.md +++ /dev/null @@ -1,100 +0,0 @@ -# 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 deleted file mode 100644 index 6448e93..0000000 --- a/devdocs/10-部署与现场方案.md +++ /dev/null @@ -1,86 +0,0 @@ -# 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 deleted file mode 100644 index cb7bd6f..0000000 --- a/devdocs/11-开发计划与里程碑.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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 deleted file mode 100644 index 25cff0b..0000000 --- a/devdocs/12-风险登记册.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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 deleted file mode 100644 index d0f3ec7..0000000 --- a/devdocs/README.md +++ /dev/null @@ -1,198 +0,0 @@ -# 运动会积分榜系统 · 开发文档集 - -| 项 | 内容 | -| --- | --- | -| 项目 | 一拖二运动会积分榜:一个服务端 + 实时积分大屏 + 裁判长管理面板 | -| 仓库 | `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)。 diff --git a/rules/README.md b/rules/README.md new file mode 100644 index 0000000..6f309c5 --- /dev/null +++ b/rules/README.md @@ -0,0 +1,29 @@ +# 规则包(rules/) + +本目录存放**可导入导出的积分规则包**,格式 YAML。 + +> ⚠️ 本目录下的规则包**不进版本库**(`.gitignore` 已排除 `rules/*`,仅保留本文件),因为可能包含赛事相关的敏感信息。 + +## 文件命名 + +`<赛事名>.yaml` + +## 顶层字段 + +| 字段 | 说明 | +| --- | --- | +| `format` / `formatVersion` | 固定为 `scoreboard-rules` / `1` | +| `meta` | 规则包名称、code、版本、更新时间 | +| `standings` | 加权系数、tie-break、奖牌统计开关 | +| `profiles` | 积分模板(分值、录取名次、并列口径) | +| `recordBonus` | 破纪录档位与分值 | +| `manualAdjustment` | 人工调整开关 | +| `channelTemplates` | 按项目类型套用的计分通道 | +| `eventOverrides` | 单个项目的例外配置 | + +## 导入导出 + +- 导出:管理端「规则」页 → 导出 YAML +- 导入:先预览差异,确认后写入并全量重算积分 + +详细定义见 [02-积分与排名规则](../devdocs/02-积分与排名规则.md) 第 7 节。