docs: 建立 devdocs 开发文档集(13 份模块文档)
This commit is contained in:
1 parent
3e9592b006
commit
3b3496559b
15 files changed
+1685
-2
No files matched your search
@@ -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)。
|
||||
@@ -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 |
|
||||
@@ -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) |
|
||||
| 注释 | 只在解释「为什么」时写;不写复述代码的注释 |
|
||||
@@ -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` 表。
|
||||
- 首次启动:建库 → 跑迁移 → 写入种子数据(默认管理员 + 默认积分模板 + 默认赛事配置)。
|
||||
- 默认管理员密码随机生成,打印到控制台,首次登录强制修改。
|
||||
@@ -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:<userId>`(定向通知,如「你的草稿已被发布」)。
|
||||
- 传输:优先 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 } ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
@@ -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,失败时明确报错并保留用户输入,不丢数据。
|
||||
- **所有列表可筛选/搜索**,成绩录入页默认聚焦上次编辑的项目。
|
||||
@@ -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` 返回:进程存活、数据库可读写、版本号、最近一次备份时间。
|
||||
- 管理面板「系统」页展示:数据库大小、备份列表、磁盘剩余空间。
|
||||
- 磁盘剩余空间低于阈值时在管理端告警(现场最容易踩的坑之一)。
|
||||
@@ -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 分钟)
|
||||
@@ -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)
|
||||
@@ -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。
|
||||
@@ -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 主机故障**——这是后果最严重的风险。备机 + 备份 + 演练,三样缺一不可。
|
||||
@@ -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)。
|
||||
Reference in new issue
Block a user