|
|
|
|
@@ -0,0 +1,378 @@
|
|
|
|
|
# `user_account` 表设计
|
|
|
|
|
|
|
|
|
|
日期:`2026-04-21`
|
|
|
|
|
|
|
|
|
|
## 1. 文档目的
|
|
|
|
|
|
|
|
|
|
这份文档用于完成 `M2` 的第一条任务:`设计 user_account`。
|
|
|
|
|
|
|
|
|
|
目标不是只列一组字段名,而是把以下内容一次钉死到可编码级别:
|
|
|
|
|
|
|
|
|
|
1. `user_account` 在新鉴权体系中的唯一职责
|
|
|
|
|
2. 它与当前 Node `users` 表的一一映射关系
|
|
|
|
|
3. 它与后续 `auth_identity`、`refresh_session` 的边界
|
|
|
|
|
4. 它需要支撑的 `/api/auth/*` 兼容链路
|
|
|
|
|
5. 它在 SpacetimeDB 中的字段、唯一约束、状态迁移与写入规则
|
|
|
|
|
|
|
|
|
|
## 2. 现有基线
|
|
|
|
|
|
|
|
|
|
当前 Node 鉴权主链已经依赖 `users` 主表完成以下能力:
|
|
|
|
|
|
|
|
|
|
1. `POST /api/auth/entry`:用户名密码登录,不存在则自动创建账号
|
|
|
|
|
2. `POST /api/auth/phone/login`:手机号验证码登录,不存在则自动创建账号
|
|
|
|
|
3. `GET /api/auth/me`:读取当前账号基础信息
|
|
|
|
|
4. `POST /api/auth/logout`:提升 `token_version`,让当前 access token 失效
|
|
|
|
|
5. `POST /api/auth/logout-all`:提升 `token_version` 并吊销全部 refresh session
|
|
|
|
|
6. `POST /api/auth/wechat/bind-phone`:待绑定微信账号激活,或把微信身份归并到已有手机号账号
|
|
|
|
|
|
|
|
|
|
当前 Node `users` 表已有字段基线:
|
|
|
|
|
|
|
|
|
|
1. `id`
|
|
|
|
|
2. `username`
|
|
|
|
|
3. `password_hash`
|
|
|
|
|
4. `token_version`
|
|
|
|
|
5. `display_name`
|
|
|
|
|
6. `login_provider`
|
|
|
|
|
7. `account_status`
|
|
|
|
|
8. `phone_number`
|
|
|
|
|
9. `phone_verified_at`
|
|
|
|
|
10. `created_at`
|
|
|
|
|
11. `updated_at`
|
|
|
|
|
|
|
|
|
|
当前真实业务结论:
|
|
|
|
|
|
|
|
|
|
1. 用户主实体已经存在,不能在 Rust 重写时把账号主表重新拆散成多个等价主表。
|
|
|
|
|
2. `password_hash` 当前仍然是账号主链的一部分,不能因为正式前台主入口转向手机号/微信就直接删除。
|
|
|
|
|
3. `token_version` 当前承担 access token 批量失效语义,必须保留。
|
|
|
|
|
4. 微信待绑定账号壳当前通过 `account_status = pending_bind_phone` 表达,这个状态必须继续保留。
|
|
|
|
|
|
|
|
|
|
## 3. 表职责边界
|
|
|
|
|
|
|
|
|
|
`user_account` 只负责“账号主实体”本身,具体边界固定如下:
|
|
|
|
|
|
|
|
|
|
### 3.1 它负责的内容
|
|
|
|
|
|
|
|
|
|
1. 稳定账号 ID
|
|
|
|
|
2. 账号显示名
|
|
|
|
|
3. 主手机号归属
|
|
|
|
|
4. 账号当前状态
|
|
|
|
|
5. 主登录方式归属
|
|
|
|
|
6. 密码登录所需的 `password_hash`
|
|
|
|
|
7. access token 统一失效计数 `token_version`
|
|
|
|
|
8. 账号级时间戳与合并痕迹
|
|
|
|
|
|
|
|
|
|
### 3.2 它不负责的内容
|
|
|
|
|
|
|
|
|
|
1. 微信 `openid / unionid / avatar` 这类 provider 明细
|
|
|
|
|
2. refresh token hash 与设备会话
|
|
|
|
|
3. 短信验证码发送与校验流水
|
|
|
|
|
4. 风控封禁
|
|
|
|
|
5. 审计日志
|
|
|
|
|
|
|
|
|
|
### 3.3 与其他表的边界
|
|
|
|
|
|
|
|
|
|
1. `auth_identity` 负责一个账号挂了哪些外部登录身份。
|
|
|
|
|
2. `refresh_session` 负责一个账号有哪些活跃设备会话。
|
|
|
|
|
3. `user_account` 只保留“主手机号”和“主登录方式”,不重复承担 provider 明细存储。
|
|
|
|
|
|
|
|
|
|
## 4. 表访问级别
|
|
|
|
|
|
|
|
|
|
`user_account` 固定为 `private table`。
|
|
|
|
|
|
|
|
|
|
原因:
|
|
|
|
|
|
|
|
|
|
1. 表内包含 `password_hash`
|
|
|
|
|
2. 表内包含手机号主归属
|
|
|
|
|
3. 表内包含账号状态和 token 失效控制字段
|
|
|
|
|
4. 前端不应直接查询该表
|
|
|
|
|
|
|
|
|
|
读取方式固定为:
|
|
|
|
|
|
|
|
|
|
1. Axum 通过服务层查询账号信息
|
|
|
|
|
2. SpacetimeDB 内部 reducer / view 通过 owner 身份读取
|
|
|
|
|
3. `/api/auth/me` 等对外接口通过 view / Axum 聚合返回脱敏 DTO
|
|
|
|
|
|
|
|
|
|
## 5. 字段设计
|
|
|
|
|
|
|
|
|
|
建议字段如下。
|
|
|
|
|
|
|
|
|
|
| 字段名 | 类型 | 必填 | 说明 |
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
| `user_id` | `String` | 是 | 主键,继续沿用当前 `user_*` 前缀格式。 |
|
|
|
|
|
| `username` | `String` | 是 | 当前密码登录用户名;手机号/微信创建的系统账号同样要写入唯一用户名。 |
|
|
|
|
|
| `password_hash` | `String` | 是 | 密码登录校验字段;手机号/微信创建账号时继续写随机密码哈希,保持兼容。 |
|
|
|
|
|
| `token_version` | `u32` | 是 | access token 统一失效计数,默认 `1`。 |
|
|
|
|
|
| `display_name` | `String` | 是 | 账号展示名;密码账号默认用户名,手机号账号默认脱敏手机号,微信待绑定账号默认微信昵称或“微信旅人”。 |
|
|
|
|
|
| `login_provider` | `String` | 是 | 当前账号的主登录归属,枚举固定为 `password`、`phone`、`wechat`。 |
|
|
|
|
|
| `account_status` | `String` | 是 | 账号状态,枚举固定为 `active`、`pending_bind_phone`、`disabled`。 |
|
|
|
|
|
| `primary_phone_e164` | `Option<String>` | 否 | 当前账号的主手机号,统一存 `E.164`。 |
|
|
|
|
|
| `phone_verified_at` | `Option<String>` | 否 | 主手机号最近一次完成校验的 UTC RFC3339 时间。 |
|
|
|
|
|
| `last_login_at` | `Option<String>` | 否 | 最近一次完成交互式登录成功的时间,不在 refresh 时更新。 |
|
|
|
|
|
| `merged_to_user_id` | `Option<String>` | 否 | 待绑定微信壳账号并入已有手机号账号时写入目标账号 ID。 |
|
|
|
|
|
| `merged_at` | `Option<String>` | 否 | 写入并入发生时间。 |
|
|
|
|
|
| `status_reason_code` | `Option<String>` | 否 | 非 `active` 状态的原因码,例如 `manual_disabled`、`merged_into_existing_account`。 |
|
|
|
|
|
| `created_at` | `String` | 是 | UTC RFC3339 创建时间。 |
|
|
|
|
|
| `updated_at` | `String` | 是 | UTC RFC3339 最近更新时间。 |
|
|
|
|
|
|
|
|
|
|
补充约束:
|
|
|
|
|
|
|
|
|
|
1. 当前阶段时间字段统一继续使用 UTC RFC3339 字符串,优先对齐现有 Node 数据与调试方式。
|
|
|
|
|
2. `username` 在账号创建后默认不可修改,本轮不设计用户名改名链路。
|
|
|
|
|
3. `password_hash` 当前视为账号主字段,而不是 `auth_identity` 子字段。
|
|
|
|
|
|
|
|
|
|
## 6. 唯一约束与索引
|
|
|
|
|
|
|
|
|
|
### 6.1 必须具备的唯一约束
|
|
|
|
|
|
|
|
|
|
1. `user_id` 主键唯一
|
|
|
|
|
2. `username` 全局唯一
|
|
|
|
|
3. `primary_phone_e164` 在非空时全局唯一
|
|
|
|
|
|
|
|
|
|
### 6.2 必须具备的查询索引
|
|
|
|
|
|
|
|
|
|
1. `username`
|
|
|
|
|
作用:支撑 `POST /api/auth/entry`
|
|
|
|
|
2. `primary_phone_e164`
|
|
|
|
|
作用:支撑 `POST /api/auth/phone/login`、`POST /api/auth/phone/change`
|
|
|
|
|
3. `account_status + updated_at`
|
|
|
|
|
作用:后续管理端、审计排查与禁用账号扫描
|
|
|
|
|
4. `merged_to_user_id`
|
|
|
|
|
作用:微信待绑定壳账号归并后排查与数据修复
|
|
|
|
|
|
|
|
|
|
### 6.3 当前阶段不放进 `user_account` 的查询需求
|
|
|
|
|
|
|
|
|
|
1. 微信身份查找
|
|
|
|
|
2. refresh token hash 查找
|
|
|
|
|
3. 会话列表查找
|
|
|
|
|
|
|
|
|
|
这些查询固定由后续 `auth_identity` 与 `refresh_session` 承担。
|
|
|
|
|
|
|
|
|
|
## 7. 状态机设计
|
|
|
|
|
|
|
|
|
|
`account_status` 当前阶段只允许以下三种值:
|
|
|
|
|
|
|
|
|
|
### 7.1 `active`
|
|
|
|
|
|
|
|
|
|
表示:
|
|
|
|
|
|
|
|
|
|
1. 账号已完成正式激活
|
|
|
|
|
2. 可以进入游戏
|
|
|
|
|
3. 可以创建与读取正式存档
|
|
|
|
|
4. 可以使用 `/api/auth/me`、`/api/auth/sessions` 等正式能力
|
|
|
|
|
|
|
|
|
|
### 7.2 `pending_bind_phone`
|
|
|
|
|
|
|
|
|
|
表示:
|
|
|
|
|
|
|
|
|
|
1. 账号由微信首次登录创建
|
|
|
|
|
2. 已经拿到微信身份,但还没有正式手机号归属
|
|
|
|
|
3. 不允许进入游戏主链
|
|
|
|
|
4. 只允许继续完成绑定手机号或退出
|
|
|
|
|
|
|
|
|
|
### 7.3 `disabled`
|
|
|
|
|
|
|
|
|
|
表示:
|
|
|
|
|
|
|
|
|
|
1. 账号已被人工禁用
|
|
|
|
|
2. 或账号已作为临时壳账号并入其他正式账号
|
|
|
|
|
3. 所有 access token 与 refresh session 后续都应视为不可继续使用
|
|
|
|
|
|
|
|
|
|
附加约束:
|
|
|
|
|
|
|
|
|
|
1. 若 `account_status = disabled` 且 `merged_to_user_id` 非空,则该记录视为“并入后保留痕迹”,不能再恢复为活跃账号。
|
|
|
|
|
2. 不新增 `merged` 枚举,统一使用 `disabled + status_reason_code = merged_into_existing_account` 表达。
|
|
|
|
|
|
|
|
|
|
## 8. 字段写入规则
|
|
|
|
|
|
|
|
|
|
### 8.1 `POST /api/auth/entry`
|
|
|
|
|
|
|
|
|
|
写入规则:
|
|
|
|
|
|
|
|
|
|
1. 先按 `username` 查询
|
|
|
|
|
2. 若不存在,则创建一条 `active` 账号
|
|
|
|
|
3. `login_provider = password`
|
|
|
|
|
4. `display_name = username`
|
|
|
|
|
5. `primary_phone_e164 = null`
|
|
|
|
|
6. `phone_verified_at = null`
|
|
|
|
|
7. `last_login_at = 当前时间`
|
|
|
|
|
|
|
|
|
|
### 8.2 `POST /api/auth/phone/login`
|
|
|
|
|
|
|
|
|
|
写入规则:
|
|
|
|
|
|
|
|
|
|
1. 先按 `primary_phone_e164` 查询
|
|
|
|
|
2. 若不存在,则创建一条 `active` 账号
|
|
|
|
|
3. `login_provider = phone`
|
|
|
|
|
4. `display_name = 脱敏手机号`
|
|
|
|
|
5. `primary_phone_e164 = 验证成功的 E.164 手机号`
|
|
|
|
|
6. `phone_verified_at = 当前时间`
|
|
|
|
|
7. `last_login_at = 当前时间`
|
|
|
|
|
|
|
|
|
|
### 8.3 `GET /api/auth/wechat/callback`
|
|
|
|
|
|
|
|
|
|
写入规则:
|
|
|
|
|
|
|
|
|
|
1. 若该微信身份未绑定任何账号,则创建一条 `pending_bind_phone` 账号壳
|
|
|
|
|
2. `login_provider = wechat`
|
|
|
|
|
3. `display_name = 微信昵称或“微信旅人”`
|
|
|
|
|
4. `primary_phone_e164 = null`
|
|
|
|
|
5. `phone_verified_at = null`
|
|
|
|
|
6. `last_login_at = 当前时间`
|
|
|
|
|
|
|
|
|
|
### 8.4 `POST /api/auth/wechat/bind-phone`
|
|
|
|
|
|
|
|
|
|
分两种情况:
|
|
|
|
|
|
|
|
|
|
1. 手机号未被使用
|
|
|
|
|
结果:更新当前 `pending_bind_phone` 账号为 `active`
|
|
|
|
|
2. 手机号已绑定正式账号
|
|
|
|
|
结果:当前壳账号不再物理删除,而是:
|
|
|
|
|
- `account_status = disabled`
|
|
|
|
|
- `status_reason_code = merged_into_existing_account`
|
|
|
|
|
- `merged_to_user_id = 目标正式账号`
|
|
|
|
|
- `merged_at = 当前时间`
|
|
|
|
|
|
|
|
|
|
同时:
|
|
|
|
|
|
|
|
|
|
1. 微信身份迁移到目标正式账号
|
|
|
|
|
2. 当前壳账号不再允许继续登录
|
|
|
|
|
|
|
|
|
|
### 8.5 `POST /api/auth/phone/change`
|
|
|
|
|
|
|
|
|
|
写入规则:
|
|
|
|
|
|
|
|
|
|
1. 校验当前账号必须是 `active`
|
|
|
|
|
2. 新手机号不能和旧手机号相同
|
|
|
|
|
3. 新手机号必须在 `user_account` 中唯一
|
|
|
|
|
4. 写入新的 `primary_phone_e164`
|
|
|
|
|
5. 写入新的 `phone_verified_at`
|
|
|
|
|
6. 若当前账号展示名本质上仍是旧手机号派生值,则同步更新 `display_name`
|
|
|
|
|
|
|
|
|
|
### 8.6 `POST /api/auth/logout` 与 `POST /api/auth/logout-all`
|
|
|
|
|
|
|
|
|
|
写入规则:
|
|
|
|
|
|
|
|
|
|
1. `token_version = token_version + 1`
|
|
|
|
|
2. `updated_at = 当前时间`
|
|
|
|
|
|
|
|
|
|
说明:
|
|
|
|
|
|
|
|
|
|
1. 继续保留当前 Node 语义,让 access token 可统一失效。
|
|
|
|
|
2. 指定设备吊销只改 `refresh_session`,不修改 `user_account.token_version`。
|
|
|
|
|
|
|
|
|
|
## 9. 读模型约束
|
|
|
|
|
|
|
|
|
|
`user_account` 本身不直接面向前端暴露,但必须能稳定支撑以下读需求:
|
|
|
|
|
|
|
|
|
|
### 9.1 `/api/auth/me`
|
|
|
|
|
|
|
|
|
|
直接提供的字段:
|
|
|
|
|
|
|
|
|
|
1. `user_id`
|
|
|
|
|
2. `display_name`
|
|
|
|
|
3. `login_provider`
|
|
|
|
|
4. `account_status`
|
|
|
|
|
5. `primary_phone_e164`
|
|
|
|
|
|
|
|
|
|
经 Axum 或 view 转换后的字段:
|
|
|
|
|
|
|
|
|
|
1. `phoneNumberMasked`
|
|
|
|
|
2. `bindingStatus`
|
|
|
|
|
3. `loginMethod`
|
|
|
|
|
|
|
|
|
|
### 9.2 `/api/auth/login-options`
|
|
|
|
|
|
|
|
|
|
不直接读取 `user_account`,但需要与 `user_account` 的状态语义兼容:
|
|
|
|
|
|
|
|
|
|
1. 未登录无账号上下文时,只返回可用登录方式
|
|
|
|
|
2. 已登录但 `pending_bind_phone` 时,前端要据 `account_status` 限制继续进入游戏
|
|
|
|
|
|
|
|
|
|
### 9.3 `/api/auth/sessions`
|
|
|
|
|
|
|
|
|
|
只依赖 `user_id` 作为 join 键,不在 `user_account` 上重复放会话信息。
|
|
|
|
|
|
|
|
|
|
## 10. 与当前 Node `users` 表的映射关系
|
|
|
|
|
|
|
|
|
|
| Node `users` 列 | 新 `user_account` 字段 | 迁移规则 |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| `id` | `user_id` | 原样迁移,继续保留 `user_*` 前缀。 |
|
|
|
|
|
| `username` | `username` | 原样迁移。 |
|
|
|
|
|
| `password_hash` | `password_hash` | 原样迁移。 |
|
|
|
|
|
| `token_version` | `token_version` | 原样迁移。 |
|
|
|
|
|
| `display_name` | `display_name` | 原样迁移。 |
|
|
|
|
|
| `login_provider` | `login_provider` | 原样迁移。 |
|
|
|
|
|
| `account_status` | `account_status` | 原样迁移。 |
|
|
|
|
|
| `phone_number` | `primary_phone_e164` | 原样迁移,但命名改为明确表示“主手机号”。 |
|
|
|
|
|
| `phone_verified_at` | `phone_verified_at` | 原样迁移。 |
|
|
|
|
|
| `created_at` | `created_at` | 原样迁移。 |
|
|
|
|
|
| `updated_at` | `updated_at` | 原样迁移。 |
|
|
|
|
|
|
|
|
|
|
新增字段回填规则:
|
|
|
|
|
|
|
|
|
|
1. `last_login_at`
|
|
|
|
|
初次迁移时先回填为 `updated_at`
|
|
|
|
|
2. `merged_to_user_id`
|
|
|
|
|
初次迁移统一为 `null`
|
|
|
|
|
3. `merged_at`
|
|
|
|
|
初次迁移统一为 `null`
|
|
|
|
|
4. `status_reason_code`
|
|
|
|
|
初次迁移统一为 `null`
|
|
|
|
|
|
|
|
|
|
## 11. reducer / service 落地约束
|
|
|
|
|
|
|
|
|
|
为避免后续实现漂移,`user_account` 相关能力固定拆成以下职责:
|
|
|
|
|
|
|
|
|
|
### 11.1 `module-auth` reducer 层
|
|
|
|
|
|
|
|
|
|
必须至少具备这些命令入口:
|
|
|
|
|
|
|
|
|
|
1. `create_password_user_account`
|
|
|
|
|
2. `create_phone_user_account`
|
|
|
|
|
3. `create_pending_wechat_user_account`
|
|
|
|
|
4. `activate_pending_wechat_user_account`
|
|
|
|
|
5. `update_user_account_phone`
|
|
|
|
|
6. `bump_user_account_token_version`
|
|
|
|
|
7. `mark_user_account_merged`
|
|
|
|
|
8. `disable_user_account`
|
|
|
|
|
9. `touch_user_account_last_login`
|
|
|
|
|
|
|
|
|
|
### 11.2 Axum 应用层
|
|
|
|
|
|
|
|
|
|
固定负责:
|
|
|
|
|
|
|
|
|
|
1. 密码校验
|
|
|
|
|
2. 短信验证码校验
|
|
|
|
|
3. 微信 code 换身份
|
|
|
|
|
4. 决定调用哪条 reducer
|
|
|
|
|
5. 再读取后续 view / join 结果返回旧接口 contract
|
|
|
|
|
|
|
|
|
|
## 12. 不允许的设计漂移
|
|
|
|
|
|
|
|
|
|
后续实现时禁止出现以下情况:
|
|
|
|
|
|
|
|
|
|
1. 因为要做 `auth_identity`,就把 `password_hash` 从 `user_account` 里提前移走。
|
|
|
|
|
2. 因为要做 `refresh_session`,就把 `token_version` 也移到会话表中。
|
|
|
|
|
3. 因为微信待绑定账号壳当前无正式游戏数据,就继续走“物理删除账号”策略。
|
|
|
|
|
4. 把手机号明细既放 `auth_identity` 又放多份等价主字段,导致唯一约束漂移。
|
|
|
|
|
5. 为了图省事把 `pending_bind_phone` 和 `disabled` 混成一个状态。
|
|
|
|
|
|
|
|
|
|
## 13. 本任务完成定义
|
|
|
|
|
|
|
|
|
|
当以下条件满足时,`设计 user_account` 视为完成:
|
|
|
|
|
|
|
|
|
|
1. `user_account` 的职责边界已经和 `auth_identity`、`refresh_session` 明确切开。
|
|
|
|
|
2. 字段、唯一约束、状态枚举、写入规则已具体到可以直接编码。
|
|
|
|
|
3. 已明确与当前 Node `users` 表的迁移关系。
|
|
|
|
|
4. 后续 `auth_identity` 与 `refresh_session` 的设计可以直接以这份文档为前置约束继续展开。
|
|
|
|
|
|
|
|
|
|
## 14. 依据文件
|
|
|
|
|
|
|
|
|
|
1. `server-node/src/routes/authRoutes.ts`
|
|
|
|
|
2. `server-node/src/auth/authService.ts`
|
|
|
|
|
3. `server-node/src/repositories/userRepository.ts`
|
|
|
|
|
4. `server-node/src/repositories/authIdentityRepository.ts`
|
|
|
|
|
5. `server-node/src/repositories/userSessionRepository.ts`
|
|
|
|
|
6. `server-node/src/db/migrations.ts`
|
|
|
|
|
7. `docs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.md`
|
|
|
|
|
8. `backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md`
|
|
|
|
|
9. `docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md`
|