Files
Scoreboard/devdocs/05-接口与实时协议.md
T

181 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 } ]
}
]
}
}
```