# SportScores **Repository Path**: codekpy/sport-scores ## Basic Information - **Project Name**: SportScores - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-04 - **Last Updated**: 2026-10-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SportScores · 校运会成绩管理 单二进制(Go + 内嵌前端)的校运会成绩管理系统:Excel 导入名册、按 **JSON Schema** 录入不同形式的成绩、 自动名次与团体统计、**时间戳增量主从同步**,并对外提供 **OBS-HUD 轮询接口**。 赛项带**三态**:`pending` 未开始 / `live` 进行中 / `done` 已结束。「赛程」页每个赛项与「成绩录入」页顶部 都有一组三态按钮,可任意来回切换(含回退到未开始),改完立即通过 HUD 接口同步给 OBS-HUD。 「成绩录入」页顶部支持**按赛项名搜索 + 按年级(高一/高二/高三…)+ 按状态筛选**,60 个赛项也能几步定位; 录入弹窗可选**系统键盘或屏幕数字键盘**(触屏现场用),选择记在本机;还有「保存并下一位」用于连续录入。 ``` ┌─────────────────────────── 一个进程 ───────────────────────────┐ │ Vue 管理界面(内嵌) REST API /api/sync/changes(主→从) │ │ ↑ ↓ │ │ PostgreSQL 只读 HUD API ──► OBS-HUD └───────────────────────────────────────────────────────────────┘ ``` ## 快速开始 ```powershell # 1. 构建(先前端,再编译含内嵌资源的后端) powershell -ExecutionPolicy Bypass -File scripts\build.ps1 # 2. 运行(默认 :8090) powershell -ExecutionPolicy Bypass -File scripts\run.ps1 # 或直接跑源码(开发用,前端从磁盘读取、免重编译) powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 ``` 打开 即为管理界面。 需要在第二台机器上开一个**只看不写**的副本(例如现场展示屏、备份机)时,见下面「主从同步」。 ## 管理员账号(写入权限) **未登录也能看**(赛程 / 成绩 / 统计 / HUD 只读接口),但**所有写操作都需要管理员登录**: 未登录时页面顶部显示「只读模式」,录入、改状态、删除、导入等按钮一律隐藏或禁用, 直接调 API 也会被拒绝(`401 unauthorized`)。 初始化方式(二选一): 1. **页面自助**:库里还没有管理员时,打开管理页面会自动弹出「初始化管理员」,设置用户名与密码后即登录。 2. **启动参数 / 环境变量**:启动时同步(或重置)指定账号,适合无人值守部署。 ```powershell # 参数 .\sport-scores.exe -admin-user admin -admin-pass 你的强密码 # 或环境变量 $env:SPORTSCORES_ADMIN_USER = 'admin' $env:SPORTSCORES_ADMIN_PASSWORD = '你的强密码' .\sport-scores.exe ``` | 项 | 说明 | |---|---| | 用户名 | 至少 2 个字符,大小写不敏感唯一 | | 密码 | 至少 6 位,存 bcrypt 哈希(`admin_users.password_hash`) | | 会话 | HttpOnly 签名 Cookie(`sc_session`),12 小时;密钥在 `data/auth-secret` 或 `SPORTSCORES_AUTH_SECRET`(≥16 字符) | | 忘记密码 | 用同一用户名加 `-admin-user/-admin-pass` 再启动一次即可重置;删库里的账号用 `delete from admin_users where username='xxx'` | > 只读副本(`-role replica`)不接受写入,也不显示登录入口;`/api/auth/me` 会返回 `readOnly: true`。 ## 数据库 本机 PostgreSQL,默认库名 `sports_scores`,用户 `postgres`。 | 方式 | 说明 | |---|---| | `%APPDATA%\postgresql\pgpass.conf` | **推荐**:`localhost:5432:*:postgres:<密码>`,psql 与 pgx 自动读取 | | 环境变量 | `SPORTSCORES_DB_PASSWORD` / `PGPASSWORD`(优先级高于 pgpass) | | 完整连接串 | `SPORTSCORES_DSN` 或 `-dsn` 参数 | | 分项覆盖 | `SPORTSCORES_DB_HOST/PORT/USER/NAME` | 首次启动会自动执行 `migrations/*.sql`(已应用版本记录在 `schema_migrations`)。 ## 数据模型(极简:2 张业务表) | 表 | 承载 | 关键点 | |---|---|---| | `events` | 赛程 | `schema` 字段**内联**该赛程的成绩记录形式(JSON Schema) | | `entries` | 名册 + 成绩 | `value` 为 null = 未录入;`(event_id, name, team)` 存活唯一 | | `admin_users` | 管理员账号 | 只有这里的账号能写;密码存 bcrypt 哈希(`0002_admin_users.sql`) | 名次在**读取时**计算(并列 1-2-2-4);积分规则为代码常量(`internal/api/stats.go`)。 所有表带 `updated_at` / `deleted_at`,用于同步(软删为墓碑)。 ## 内置成绩记录形式 | 预设 id | 名称 | 结构 | 自动排名 | |---|---|---|---| | `track_time` | 径赛计时 | `{time: 秒}` | 越小越好,显示 `1:05.42` | | `field_height` | 田赛高度 | `{height: 米}` | 越大越好 | | `field_distance` | 田赛远度 | `{distance: 米}` | 越大越好 | | `count_int` | 计数成绩 | `{count: 次}` | 越大越好,显示整数(跳绳等) | | `rank_only` | 名次制 | `{remark}` | 无(手动录入名次) | 扩展关键字:`x-format`(`time` / `fixed:N` / `int`)、`x-unit`、`x-sort: {field, direction}`。 > 简化设计:预设写在 Go 代码里(`internal/domain/presets.go`),使用时整体写入 `events.schema`,不再单设形式表。 ## 主要接口 | 用途 | 接口 | |---|---| | 登录 / 会话 | `GET /api/auth/me`、`POST /api/auth/login`、`POST /api/auth/logout`、`POST /api/auth/setup`(仅首次初始化) | | 赛程 | `GET/POST /api/events`、`GET/PUT/DELETE /api/events/{id}`、`POST /api/events/{id}/status` | | 成绩记录形式 | `GET /api/schema-presets`(只读预设) | | 名册(含成绩) | `GET/POST /api/events/{id}/entries`、`PUT/DELETE /api/entries/{id}` | | 名次化成绩 | `GET /api/events/{id}/results` | | 统计 | `GET /api/stats/overview`、`GET /api/stats/teams` | | Excel 导入 | `POST /api/import/preview`(上传预览)→ `POST /api/import/apply`(列绑定导入,幂等) | | 主从同步 | `GET /api/sync/changes?since=&cursor=&limit=` | | OBS-HUD | `GET /api/hud/snapshot`、`/api/hud/events`、`/api/hud/events/{id}/results`、`/api/hud/events/{id}/startlist` | `PUT /api/entries/{id}` 的成绩语义:**字段缺省** = 保留原值;**显式 `null`** = 清除成绩;**有值** = 校验后写入。 除认证接口与 `GET`/`HEAD` 外,`/api/**` 的写操作都需要管理员会话 Cookie,否则返回 `401 {"error":{"code":"unauthorized",…}}`;`GET` 接口(含全部 HUD 接口与 `GET /api/sync/changes`)保持公开。 `/api/hud/events` 与 `/api/hud/snapshot` 里的每个赛项都带 **`schemaTitle`**(该赛项所用成绩单据的名字,如 `径赛计时` / `田赛高度` / `田赛远度` / `名次制`), 供 OBS-HUD 在没有名称关键词可匹配时判断赛项类型(从而归入对应的直播场景)。 ## 主从同步(单向 master → slave) 写入一律发生在主机;从机是**只读副本**(所有写请求返回 403),每 5s 增量拉取一次, 游标存在 `<数据目录>/sync-cursor.json`,重启后接着上一次的位置继续拉。 副本保留主机时间戳(`updated_at` 触发器识别显式赋值),因此主从的行内容是一致的。 ### 从机需要什么 | 项 | 说明 | |---|---| | 自己的数据库 | 用 `SPORTSCORES_DB_NAME` 指到另一个库(如 `sports_scores_replica`),**不能与主机共用同一个库** | | 自己的数据目录 | 游标不要和主机混,约定用 `-data data-replica` | | 自己的端口 | 同机并存时用 `-addr :8091` 之类,避免和主机撞端口 | | 能访问主机 | 从机要能 HTTP 访问 `-master` 的地址(先在本机 `curl http://<主机IP>:8090/healthz` 验一下) | 首次启动会自动执行 `migrations/*.sql` 建表;没有游标文件时从「零」开始拉, 所以**空库也能直接追上主机已有的全部数据**(不需要手动导出导入)。 ### 启动 ```powershell # A. 另一台机器(典型部署):库名默认就是 sports_scores .\bin\sport-scores.exe -role replica -master http://192.168.1.5:8090 -data data-replica # B. 同一台机器上和主机并存(演示 / 自测) $env:SPORTSCORES_DB_NAME = 'sports_scores_replica' powershell -ExecutionPolicy Bypass -File scripts\db-init.ps1 # 建从机库(已存在会提示) powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 -role replica -master http://127.0.0.1:8090 -addr :8091 -data data-replica ``` `scripts/dev.ps1` 与 `scripts/run.ps1` 会把参数原样透传给服务端,所以从机也能用脚本启动。 从机的管理界面(`http://<从机>:8091/`)可以正常查看数据,但**不要在上面录入**(只会 403)。 ### 相关参数(`-role replica` 时) | 参数 | 默认 | 说明 | |---|---|---| | `-master` | 空(必填) | 主机地址,如 `http://192.168.1.5:8090` | | `-sync-interval` | `5s` | 增量拉取间隔(现场网络紧张时可放宽到 `10s`) | | `-data` | `data` | 数据目录,从机建议 `data-replica` | | `-addr` | `:8090` | 监听地址,同机并存改成 `:8091` | | `-dsn` / `SPORTSCORES_DB_*` | 同主机 | 从机自己的库连接信息 | ### 怎么确认同步正常 1. 从机日志:启动时打印 `从机同步已启动 master=… interval=5s`,网络/主机异常时打 `增量拉取失败`(每轮重试,不用重启)。 2. `data-replica/sync-cursor.json` 每 5s 被重写一次,里面的 `t` 是**最后一次成功应用的主机变更时间戳**。 3. 直接比对两边数据量:`psql -d sports_scores -tAc "select count(*) from events"` 与 `psql -d sports_scores_replica -tAc "select count(*) from events"`。 ### 从机也是一套完整的只读 API 从机跑的是和主机**同一套路由**,所有 `GET` 都能用,只有写操作被挡(403,`error.code = read_only`): | 用途 | 从机可用 | |---|---| | 角色自检 | `GET /healthz` → `{"ok":true,"readOnly":true,…}`(用它区分主机/副本) | | HUD 轮询 | `/api/hud/snapshot`、`/api/hud/events`、`/api/hud/events/{id}/results`、`/api/hud/events/{id}/startlist` | | 业务读 | `/api/events`、`/api/events/{id}/entries`、`/api/events/{id}/results`、`/api/stats/overview`、`/api/stats/teams`、`/api/schema-presets` | | 管理界面 | `http://<从机>:8091/` 可正常打开(只读,别在上面录入) | | 写入 | ❌ 一律 403(含 Excel 导入、赛项状态切换) | 所以可以把**只读流量全部导到从机、主机只负责写入**。典型做法:让 OBS-HUD 指向副本轮询, 这样直播机的轮询压力不落在主机上,主机和直播机跨网段时也更稳: ```powershell go run ./cmd/server -provider sportscores -provider-url http://<从机IP>:8091 ``` CORS 是 `*`,浏览器页面也可以直接跨域读从机。注意方向是单向的(master → slave): **现场录入只能发生在主机**,从机上的任何写操作都只会被拒。 ## 目录结构 ``` cmd/server/ 启动、路由、主从角色 cmd/genxlsx/ 生成演示名册 Excel(data/sample-roster.xlsx) internal/domain/ 实体、JSON Schema 编译/校验/格式化、名次计算、内置预设 internal/api/ REST、导入、统计、同步入口、HUD internal/sync/ 增量导出(主机)与拉取应用(从机) internal/webui/ 前端静态资源(磁盘 / 内嵌) web/ Vue 3 + Vite + TS + Tailwind v4 管理界面(dist 内嵌进二进制) migrations/ SQL 迁移(自动执行) scripts/ build / dev / run / smoke / db-init 等 ``` ## 测试 ```powershell go test ./... # 单元测试(domain 校验/名次、Excel 解析) powershell -File scripts\smoke.ps1 # 端到端冒烟(25 项,需服务已启动) npm --prefix web run build # 前端类型检查 + 构建 ```