Add backend foundation and config-driven workbench

This commit is contained in:
2026-04-01 15:01:44 +08:00
parent 88b8f05f03
commit 94a1f0ba78
68 changed files with 10833 additions and 0 deletions

54
backend/docs/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Backend Docs
这套文档服务两个目的:
1. 让后面开发时能快速查到当前后端边界
2. 把“配置驱动游戏”的核心约束写清楚,避免业务层和游戏层重新耦合
## 建议阅读顺序
1. [系统架构](D:/dev/cmr-mini/backend/docs/系统架构.md)
2. [核心流程](D:/dev/cmr-mini/backend/docs/核心流程.md)
3. [API 清单](D:/dev/cmr-mini/backend/docs/接口清单.md)
4. [数据模型](D:/dev/cmr-mini/backend/docs/数据模型.md)
5. [配置管理方案](D:/dev/cmr-mini/backend/docs/配置管理方案.md)
6. [前后端联调清单](D:/dev/cmr-mini/backend/docs/前后端联调清单.md)
7. [TodoList](D:/dev/cmr-mini/backend/docs/todolist.md)
8. [开发说明](D:/dev/cmr-mini/backend/docs/开发说明.md)
## 当前系统范围
当前 backend 已覆盖:
- 多租户入口识别
- APP 短信登录
- 微信小程序登录
- 手机号绑定与账号合并
- 首页卡片与入口聚合
- Event 详情与 play 上下文
-`event_release` 为核心的 launch
- session 生命周期
- session 结果沉淀
- 开发 workbench
下一阶段建议重点:
- 可伸缩配置管理
- source/build/release 分层
- 配置构建器
- 发布资产清单
## 当前最重要的设计约束
- 用户是平台级,不是俱乐部级
- 渠道是入口,不是用户体系
- `event` 是业务对象,不是运行配置本体
- `event_release` 才是进入游戏时真正绑定的配置发布对象
- `game_session` 必须固化当时实际使用的 release
## 代码入口
- 程序入口:[main.go](D:/dev/cmr-mini/backend/cmd/api/main.go)
- 应用装配:[app.go](D:/dev/cmr-mini/backend/internal/app/app.go)
- 路由注册:[router.go](D:/dev/cmr-mini/backend/internal/httpapi/router.go)
- migration[migrations](D:/dev/cmr-mini/backend/migrations)

255
backend/docs/todolist.md Normal file
View File

@@ -0,0 +1,255 @@
# Backend TodoList
## 1. 目标
这份 TodoList 只列当前需要 backend 配合联调和近期应推进的事项。
原则:
- 不重复写已经稳定可用的能力
- 优先写会影响前后端联调闭环的点
- 边界不清的事项单独标记“需确认”
## 2. 当前联调现状
当前已经可联调的主链:
- 微信小程序登录
- `GET /events/{eventPublicID}/play`
- `POST /events/{eventPublicID}/launch`
- `POST /sessions/{sessionPublicID}/start`
- `POST /sessions/{sessionPublicID}/finish`
- `GET /sessions/{sessionPublicID}/result`
小程序侧已经具备:
- backend 地址和 token 持久化
- `launch -> GameLaunchEnvelope` 适配
- 进入地图后自动上报 `session start`
- 对局结束后自动上报 `session finish`
所以 backend 现在最重要的不是再扩散接口,而是把当前契约和语义收稳。
## 3. P0 必做
## 3.1 固定 session 状态语义
需要 backend 明确并固定:
- `finished`
- `failed`
- `cancelled`
建议当前口径:
- 正常打终点完成:`finished`
- 超时结束:`failed`
- 主动退出 / 放弃恢复:`cancelled`
说明:
- 小程序现在已经按这个方向接
- 如果 backend 想改这 3 个状态语义,需要先讨论,不要单边改
## 3.2 明确“放弃恢复”的后端处理
这是当前最值得后端配合确认的一点。
当前小程序本地恢复逻辑已经是:
- 进入程序检测到未正常结束对局
- 弹确认框
- 玩家可“继续恢复”或“放弃”
现在本地“放弃”只会清除本地恢复快照。
backend 需要确认的目标语义是:
> 玩家点击“放弃恢复”后,这一局是否应同时在业务后端标记为 `cancelled`。
我建议 backend 采用:
- **是,应标记为 `cancelled`**
原因:
- 否则 `ongoingSession` 会继续存在
- `/events/{id}/play``/me/entry-home` 可能一直把它当成可继续的局
- 会和小程序本地“已放弃”产生语义分叉
建议 backend 配合确认:
1. `POST /sessions/{id}/finish` 使用 `status=cancelled` 是否就是官方放弃语义
2. 如果客户端持有旧 `sessionToken`,恢复放弃时是否允许直接调用 `finish(cancelled)`
3. `cancelled` 后,`event play``entry-home` 中不再返回为 `ongoingSession`
备注:
- 如果 backend 认可这套语义,小程序侧下一步就可以把“点击放弃恢复”改成同步调用 `finish(cancelled)`
## 3.3 保证 start / finish 幂等与重复调用安全
联调和真实环境里,以下情况很常见:
- 网络重试
- 页面重进
- 故障恢复后二次补报
- 用户重复点击
backend 需要确认:
- `start` 重复调用的幂等语义
- `finish` 重复调用的幂等语义
建议:
- `start`:如果已 `running`,返回当前 session视为成功
- `finish`:如果已进入终态,返回当前 session/result视为成功
目的:
- 不把客户端补偿逻辑变成一堆冲突分支
## 3.4 固定 `launch` 返回契约,不随意漂移
当前客户端已经按下面这些字段接入:
- `launch.resolvedRelease.releaseId`
- `launch.resolvedRelease.manifestUrl`
- `launch.resolvedRelease.manifestChecksumSha256`
- `launch.config.configUrl`
- `launch.config.configLabel`
- `launch.config.releaseId`
- `launch.config.routeCode`
- `launch.business.sessionId`
- `launch.business.sessionToken`
- `launch.business.sessionTokenExpiresAt`
backend 现在需要做的是:
- 先保持这些字段名稳定
- 如果要调整命名或层级,先沟通
## 4. P1 应尽快做
## 4.1 增加用户身体资料读取接口
小程序侧已经有:
- telemetry profile 合并入口
- 心率/卡路里计算逻辑
backend 下一步建议提供:
- 当前用户 body profile 查询接口
建议返回至少包含:
- `birthDate``heartRateAge`
- `weightKg`
- `restingHeartRateBpm`
- `maxHeartRateBpm`(可选)
这样后面心率页和消耗估算就能真实接业务数据。
## 4.2 给 `session result` 补一点稳定摘要字段校验
客户端现在会上报:
- `finalDurationSec`
- `finalScore`
- `completedControls`
- `totalControls`
- `distanceMeters`
- `averageSpeedKmh`
backend 建议补两件事:
- 合理性校验
- 空值容忍
不要因为某个可选字段缺失就整局 finish 失败。
## 4.3 dev workbench 增加一组“恢复 / 取消恢复”场景按钮
当前 workbench 已经很好用了。
建议后续再补:
- 标记 session 为 `cancelled`
- 查询 ongoing session
- 快速查看某个用户最新 session 状态
这会很适合配合小程序故障恢复联调。
## 5. P2 下一阶段
## 5.1 配置后台 source / build / release 真正开始做
当前已经有:
- 表结构
- 架构文档
还缺:
- source CRUD
- build 触发
- manifest 产物生成
- release 发布
- asset index 查询
这个建议在当前主链联稳之后再推进。
## 5.2 page / cards / competition 等业务对象继续长出来
这部分不是当前联调阻塞项,但后面会成为业务壳的重要组成。
## 6. 需要先讨论再动的边界
这些事项 backend 不建议自己先拍板:
### 6.1 `failed` 是否专指超时
当前建议是:
- 超时 -> `failed`
- 主动退出 / 放弃恢复 -> `cancelled`
如果 backend 有别的语义方案,需要先统一。
### 6.2 放弃恢复是否一定写后端
我个人建议写后端,并落成 `cancelled`
但如果 backend 团队认为:
- 放弃恢复只影响本地
- 业务上仍允许以后继续从服务端 ongoing session 恢复
那就必须明确告知客户端,不然两边会冲突。
### 6.3 result 页是以后继续本地展示,还是跳业务结果页
当前客户端是本地结果页。
backend 后面如果要接业务结果页,最好提前定:
- finish 成功后是否仍停留地图内结果页
- 还是跳业务壳结果页
## 7. 我建议的最近动作
backend 现在最值得先做的,不是扩接口,而是先确认下面 3 条:
1. `finished / failed / cancelled` 三态语义
2. 放弃恢复是否写 `cancelled`
3. `start / finish` 是否按幂等处理
这 3 条一旦确定,前后端联调会顺很多。
## 8. 一句话结论
当前 backend 最重要的任务不是“再加更多接口”,而是:
> 先把 session 运行态语义和故障恢复放弃语义定稳,再继续扩后台配置系统。

View File

@@ -0,0 +1,369 @@
# 前后端联调清单
## 1. 目的
这份清单只回答三件事:
1. 小程序当前已经具备哪些接后端的前置能力
2. backend 当前已经提供了哪些可联调接口
3. 哪些链路已经能接,哪些链路还缺适配
本文不讨论未来大而全后台方案,只服务当前联调落地。
## 2. 当前结论
当前状态可以概括成一句话:
> backend 业务主链已经可联调;小程序地图运行内核也已经成型;两边之间还缺一层业务接入和会话上报适配。
也就是说:
- 登录、活动详情、launch、session、result 这一条后端链已经可用
- 小程序地图页已经支持携带 `configUrl / releaseId / sessionId / sessionToken`
- 但小程序当前仍主要走本地 demo / 直连 OSS manifest
- 真正的“后端 launch -> 地图页 -> session start/finish/result”还没有正式接上
## 3. 小程序当前已具备的联调基础
## 3.1 启动信封已经成型
地图页不是只吃一个 `configUrl`,而是吃一份启动信封:
- [gameLaunch.ts](D:/dev/cmr-mini/miniprogram/utils/gameLaunch.ts)
当前结构:
- `config.configUrl`
- `config.configLabel`
- `config.configChecksumSha256`
- `config.releaseId`
- `config.routeCode`
- `business.source`
- `business.competitionId`
- `business.eventId`
- `business.launchRequestId`
- `business.participantId`
- `business.sessionId`
- `business.sessionToken`
- `business.sessionTokenExpiresAt`
- `business.realtimeEndpoint`
- `business.realtimeToken`
这意味着:
- backend `launch` 返回的数据结构已经能自然装进小程序地图启动链
- 地图页并不需要重构启动模型,只需要把业务页接到 `GameLaunchEnvelope`
## 3.2 地图页已经支持远端 manifest 启动
- [map.ts](D:/dev/cmr-mini/miniprogram/pages/map/map.ts)
当前地图页会:
1. 解析 `GameLaunchEnvelope`
2.`loadRemoteMapConfig(configUrl)`
3. 编译 runtime profile
4. 启动 `MapEngine`
所以只要后端能给出:
- `manifestUrl`
- `releaseId`
- `configChecksumSha256`
地图页就可以直接跑。
## 3.3 会话态字段已经进入地图页
地图页当前已经能接收并持有:
- `sessionId`
- `sessionToken`
- `sessionTokenExpiresAt`
这说明后面接:
- `POST /sessions/{id}/start`
- `POST /sessions/{id}/finish`
不需要再改地图启动协议。
## 3.4 故障恢复也已经具备会话上下文承载
故障恢复快照当前会保留:
- `launchEnvelope`
- 运行态快照
这意味着一旦接入后端 session 后,恢复链也可以继续沿用同一份 `launchEnvelope`
## 4. backend 当前已具备的联调基础
## 4.1 路由主链已落地
- [router.go](D:/dev/cmr-mini/backend/internal/httpapi/router.go)
当前已实现:
- `POST /auth/login/wechat-mini`
- `GET /me/entry-home`
- `GET /events/{eventPublicID}/play`
- `POST /events/{eventPublicID}/launch`
- `POST /sessions/{sessionPublicID}/start`
- `POST /sessions/{sessionPublicID}/finish`
- `GET /sessions/{sessionPublicID}/result`
- `GET /me/results`
## 4.2 launch 返回结构已贴近客户端
- [核心流程.md](D:/dev/cmr-mini/backend/docs/核心流程.md)
- [接口清单.md](D:/dev/cmr-mini/backend/docs/接口清单.md)
当前 `launch` 返回重点:
- `launch.resolvedRelease.releaseId`
- `launch.resolvedRelease.manifestUrl`
- `launch.resolvedRelease.manifestChecksumSha256`
- `launch.business.sessionId`
- `launch.business.sessionToken`
这和小程序 `GameLaunchEnvelope` 基本是同一语义。
## 4.3 session 运行态和结果态已分离
- [session_service.go](D:/dev/cmr-mini/backend/internal/service/session_service.go)
当前已经区分:
- 业务登录态:`access_token`
- 局内运行态:`sessionToken`
这对地图页是对的,因为地图页真正需要的是:
- 进入前有业务 token
- 进入后局内动作用 sessionToken
## 4.4 开发 workbench 已可用于联调
- [dev_handler.go](D:/dev/cmr-mini/backend/internal/httpapi/handlers/dev_handler.go)
当前 workbench 已能串:
- bootstrap
- auth
- entry/home
- event play / launch
- session start / finish / detail
- result 查询
这对前后端联调非常有价值,说明后端已经不是“只看文档”阶段。
## 5. 当前已经能接的链路
## 5.1 P0登录与业务页前置链
可接:
1. 小程序 `wx.login`
2. `POST /auth/login/wechat-mini`
3. 拿到 `accessToken`
4.`GET /me/entry-home`
5.`GET /events/{eventPublicID}/play`
当前缺口:
- 小程序还没有正式业务页 API 适配层
- 还没有统一 token 持久化与请求封装
## 5.2 P0launch 进入地图
可接:
1. 前置业务页拿到 event play
2.`POST /events/{eventPublicID}/launch`
3. 把返回结果映射成 `GameLaunchEnvelope`
4. `navigateTo('/pages/map/map?...')`
当前缺口:
- 还没有一层 `backend launch -> GameLaunchEnvelope` 的适配函数
- 当前 `gameLaunch.ts` 仍偏 demo/static config 驱动
## 5.3 P0finish 回传结果
可接:
1. 地图页结束一局
2. 提取结果摘要
3.`sessionId + sessionToken``POST /sessions/{id}/finish`
4. 业务页或结果页再查 `GET /sessions/{id}/result`
当前缺口:
- 小程序本地结果页已经有摘要,但还没有正式调用 backend finish
- finish payload 和本地 `resultSummary` 之间还需要一层映射
## 6. 当前还不能说已经接通的链路
## 6.1 配置后台 source/build/release
backend 当前已经有:
- 表结构
- 文档模型
但还没有真正开放:
- `config source`
- `build`
- `release assets`
- `preview launch`
也就是说:
**配置后台链还不能联调,只能联业务主链。**
## 6.2 body profile / 遥测个体化
小程序已经有:
- 身体数据入口
- 遥测 runtime profile
backend 文档里也规划了:
- 用户身体资料
但当前接口清单里还没有明确的 body profile 读接口落到小程序链上,所以这条还不能算当前联调主线。
## 7. 当前最大的接口适配缺口
我认为目前最大缺口只有 4 个:
### 7.1 业务 API 客户端缺失
小程序当前缺:
- 统一 `request` 封装
- token 持久化
- access token 刷新
- backend DTO -> 小程序 view model 适配
### 7.2 launch 适配层缺失
需要一层明确的转换:
`LaunchResponse -> GameLaunchEnvelope`
这里最适合单独做成一个小模块,而不是散落在页面里。
### 7.3 session finish 映射缺失
地图页当前本地已经有:
- 用时
- 分数
- 完成点数
- 里程
- 速度
- 最大心率
但还没有一个稳定函数把它映射为 backend finish payload。
### 7.4 业务结果页与地图结果页还未打通
现在地图页已经有自己的结果页。
后面要决定:
- 地图页结果页先本地展示,再异步回传
- 还是 finish 成功后跳业务结果页
这件事需要前后端统一策略。
## 8. 推荐联调顺序
建议按下面顺序推进,不要跳步:
### 第一步:接微信小程序登录
目标:
- 小程序拿到 `accessToken`
- 能请求鉴权接口
### 第二步:接 event play
目标:
- 小程序业务页能拿到:
- `event`
- `resolvedRelease`
- `play.canLaunch`
- `play.ongoingSession`
### 第三步:接 launch -> map
目标:
- 从后端 launch 返回直接进入地图
- 不再靠 demo preset 手工切配置
### 第四步:接 start / finish / result
目标:
- 开赛后能回传 start
- 结束后能回传 finish
- 结果页能查 backend result
### 第五步:再考虑 ongoing session 恢复
目标:
- backend ongoing session
- 本地故障恢复
两条链统一口径
## 9. 当前已落地的小程序联调适配
小程序侧当前已经补了第一批适配层:
- [backendAuth.ts](D:/dev/cmr-mini/miniprogram/utils/backendAuth.ts)
- [backendApi.ts](D:/dev/cmr-mini/miniprogram/utils/backendApi.ts)
- [backendLaunchAdapter.ts](D:/dev/cmr-mini/miniprogram/utils/backendLaunchAdapter.ts)
- [index.ts](D:/dev/cmr-mini/miniprogram/pages/index/index.ts)
当前已具备:
- 后端 base URL 本地持久化
- access / refresh token 本地持久化
- 微信小程序登录请求封装
- `event play` 请求封装
- `launch -> GameLaunchEnvelope` 适配
- 从首页直接 `launch` 进入地图
- 地图页 `session start / finish` 上报接入
因此当前主链已从“可分析”进入“可实测”。
## 10. 我建议的最近行动项
如果开始联调,我建议先做这 3 件事:
1. 新增小程序 `backendApi` 请求层
先只包 auth / event play / launch / session finish
2. 新增 `launchAdapter`
把 backend launch 响应稳定转成 `GameLaunchEnvelope`
3. 新增 `finishAdapter`
把地图页结果摘要稳定转成 backend finish payload
这三件做完,前后端主链就能真正接起来。
## 11. 一句话结论
当前最真实的进度判断是:
> backend 业务后端主链已经进入可联调阶段;小程序地图运行内核也已经具备承接能力;下一步最值钱的是补小程序业务 API 层和 launch/finish 两个适配器。

View File

@@ -0,0 +1,182 @@
# 开发说明
## 1. 环境变量
参考 [`.env.example`](D:/dev/cmr-mini/backend/.env.example)。
当前最关键的变量:
- `APP_ENV`
- `HTTP_ADDR`
- `DATABASE_URL`
- `JWT_ACCESS_SECRET`
- `AUTH_SMS_PROVIDER`
- `AUTH_DEV_SMS_CODE`
- `WECHAT_MINI_APP_ID`
- `WECHAT_MINI_APP_SECRET`
- `WECHAT_MINI_DEV_PREFIX`
- `LOCAL_EVENT_DIR`
- `ASSET_BASE_URL`
## 2. 本地启动
```powershell
cd D:\dev\cmr-mini\backend
go run .\cmd\api
```
如果你想固定跑开发工作台常用端口 `18090`,直接执行:
```powershell
cd D:\dev\cmr-mini\backend
.\scripts\start-dev.ps1
```
默认会设置:
- `APP_ENV=development`
- `HTTP_ADDR=:18090`
- `DATABASE_URL=postgres://postgres:asdf*123@192.168.100.77:5432/cmr20260401?sslmode=disable`
- `AUTH_SMS_PROVIDER=console`
- `WECHAT_MINI_DEV_PREFIX=dev-`
启动后可直接打开:
- [http://127.0.0.1:18090/dev/workbench](http://127.0.0.1:18090/dev/workbench)
## 3. 当前开发约定
### 3.1 开发阶段先不用 Redis
当前第一版全部依赖:
- PostgreSQL
- JWT
- refresh token 持久化
Redis 后面只在需要性能优化、限流或短期票据缓存时再接。
### 3.2 开发环境短信
当前默认可走 `console` provider。
用途:
- 本地联调无需接真实短信供应商
### 3.3 微信小程序开发态
当前支持 `dev-` 前缀 code。
适合:
- 后端联调
- workbench 快速验证
### 3.4 本地配置目录
当前支持从根目录 [event](D:/dev/cmr-mini/event) 导入本地配置文件。
相关环境变量:
- `LOCAL_EVENT_DIR`
- `ASSET_BASE_URL`
作用:
- `LOCAL_EVENT_DIR` 决定本地 source config 从哪里读
- `ASSET_BASE_URL` 决定 preview build 时如何把相对资源路径归一化成可运行 URL
## 4. Migration
当前 migration 文件在 [migrations](D:/dev/cmr-mini/backend/migrations)。
执行原则:
1. 按编号顺序执行
2. schema 变更只通过新增 migration 完成
3. 不直接改线上已执行 migration
## 5. 开发工作台
### `POST /dev/bootstrap-demo`
它会保证 demo 数据存在:
- `tenant_demo`
- `mini-demo`
- `evt_demo_001`
- `rel_demo_001`
- `card_demo_001`
### `GET /dev/workbench`
这是当前最重要的联调工具。
可以直接测试:
- 登录
- 入口解析
- 首页聚合
- event play
- 配置导入、preview build、publish build
- launch
- session start / finish
- result
- profile
并且支持:
- quick flow
- scenario 保存/导入/导出
- curl 导出
- request history
## 6. 当前推荐联调顺序
### 场景一:小程序快速进入
1. `bootstrap-demo`
2. `login/wechat-mini`
3. `me/entry-home`
4. `events/{id}/play`
5. `events/{id}/launch`
6. `sessions/{id}/start`
7. `sessions/{id}/finish`
8. `sessions/{id}/result`
### 场景二APP 主身份
1. `auth/sms/send`
2. `auth/login/sms`
3. `me/entry-home`
4. `launch`
5. `session`
6. `result`
### 场景三:微信轻账号绑定手机号
1. `login/wechat-mini`
2. `auth/sms/send` with `scene=bind_mobile`
3. `auth/bind/mobile`
4. `me/profile`
### 场景四:配置发布到可启动 release
1. `bootstrap-demo`
2. `dev/events/{eventPublicID}/config-sources/import-local`
3. `dev/config-builds/preview`
4. `dev/config-builds/publish`
5. `events/{id}`
6. `events/{id}/launch`
## 7. 当前后续开发建议
文档整理完之后,后面建议按这个顺序继续:
1. 抽出更通用的 `play context -> launch` 模型
2. 补赛事与报名层
3. 补页面配置和白标首页
4. 再考虑实时网关票据
不要跳回去把玩法规则塞进 backend。

View File

@@ -0,0 +1,425 @@
# API 清单
本文档只记录当前 backend 已实现接口,不写未来规划接口。
## 1. Health
### `GET /healthz`
用途:
- 健康检查
## 2. Auth
### `POST /auth/sms/send`
用途:
- 发登录验证码
- 发绑定手机号验证码
核心参数:
- `countryCode`
- `mobile`
- `clientType`
- `deviceKey`
- `scene`
### `POST /auth/login/sms`
用途:
- APP 手机号验证码登录
返回重点:
- `user`
- `tokens.accessToken`
- `tokens.refreshToken`
### `POST /auth/login/wechat-mini`
用途:
- 微信小程序登录
开发态:
- 支持 `dev-` 前缀 code
### `POST /auth/bind/mobile`
鉴权:
- Bearer token
用途:
- 已登录用户绑定手机号
- 必要时执行账号合并
### `POST /auth/refresh`
用途:
- 刷新 access token
### `POST /auth/logout`
用途:
- 撤销 refresh token
## 3. Entry / Home
### `GET /entry/resolve`
用途:
- 解析当前入口归属哪个 tenant / channel
查询参数:
- `channelCode`
- `channelType`
- `platformAppId`
- `tenantCode`
### `GET /home`
用途:
- 返回入口首页卡片
### `GET /cards`
用途:
- 只返回卡片列表
### `GET /me/entry-home`
鉴权:
- Bearer token
用途:
- 首页聚合接口
返回重点:
- `user`
- `tenant`
- `channel`
- `cards`
- `ongoingSession`
- `recentSession`
## 4. Event
### `GET /events/{eventPublicID}`
用途:
- Event 详情
返回重点:
- `event`
- `release`
- `resolvedRelease`
### `GET /events/{eventPublicID}/play`
鉴权:
- Bearer token
用途:
- 活动详情页 / 开始前准备页聚合
返回重点:
- `event`
- `release`
- `resolvedRelease`
- `play.canLaunch`
- `play.primaryAction`
- `play.launchSource`
- `play.ongoingSession`
- `play.recentSession`
### `POST /events/{eventPublicID}/launch`
鉴权:
- Bearer token
用途:
- 基于当前 event 的可启动 release 创建一局 session
请求体重点:
- `releaseId`
- `clientType`
- `deviceKey`
返回重点:
- `launch.source`
- `launch.resolvedRelease`
- `launch.config`
- `launch.business.sessionId`
- `launch.business.sessionToken`
### `GET /events/{eventPublicID}/config-sources`
鉴权:
- Bearer token
用途:
- 查看某个 event 的 source config 列表
### `GET /config-sources/{sourceID}`
鉴权:
- Bearer token
用途:
- 查看单条 source config 明细
### `GET /config-builds/{buildID}`
鉴权:
- Bearer token
用途:
- 查看单次 build 明细
## 5. Session
### `GET /sessions/{sessionPublicID}`
鉴权:
- Bearer token
用途:
- 查询一局详情
返回重点:
- `session`
- `event`
- `resolvedRelease`
### `POST /sessions/{sessionPublicID}/start`
鉴权:
- `sessionToken`
用途:
- 将 session 从 `launched` 推进到 `running`
### `POST /sessions/{sessionPublicID}/finish`
鉴权:
- `sessionToken`
用途:
- 结束一局
- 同时沉淀结果摘要
请求体重点:
- `sessionToken`
- `status`
- `summary.finalDurationSec`
- `summary.finalScore`
- `summary.completedControls`
- `summary.totalControls`
- `summary.distanceMeters`
- `summary.averageSpeedKmh`
- `summary.maxHeartRateBpm`
### `GET /me/sessions`
鉴权:
- Bearer token
用途:
- 查询用户最近 session
## 6. Result
### `GET /sessions/{sessionPublicID}/result`
鉴权:
- Bearer token
用途:
- 查询单局结果页数据
返回重点:
- `session`
- `result`
`session` 中会带:
- `releaseId`
- `configLabel`
### `GET /me/results`
鉴权:
- Bearer token
用途:
- 查询用户最近结果列表
## 7. Profile
### `GET /me`
鉴权:
- Bearer token
用途:
- 当前用户基础信息
### `GET /me/profile`
鉴权:
- Bearer token
用途:
- “我的页”聚合接口
返回重点:
- `user`
- `bindings`
- `recentSessions`
## 8. Dev
### `POST /dev/bootstrap-demo`
环境:
- 仅 non-production
用途:
- 自动准备 demo tenant / channel / event / release / card
### `GET /dev/workbench`
环境:
- 仅 non-production
用途:
- 后端自带 API 测试面板
当前支持:
- bootstrap
- auth
- entry/home
- event/play/launch
- session start/finish/detail
- result 查询
- profile 查询
- quick flows
- scenarios
- request history
- curl 导出
### `GET /dev/config/local-files`
环境:
- 仅 non-production
用途:
- 列出本地配置目录中的 JSON 文件
### `POST /dev/events/{eventPublicID}/config-sources/import-local`
环境:
- 仅 non-production
用途:
- 从本地配置目录导入 source config
请求体重点:
- `fileName`
- `notes`
### `POST /dev/config-builds/preview`
环境:
- 仅 non-production
用途:
- 基于 source config 生成 preview build
请求体重点:
- `sourceId`
### `POST /dev/config-builds/publish`
环境:
- 仅 non-production
用途:
- 将成功的 preview build 发布成正式 release
- 自动切换 `event.current_release_id`
请求体重点:
- `buildId`
返回重点:
- `release.releaseId`
- `release.manifestUrl`
- `release.configLabel`

View File

@@ -0,0 +1,171 @@
# 数据模型
当前 migration 共 5 版。
## 1. 迁移清单
- [0001_init.sql](D:/dev/cmr-mini/backend/migrations/0001_init.sql)
- [0002_launch.sql](D:/dev/cmr-mini/backend/migrations/0002_launch.sql)
- [0003_home.sql](D:/dev/cmr-mini/backend/migrations/0003_home.sql)
- [0004_results.sql](D:/dev/cmr-mini/backend/migrations/0004_results.sql)
- [0005_config_pipeline.sql](D:/dev/cmr-mini/backend/migrations/0005_config_pipeline.sql)
## 2. 表分组
### 2.1 多租户与入口
- `tenants`
- `entry_channels`
职责:
- 识别品牌壳
- 识别渠道入口
- 承接后续俱乐部 / 政府公众号 / H5 / 二维码入口
### 2.2 用户与登录
- `users`
- `login_identities`
- `auth_sms_codes`
- `auth_refresh_tokens`
职责:
- 平台级用户
- 多身份登录
- 验证码记录
- refresh token 持久化
当前身份示例:
- `mobile`
- `wechat_mini_openid`
- `wechat_unionid`
### 2.3 业务对象与配置发布
- `events`
- `event_releases`
职责分工:
- `events` 管业务对象身份和展示
- `event_releases` 管发布后的运行配置入口
关键字段:
- `events.current_release_id`
- `event_releases.release_public_id`
- `event_releases.config_label`
- `event_releases.manifest_url`
- `event_releases.manifest_checksum_sha256`
- `event_releases.route_code`
### 2.4 首页与入口卡片
- `cards`
职责:
- 支撑首页卡片
- 运营入口聚合
- tenant/channel 维度展示控制
### 2.5 运行态
- `game_sessions`
- `session_results`
职责:
- 固化一局游戏
- 固化该局绑定的 release
- 固化局后结果摘要
### 2.6 配置构建与发布资产
- `event_config_sources`
- `event_config_builds`
- `event_release_assets`
职责:
- 保存编辑态 source config
- 保存构建后的 manifest 和 asset index
- 保存正式 release 关联的资产清单
## 3. 当前最关键的关系
### `tenant -> entry_channel`
一个 tenant 下可有多个渠道入口。
### `user -> login_identity`
一个平台用户可绑定多个登录身份。
### `event -> event_release`
一个 event 可有多个 release。
客户端真正进入游戏时,最终会消费其中一份 release 的 manifest。
### `event_release -> game_session`
一局 session 必须绑定一份明确的 release。
这是当前系统最关键的配置驱动约束。
### `game_session -> session_result`
一局结束后可有一条结果摘要。
### `event_config_source -> event_config_build -> event_release`
这是后续配置生命周期主链:
- source 是编辑态
- build 是构建态
- release 是发布态
## 4. 当前已落库但仍应注意的边界
### 4.1 不要把玩法细节塞回事件主表
当前数据库只记录:
- 发布关系
- manifest 入口
- 结果摘要
玩法解释器仍应留在游戏客户端。
### 4.2 不要让历史局跟随当前 release 漂移
即使 event 后面发布新版本:
- 旧 session 仍然指向旧 `event_release_id`
- 旧 result 仍然对应旧 release
### 4.3 不要把登录态和运行态混在一起
当前已有两种 token
- `access_token`
- `sessionToken`
后面如果加实时网关,也应继续区分。
## 5. 当前缺口
当前 schema 还没有这些模块:
- `competitions`
- `registrations`
- `page_configs`
- `clubs`
- `client_devices`
- 实时票据 / 网关票据
这些后面要按真正业务需要补 migration不要先拍脑袋建大而全表。

View File

@@ -0,0 +1,204 @@
# 核心流程
## 1. 总流程
```mermaid
flowchart LR
A["Entry Resolve"] --> B["Auth"]
B --> C["Home / Cards"]
C --> D["Event Play"]
D --> E["Resolve Release"]
E --> F["Launch Session"]
F --> G["Client Load Manifest"]
G --> H["Session Start / Finish"]
H --> I["Result / History"]
```
## 2. 入口解析
入口层先解决:
- 用户从哪个渠道进来
- 当前归属哪个 `tenant`
- 当前品牌壳和首页卡片应该加载什么
当前对应接口:
- `GET /entry/resolve`
- `GET /home`
- `GET /cards`
- `GET /me/entry-home`
## 3. 登录流程
### 3.1 APP
APP 当前主链是手机号验证码:
1. `POST /auth/sms/send`
2. `POST /auth/login/sms`
3. 返回 `access_token + refresh_token`
### 3.2 微信小程序
微信小程序当前主链是:
1. 客户端 `wx.login`
2. `POST /auth/login/wechat-mini`
3. 后端换取 `openid`
4. 返回 `access_token + refresh_token`
开发环境也支持 `dev-` 前缀 code。
### 3.3 绑定与合并
当小程序用户后续绑定手机号时:
1. 先发 `bind_mobile` 场景验证码
2. `POST /auth/bind/mobile`
3. 如果手机号已属于别的用户,则合并到手机号主账号
当前策略是:
- 手机号账号优先
- 微信轻账号并入手机号账号
## 4. 首页流程
首页不是固定首页,而是“入口上下文首页”。
当前聚合接口:
- `GET /me/entry-home`
它会返回:
- 当前用户
- 当前 tenant
- 当前 channel
- 当前 cards
- 继续中的 session
- 最近一局 session
## 5. Event Play 流程
活动详情页或开始前准备页不应该只拿 `event`
它还必须拿到:
- 当前是否可启动
- 当前会落到哪份 `release`
- 是否有 ongoing session
- 当前推荐动作是什么
当前聚合接口:
- `GET /events/{eventPublicID}/play`
它会返回:
- `event`
- `release`
- `resolvedRelease`
- `play.canLaunch`
- `play.primaryAction`
- `play.launchSource`
- `play.ongoingSession`
- `play.recentSession`
## 6. Launch 流程
### 6.1 当前原则
启动一局游戏时,不是“启动一个 event”。
而是:
> 基于 event 当前可启动的 release创建一条固化 release 的 session。
### 6.2 当前接口
- `POST /events/{eventPublicID}/launch`
当前请求体支持:
- `releaseId`
- `clientType`
- `deviceKey`
当前返回会带:
- `launch.source`
- `launch.resolvedRelease`
- `launch.config`
- `launch.business.sessionId`
- `launch.business.sessionToken`
### 6.3 客户端应如何使用
客户端进入游戏前,应以返回中的这几项为准:
- `launch.resolvedRelease.releaseId`
- `launch.resolvedRelease.manifestUrl`
- `launch.resolvedRelease.manifestChecksumSha256`
而不是再拿 `event` 自己去猜。
## 7. Session 流程
### 7.1 当前接口
- `GET /sessions/{sessionPublicID}`
- `POST /sessions/{sessionPublicID}/start`
- `POST /sessions/{sessionPublicID}/finish`
- `GET /me/sessions`
### 7.2 鉴权模型
查询接口:
-`access_token`
局内动作接口:
-`sessionToken`
这保证了业务登录态和一局游戏运行态是分开的。
## 8. 结果流程
### 8.1 当前接口
- `GET /sessions/{sessionPublicID}/result`
- `GET /me/results`
### 8.2 当前 finish payload
`finish` 当前支持上传结果摘要:
- `finalDurationSec`
- `finalScore`
- `completedControls`
- `totalControls`
- `distanceMeters`
- `averageSpeedKmh`
- `maxHeartRateBpm`
### 8.3 结果页约束
结果页应该基于 session 结果查看,不应该回头去查当前 event 当前 release。
因为:
- 一个 event 未来可能发布新版本
- 历史结果必须追溯到当时真实跑过的那份 release
## 9. 当前最应该坚持的流程约束
业务主线应始终保持为:
`entry -> auth -> event play -> resolve release -> launch -> session -> result`
不要退回成:
`event -> launch -> game`

View File

@@ -0,0 +1,200 @@
# 系统架构
## 1. 目标
当前 backend 不是一个“给地图页喂数据的简单服务”,而是一个业务壳后端。
它负责:
- 用户与登录
- 多租户与多入口
- 首页与业务入口聚合
- Event 业务对象
- 配置发布解析
- 启动一局游戏
- session 生命周期
- 结果沉淀
它不负责:
- 解释游戏玩法细节
- 运行时解析复杂地图规则
- 直接下发数据库编辑态对象给客户端
## 2. 分层
### 2.1 平台层
平台层统一处理:
- `tenant`
- `entry_channel`
- `user`
- `login_identity`
- `auth_refresh_token`
这层是整个平台共用能力。
### 2.2 业务层
业务层统一处理:
- `card`
- `event`
- `event_play`
- `entry_home`
- `profile`
它面向页面和运营入口,但不直接承载游戏规则。
### 2.3 配置发布层
配置发布层统一处理:
- `event_release`
- `manifest_url`
- `manifest_checksum_sha256`
- `route_code`
这层是“客户端真正进入游戏时要消费的运行配置入口”。
### 2.4 运行层
运行层统一处理:
- `game_session`
- `session_token`
- `session_results`
这层不关心编辑态,只关心“一局游戏”。
## 3. 最重要的对象关系
### 3.1 `event`
`event` 是业务对象。
它负责:
- 活动身份
- 展示名称
- 业务状态
- 当前指向的发布版本
它不是客户端实际运行的配置文件本体。
### 3.2 `event_release`
`event_release` 是配置发布对象。
它负责:
- 这次发布的 `manifest_url`
- 配置标签 `config_label`
- 可选校验值
- 可选 `route_code`
进入游戏时,客户端真正需要的是这里。
### 3.3 `game_session`
`game_session` 是运行对象。
它必须固化:
- 当前用户
- 当前 event
- 当前实际使用的 `event_release`
- 当前 `session_token`
这样后续哪怕 event 切到新 release旧 session 也不会漂移。
## 4. 配置驱动原则
这套系统必须坚持下面这条原则:
> 业务层先解析出一份可启动的 release客户端再基于这份 release 的 manifest 进入游戏。
不能走成:
> 客户端拿到 event 后自己再去推断该加载哪份配置
所以当前接口都在往这个方向收口:
- `GET /events/{id}/play` 会返回 `resolvedRelease`
- `POST /events/{id}/launch` 会返回 `resolvedRelease`
- `GET /sessions/{id}` 会返回 `resolvedRelease`
- `GET /sessions/{id}/result` 能追溯到当时的 release
## 5. 代码分层
### 5.1 入口层
- [main.go](D:/dev/cmr-mini/backend/cmd/api/main.go)
- [app.go](D:/dev/cmr-mini/backend/internal/app/app.go)
- [config.go](D:/dev/cmr-mini/backend/internal/app/config.go)
### 5.2 HTTP 层
- [router.go](D:/dev/cmr-mini/backend/internal/httpapi/router.go)
- [handlers](D:/dev/cmr-mini/backend/internal/httpapi/handlers)
- [middleware](D:/dev/cmr-mini/backend/internal/httpapi/middleware)
### 5.3 用例层
- [service](D:/dev/cmr-mini/backend/internal/service)
当前主要服务:
- `AuthService`
- `EntryService`
- `HomeService`
- `EntryHomeService`
- `EventService`
- `EventPlayService`
- `SessionService`
- `ResultService`
- `ProfileService`
- `DevService`
### 5.4 数据层
- [store/postgres](D:/dev/cmr-mini/backend/internal/store/postgres)
特点:
- 手写 SQL
- `pgx` 连接池
- 不依赖 ORM
### 5.5 平台适配层
- [jwtx](D:/dev/cmr-mini/backend/internal/platform/jwtx)
- [security](D:/dev/cmr-mini/backend/internal/platform/security)
- [wechatmini](D:/dev/cmr-mini/backend/internal/platform/wechatmini)
## 6. 当前边界
### 6.1 backend 管什么
- 业务身份
- 配置发布解析
- 启动编排
- 一局的生命周期和结果
### 6.2 游戏客户端管什么
- 下载 `manifest_url`
- 解析运行配置
- 驱动地图和玩法
- 产生过程数据和结束摘要
### 6.3 后续网关该怎么接
后面如果接实时网关,建议仍然走:
- backend 负责登录与 launch
- launch 或 session 负责产出短期实时票据
- 网关只认 backend 签发的运行态票据
不要把微信身份或业务 token 直接暴露给实时网关。

View File

@@ -0,0 +1,412 @@
# 配置管理方案
## 1. 目标
后续 backend 不应该只“管理一个 event JSON 文件”,而应该管理一整套可伸缩的配置生命周期。
这套生命周期至少要覆盖:
1. 编辑态源配置
2. 构建态中间产物
3. 对外发布版本
4. 启动时绑定的 release
5. 运行完成后的 session 追溯
核心目标不是支持当前字段,而是支持以后继续加字段时,主架构不需要推翻。
## 2. 当前现状
当前根目录下的 [event](D:/dev/cmr-mini/event) 已经保存了最小启动配置样例:
- [classic-sequential.json](D:/dev/cmr-mini/event/classic-sequential.json)
- [score-o.json](D:/dev/cmr-mini/event/score-o.json)
从这两个样例看,当前“最小启动配置”已经有了很好的雏形:
- `app`
- `map`
- `playfield`
- `game.mode`
这类文件很适合作为运行时 manifest 的基础形态。
但如果后续继续往里面堆:
- 赛事规则
- 计分规则
- 内容页
- 安全策略
- 品牌配置
- 多媒体资源
- telemetry 开关
- 实验字段
就不能再只靠单个最终 JSON 手工维护了。
## 3. 核心原则
### 3.1 稳定的是层,不是字段
后端要稳定的是这些层:
- `source config`
- `build`
- `release`
- `launch`
- `session`
而不是把所有具体配置字段都设计成强结构数据库列。
### 3.2 编辑态和运行态必须分离
编辑态:
- 配置项可以很多
- 允许草稿
- 允许试验字段
- 允许中间状态
运行态:
- 必须稳定
- 必须可校验
- 必须有版本
- 必须能被客户端直接消费
### 3.3 客户端只消费发布产物
客户端进入游戏时,不应直接读取编辑态对象。
客户端应该只消费:
- `manifest_url`
- `manifest_checksum_sha256`
- 与 manifest 配套的发布资源
### 3.4 session 必须固化 release
只要一局启动了:
- 必须固化 `event_release_id`
- 后续 event 切新发布,不影响老 session
- 结果页和历史页都必须能回看当时那份配置
## 4. 三层配置模型
## 4.1 第一层:源配置
这是编辑态配置。
建议特点:
- 允许字段增长
- 允许草稿
- 允许频繁修改
- 主要存 `jsonb`
它对应“最大启动配置”或“完整编辑配置集合”。
### 可能包含的块
- `app`
- `branding`
- `map`
- `playfield`
- `game`
- `rules`
- `scoring`
- `timeControl`
- `content`
- `assets`
- `safety`
- `telemetry`
- `featureFlags`
## 4.2 第二层:构建产物
这是后端根据源配置构建出来的中间结果。
建议职责:
- schema 校验
- 引用资源补全
- 相对路径转绝对路径
- 生成最终 manifest
- 生成资产清单
- 记录构建日志
这一层是后续做“预览构建”“草稿预览”“发布前检查”的关键。
## 4.3 第三层:发布版本
这是正式对外运行时版本。
建议职责:
- 绑定 build 结果
- 绑定 manifest URL
- 绑定 checksum
- 绑定资源清单
- 进入 launch 链路
当前已有的 `event_releases` 就是这层的起点,但后面还需要更完整的 build / assets 支撑。
## 5. 最小启动配置和最大配置怎么定义
建议不要把“最小配置 / 最大配置”当成数据库对象名,而要作为两种形态理解。
### 5.1 最小启动配置
就是客户端能开局所必需的最小 manifest。
建议包含:
- `schemaVersion`
- `releaseId`
- `app`
- `map`
- `playfield`
- `game`
- 必要资源引用
特点:
- 结构稳定
- 字段尽量少
- 客户端可直接消费
### 5.2 最大配置
就是完整编辑态 source config。
特点:
- 字段可以很多
- 块可以不断扩展
- 不要求直接给客户端消费
- 构建后才会变成运行时 manifest
## 6. 当前 event 目录该扮演什么角色
当前根目录 [event](D:/dev/cmr-mini/event) 建议继续保留,但角色要明确:
它应该是:
- 本地源配置样例目录
- 构建输入参考目录
- 调试和原型验证输入
它不应该直接承担:
- 线上唯一配置源
- 发布版本存储
- 客户端直接运行入口
线上真正的运行入口应当是:
- 数据库里的 release 元数据
- 对象存储/CDN 里的 manifest 和资源
## 7. 数据模型建议
在当前 [数据模型.md](D:/dev/cmr-mini/backend/docs/数据模型.md) 基础上,建议新增 3 张核心表。
这 3 张表的第一版 migration 已经落在:
- [0005_config_pipeline.sql](D:/dev/cmr-mini/backend/migrations/0005_config_pipeline.sql)
## 7.1 `event_config_sources`
用途:
- 存编辑态源配置版本
建议字段:
- `id`
- `event_id`
- `source_version_no`
- `source_kind`
- `schema_id`
- `schema_version`
- `status`
- `source_jsonb`
- `notes`
- `created_by_user_id`
- `created_at`
说明:
- `source_jsonb` 存完整编辑态配置
- `schema_id + schema_version` 用来做校验
## 7.2 `event_config_builds`
用途:
- 存一次构建的结果
建议字段:
- `id`
- `event_id`
- `source_id`
- `build_no`
- `build_status`
- `build_log`
- `manifest_jsonb`
- `asset_index_jsonb`
- `created_by_user_id`
- `created_at`
说明:
- `manifest_jsonb` 是构建后得到的运行 manifest
- `asset_index_jsonb` 是构建时收集到的资源清单
## 7.3 `event_release_assets`
用途:
- 存 release 的资源清单
建议字段:
- `id`
- `event_release_id`
- `asset_type`
- `asset_key`
- `asset_path`
- `asset_url`
- `checksum`
- `size_bytes`
- `meta_jsonb`
说明:
- 这张表非常适合后面做资源核对、回滚、调试和发布检查
## 8. 强结构和弱结构怎么分
## 8.1 强结构字段
这些字段后端应强约束:
- `event_id`
- `release_id`
- `manifest_url`
- `manifest_checksum_sha256`
- `status`
- `published_at`
- `session_public_id`
- `event_release_id`
这些是运行链路基础,不适合做成松散字段。
## 8.2 弱结构字段
这些字段建议主要放 `jsonb`
- 玩法规则
- 计分策略
- 文案内容
- H5 内容块
- 品牌视觉配置
- 资源扩展配置
- feature flags
- 实验字段
这样后面新增字段时,主链路不会被迫重构。
## 9. 后端后续能力建议
## 9.1 源配置管理
建议支持:
- 保存草稿 source
- 查看 source 历史版本
- source diff
- 从文件导入 source
## 9.2 构建能力
建议支持:
- 校验 source schema
- 校验资源引用存在
- 生成 manifest
- 生成 asset index
- 输出 build log
## 9.3 发布能力
建议支持:
- 从某个 build 发布 release
- 生成 `manifest_url`
- 上传 release 资产
- 标记当前生效 release
- 回滚旧 release
## 9.4 调试能力
建议支持:
- 预览构建结果
- 查看某个 release 资产清单
- 查看某个 session 实际绑定的 release 和 manifest
## 10. 推荐 API 路线
建议后面按这个顺序补接口:
### 第一批source
- `POST /events/{id}/config-sources`
- `GET /events/{id}/config-sources`
- `GET /config-sources/{id}`
### 第二批build
- `POST /config-sources/{id}/build`
- `GET /builds/{id}`
- `GET /builds/{id}/manifest`
### 第三批release
- `POST /builds/{id}/release`
- `GET /releases/{id}`
- `GET /releases/{id}/assets`
### 第四批preview
- `GET /events/{id}/preview-play`
- `POST /builds/{id}/preview-launch`
## 11. 推荐开发顺序
当前最值得先做的不是配置后台 UI而是配置构建器。
建议顺序:
1. 先定义 source config 和 manifest 的字段边界
2. 先建 `event_config_sources`
3. 先做 schema 校验器
4. 先做 build 产物生成
5. 再建 `event_config_builds`
6. 再做正式 release 发布
7. 最后才做后台编辑器
原因很简单:
- 没有 build/release 核心能力,后台只是个大表单
- 先把构建链打通,后面各种管理壳层才有基础
## 12. 一句话结论
后续 backend 不该做成“管理一个越来越大的 event JSON 文件”,而应该做成:
> 源配置管理 + 构建产物管理 + release 发布管理 + session 绑定 release
这样以后无论你配置项怎么继续长,主架构都还能撑住。