181 lines
6.0 KiB
Markdown
181 lines
6.0 KiB
Markdown
# 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 } ]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|