# 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 } ] } ] } } ```