702 lines
11 KiB
Markdown
702 lines
11 KiB
Markdown
# 业务后端数据库初版方案
|
||
> 文档版本:v1.0
|
||
> 最后更新:2026-04-02 08:28:05
|
||
|
||
|
||
## 1. 目标
|
||
|
||
本文档定义本项目业务后端第一版数据库方案。
|
||
|
||
第一版目标不是一次性覆盖整个平台所有能力,而是先稳定支撑以下范围:
|
||
|
||
- 微信小程序登录与用户身份
|
||
- 用户身体资料
|
||
- 多租户与俱乐部基础隔离
|
||
- 首页卡片、赛事、地图、Event 查询
|
||
- 配置对象的版本化管理与发布
|
||
- `launch` 启动与 `session_token`
|
||
- 小程序业务层与配置驱动游戏层对接
|
||
|
||
明确不纳入第一版数据库的能力:
|
||
|
||
- 支付与分账
|
||
- UGC 审核流
|
||
- 订单退款
|
||
- 成绩回放明细
|
||
- GPS / 心率明细归档
|
||
- 复杂运营报表
|
||
|
||
这些能力建议放到后续 migration。
|
||
|
||
## 2. 核心原则
|
||
|
||
### 2.1 业务数据与配置数据分层
|
||
|
||
数据库里同时存在两类对象:
|
||
|
||
- 业务状态对象
|
||
- 配置发布对象
|
||
|
||
两者要分层,不要揉成一个“超级事件表”。
|
||
|
||
### 2.2 运行态配置仍然走发布产物
|
||
|
||
数据库管理的是编辑态与发布关系,客户端运行时最终仍然消费静态发布结果,例如:
|
||
|
||
- `manifest_url`
|
||
- `manifest_checksum_sha256`
|
||
- 地图资源路径
|
||
- Event 发布配置
|
||
|
||
不要让客户端在运行时直接拼数据库对象。
|
||
|
||
### 2.3 游戏规则不进业务表细字段
|
||
|
||
业务后端负责:
|
||
|
||
- 用户
|
||
- 赛事
|
||
- 报名
|
||
- 发布
|
||
- 启动
|
||
|
||
业务后端不应该成为所有玩法字段的解释器。玩法细节优先放在版本化 `jsonb` 内容中。
|
||
|
||
### 2.4 所有对外 ID 使用 public_id
|
||
|
||
数据库内部主键统一使用 `uuid`。
|
||
|
||
对客户端暴露的对象使用稳定 `public_id`:
|
||
|
||
- `user_public_id`
|
||
- `competition_public_id`
|
||
- `map_public_id`
|
||
- `event_public_id`
|
||
- `release_public_id`
|
||
- `session_public_id`
|
||
|
||
原因:
|
||
|
||
- 内外 ID 解耦
|
||
- 便于迁移
|
||
- 便于白标和多端统一
|
||
- 避免顺序 ID 暴露内部增长信息
|
||
|
||
### 2.5 token 只存 hash,不存明文
|
||
|
||
以下内容不应明文入库:
|
||
|
||
- 短信验证码
|
||
- refresh token
|
||
- session token
|
||
|
||
数据库只保存 hash。
|
||
|
||
## 3. 第一版建议模块
|
||
|
||
第一版数据库建议拆成 6 组:
|
||
|
||
1. 租户与组织
|
||
2. 用户与登录
|
||
3. 配置对象与发布
|
||
4. 赛事业务对象
|
||
5. 页面与卡片
|
||
6. 启动与 session
|
||
|
||
## 4. 表清单
|
||
|
||
### 4.1 租户与组织
|
||
|
||
#### `tenants`
|
||
|
||
平台租户。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `tenant_code`
|
||
- `name`
|
||
- `status`
|
||
- `theme_jsonb`
|
||
- `settings_jsonb`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
说明:
|
||
|
||
- `theme_jsonb` 存白标主题
|
||
- `settings_jsonb` 存租户级开关
|
||
|
||
#### `clubs`
|
||
|
||
租户下的俱乐部或品牌实体。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `tenant_id`
|
||
- `club_code`
|
||
- `name`
|
||
- `status`
|
||
- `profile_jsonb`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
说明:
|
||
|
||
- 第一版建议 club 从属于 tenant
|
||
- 不建议一开始做过深的组织树
|
||
|
||
### 4.2 用户与登录
|
||
|
||
#### `app_users`
|
||
|
||
平台用户主表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `user_public_id`
|
||
- `default_tenant_id`
|
||
- `status`
|
||
- `nickname`
|
||
- `avatar_url`
|
||
- `last_login_at`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
#### `login_identities`
|
||
|
||
登录身份绑定表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `user_id`
|
||
- `identity_type`
|
||
- `provider`
|
||
- `provider_subject`
|
||
- `country_code`
|
||
- `mobile`
|
||
- `status`
|
||
- `profile_jsonb`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
身份示例:
|
||
|
||
- 手机号
|
||
- 微信 `openid`
|
||
- 微信 `unionid`
|
||
|
||
#### `client_devices`
|
||
|
||
客户端设备标识记录。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `device_key`
|
||
- `platform`
|
||
- `first_seen_at`
|
||
- `last_seen_at`
|
||
- `meta_jsonb`
|
||
|
||
说明:
|
||
|
||
- `device_key` 对应前端 `device_id`
|
||
|
||
#### `auth_sms_codes`
|
||
|
||
短信验证码发送与校验记录。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `scene`
|
||
- `country_code`
|
||
- `mobile`
|
||
- `client_type`
|
||
- `device_key`
|
||
- `code_hash`
|
||
- `provider_payload_jsonb`
|
||
- `expires_at`
|
||
- `cooldown_until`
|
||
- `consumed_at`
|
||
- `created_at`
|
||
|
||
#### `auth_refresh_tokens`
|
||
|
||
刷新 token 持久化表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `user_id`
|
||
- `client_type`
|
||
- `device_key`
|
||
- `token_hash`
|
||
- `issued_at`
|
||
- `expires_at`
|
||
- `revoked_at`
|
||
- `replaced_by_token_id`
|
||
|
||
#### `user_body_profiles`
|
||
|
||
用户当前身体档案。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `user_id`
|
||
- `status`
|
||
- `completed_at`
|
||
- `current_version_id`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
#### `user_body_profile_versions`
|
||
|
||
身体档案历史版本。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `profile_id`
|
||
- `version_no`
|
||
- `gender`
|
||
- `birth_date`
|
||
- `height_cm`
|
||
- `weight_kg`
|
||
- `resting_heart_rate_bpm`
|
||
- `max_heart_rate_bpm`
|
||
- `created_at`
|
||
|
||
### 4.3 配置对象与发布
|
||
|
||
这一组直接对应你现有的配置驱动架构。
|
||
|
||
#### `maps` / `map_versions`
|
||
|
||
地图对象及版本。
|
||
|
||
主表管理:
|
||
|
||
- `map_public_id`
|
||
- `slug`
|
||
- `name`
|
||
- `status`
|
||
- `tenant_id`
|
||
- `current_version_id`
|
||
|
||
版本表管理:
|
||
|
||
- `version_no`
|
||
- `content_jsonb`
|
||
- `created_at`
|
||
|
||
#### `playfields` / `playfield_versions`
|
||
|
||
路线、点位、场地定义。
|
||
|
||
#### `game_modes` / `game_mode_versions`
|
||
|
||
玩法模式配置,例如:
|
||
|
||
- `classic-sequential`
|
||
- `score-o`
|
||
|
||
#### `resource_packs` / `resource_pack_versions`
|
||
|
||
资源包,例如:
|
||
|
||
- 音效
|
||
- 素材包
|
||
- UI 资源集
|
||
|
||
#### `events` / `event_versions`
|
||
|
||
Event 本身与版本。
|
||
|
||
建议:
|
||
|
||
- `events` 管对象身份
|
||
- `event_versions` 管编辑态装配结果
|
||
|
||
`event_versions` 推荐显式引用:
|
||
|
||
- `map_version_id`
|
||
- `playfield_version_id`
|
||
- `game_mode_version_id`
|
||
- `resource_pack_version_id`
|
||
|
||
同时保留:
|
||
|
||
- `content_jsonb`
|
||
|
||
这样既有强关系,又保留灵活字段。
|
||
|
||
#### `event_releases`
|
||
|
||
发布记录表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `release_public_id`
|
||
- `event_id`
|
||
- `event_version_id`
|
||
- `release_no`
|
||
- `manifest_url`
|
||
- `manifest_checksum_sha256`
|
||
- `status`
|
||
- `published_by_user_id`
|
||
- `published_at`
|
||
- `payload_jsonb`
|
||
|
||
说明:
|
||
|
||
- 客户端主要读这张表产出的 URL 与校验值
|
||
- `events.current_release_id` 可指向当前对外生效版本
|
||
|
||
### 4.4 赛事业务对象
|
||
|
||
#### `competitions`
|
||
|
||
赛事主表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `competition_public_id`
|
||
- `tenant_id`
|
||
- `club_id`
|
||
- `slug`
|
||
- `display_name`
|
||
- `status`
|
||
- `registration_enabled`
|
||
- `leaderboard_enabled`
|
||
- `realtime_board_enabled`
|
||
- `competition_start_at`
|
||
- `competition_end_at`
|
||
- `content_jsonb`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
#### `competition_events`
|
||
|
||
赛事与 Event 的关联表。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `competition_id`
|
||
- `event_id`
|
||
- `event_release_id`
|
||
- `is_default`
|
||
- `sort_order`
|
||
- `relation_status`
|
||
- `created_at`
|
||
|
||
说明:
|
||
|
||
- 支持赛事绑定多个 Event
|
||
- 支持按赛事锁定某个 release
|
||
|
||
#### `registrations`
|
||
|
||
报名记录。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `registration_public_id`
|
||
- `competition_id`
|
||
- `user_id`
|
||
- `group_id`
|
||
- `status`
|
||
- `form_payload_jsonb`
|
||
- `approved_at`
|
||
- `cancelled_at`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
说明:
|
||
|
||
- 第一版先不强行拆复杂参赛人结构
|
||
- `form_payload_jsonb` 足够承接早期变化
|
||
|
||
### 4.5 页面与卡片
|
||
|
||
#### `page_configs` / `page_config_versions`
|
||
|
||
H5 / 白标页面配置。
|
||
|
||
主表建议字段:
|
||
|
||
- `id`
|
||
- `tenant_id`
|
||
- `club_id`
|
||
- `page_code`
|
||
- `name`
|
||
- `status`
|
||
- `current_version_id`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
版本表建议字段:
|
||
|
||
- `id`
|
||
- `page_config_id`
|
||
- `version_no`
|
||
- `dsl_jsonb`
|
||
- `theme_jsonb`
|
||
- `feature_flags_jsonb`
|
||
- `status`
|
||
- `created_at`
|
||
|
||
#### `cards`
|
||
|
||
首页卡片与运营入口。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `card_public_id`
|
||
- `tenant_id`
|
||
- `club_id`
|
||
- `card_type`
|
||
- `display_name`
|
||
- `competition_id`
|
||
- `event_id`
|
||
- `map_id`
|
||
- `page_config_id`
|
||
- `html_url`
|
||
- `cover_url`
|
||
- `display_slot`
|
||
- `display_priority`
|
||
- `status`
|
||
- `starts_at`
|
||
- `ends_at`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
说明:
|
||
|
||
- 这张表直接支撑 `/cards`
|
||
- 允许卡片指向赛事、页面或其他目标
|
||
|
||
### 4.6 启动与 session
|
||
|
||
#### `game_sessions`
|
||
|
||
游戏启动记录。
|
||
|
||
建议字段:
|
||
|
||
- `id`
|
||
- `session_public_id`
|
||
- `tenant_id`
|
||
- `user_id`
|
||
- `competition_id`
|
||
- `registration_id`
|
||
- `event_id`
|
||
- `event_release_id`
|
||
- `launch_request_id`
|
||
- `participant_public_id`
|
||
- `device_key`
|
||
- `client_type`
|
||
- `route_code`
|
||
- `status`
|
||
- `session_token_hash`
|
||
- `session_token_expires_at`
|
||
- `realtime_endpoint`
|
||
- `realtime_token_hash`
|
||
- `launched_at`
|
||
- `started_at`
|
||
- `ended_at`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
说明:
|
||
|
||
- 第一版闭环到 `launch` 即可
|
||
- `session_token` 用于后续 session 相关接口开放后继续扩展
|
||
- `launch_request_id` 需要唯一,支撑幂等
|
||
|
||
## 5. 当前 API 到表的映射
|
||
|
||
### `POST /auth/sms/send`
|
||
|
||
写:
|
||
|
||
- `auth_sms_codes`
|
||
|
||
### `POST /auth/login/sms`
|
||
|
||
读写:
|
||
|
||
- `auth_sms_codes`
|
||
- `app_users`
|
||
- `login_identities`
|
||
- `user_body_profiles`
|
||
- `user_body_profile_versions`
|
||
- `auth_refresh_tokens`
|
||
|
||
### `POST /auth/login/wechat`
|
||
|
||
读写:
|
||
|
||
- `app_users`
|
||
- `login_identities`
|
||
- `user_body_profiles`
|
||
- `user_body_profile_versions`
|
||
- `auth_refresh_tokens`
|
||
|
||
### `POST /auth/refresh`
|
||
|
||
读写:
|
||
|
||
- `auth_refresh_tokens`
|
||
|
||
### `PUT /me/body-profile`
|
||
|
||
读写:
|
||
|
||
- `user_body_profiles`
|
||
- `user_body_profile_versions`
|
||
|
||
### `GET /cards`
|
||
|
||
读:
|
||
|
||
- `cards`
|
||
- 可选补充 `competitions`
|
||
|
||
### `GET /competitions/{competition_id}`
|
||
|
||
读:
|
||
|
||
- `competitions`
|
||
- `competition_events`
|
||
- `events`
|
||
- `event_releases`
|
||
- `registrations`
|
||
|
||
### `GET /events/{event_id}` / `GET /competitions/{competition_id}/events/{event_id}`
|
||
|
||
读:
|
||
|
||
- `events`
|
||
- `event_releases`
|
||
- `maps`
|
||
- `competitions`
|
||
- `registrations`
|
||
|
||
### `POST /competitions/{competition_id}/registrations`
|
||
|
||
写:
|
||
|
||
- `registrations`
|
||
|
||
### `POST /events/{event_id}/launch`
|
||
|
||
读写:
|
||
|
||
- `events`
|
||
- `event_releases`
|
||
- `registrations` 可选
|
||
- `game_sessions`
|
||
|
||
## 6. 第一版不建议做复杂拆分的地方
|
||
|
||
以下字段第一版优先用 `jsonb`,不要先做一堆子表:
|
||
|
||
- 赛事详情扩展内容
|
||
- 报名附加表单
|
||
- 页面 DSL
|
||
- theme 配置
|
||
- feature flags
|
||
- Event 覆盖项
|
||
- 配置对象的实验字段
|
||
|
||
原因很简单:
|
||
|
||
- 你现在业务和玩法都在快速变化
|
||
- 先保留灵活性比过度范式化更重要
|
||
|
||
## 7. 第一版不建议进入数据库的内容
|
||
|
||
第一版不建议落库:
|
||
|
||
- 实时网关运行态内存结构
|
||
- GPS 点逐秒明细
|
||
- 心率逐秒明细
|
||
- WebGL 渲染状态
|
||
- 设备桥接瞬时事件
|
||
|
||
这些应该仍然留在:
|
||
|
||
- `realtime-gateway`
|
||
- 对象存储
|
||
- 后续归档服务
|
||
|
||
不应直接压进业务主库。
|
||
|
||
## 8. 推荐 migration 顺序
|
||
|
||
建议按下面顺序建表:
|
||
|
||
1. 租户与组织
|
||
2. 用户与登录
|
||
3. 配置对象与版本表
|
||
4. 发布表
|
||
5. 赛事与关联
|
||
6. 页面与卡片
|
||
7. session
|
||
|
||
这样依赖关系最清晰。
|
||
|
||
## 9. 第二版可新增的模块
|
||
|
||
建议后续 migration 再补:
|
||
|
||
- `orders`
|
||
- `payment_transactions`
|
||
- `refunds`
|
||
- `ugc_assets`
|
||
- `ugc_posts`
|
||
- `ugc_reviews`
|
||
- `session_uploads`
|
||
- `session_results`
|
||
- `gps_tracks`
|
||
- `heart_rate_streams`
|
||
- `channel_entries`
|
||
- `campaigns`
|
||
|
||
## 10. 当前最适合你的起步方式
|
||
|
||
如果你现在准备开始做后端,我建议不要先写所有 API,而是按这个顺序开工:
|
||
|
||
1. 先建数据库与 migration
|
||
2. 先写用户、赛事、Event、launch 这 4 个核心域
|
||
3. 先让小程序跑通登录 -> 看赛事 -> launch -> 进入游戏
|
||
4. 再补报名
|
||
5. 再补页面配置、卡片、俱乐部首页
|
||
6. 支付和 UGC 放到后续版本
|
||
|
||
## 11. 一句话总结
|
||
|
||
第一版数据库应该同时支撑两件事:
|
||
|
||
- 业务闭环
|
||
- 配置发布
|
||
|
||
但不能把它们混成一套随意增长的表结构。
|
||
|
||
正确方向是:
|
||
|
||
> PostgreSQL 存业务状态 + 版本化配置对象,Go API 负责查询与发布编排,客户端继续消费发布后的运行态配置。
|
||
|
||
|