docs: design auth user account table

This commit is contained in:
2026-04-21 02:03:49 +08:00
parent a1a02eee7d
commit eaccad289f
5 changed files with 390 additions and 2 deletions

View File

@@ -140,7 +140,8 @@
### SpacetimeDB 身份表 ### SpacetimeDB 身份表
- [ ] 设计 `user_account` - [x] 设计 `user_account`
交付物:[../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
- [ ] 设计 `auth_identity` - [ ] 设计 `auth_identity`
- [ ] 设计 `refresh_session` - [ ] 设计 `refresh_session`
- [ ] 设计 `auth_audit_log` - [ ] 设计 `auth_audit_log`

View File

@@ -4,6 +4,7 @@
## 文档列表 ## 文档列表
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。 - [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
- [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。 - [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。
- [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。 - [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。

View File

@@ -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`

View File

@@ -306,6 +306,10 @@ server-rs/
2. 密码哈希、短信验证码校验、微信 code 换 token 的动作在 Axum 完成。 2. 密码哈希、短信验证码校验、微信 code 换 token 的动作在 Axum 完成。
3. Axum 再调用 reducer 写入最终结果。 3. Axum 再调用 reducer 写入最终结果。
`user_account` 的详细字段、状态迁移与旧 `users` 映射规则,见:
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
### B. 运行时主状态表 ### B. 运行时主状态表
- `runtime_snapshot` - `runtime_snapshot`

View File

@@ -13,7 +13,7 @@
## 2. 当前阶段说明 ## 2. 当前阶段说明
当前提交仅完成目录占位,不提前进入接口、表结构与 token 细节实现 当前阶段已先冻结第一张账号主表 `user_account` 的设计,其余身份表、会话表与 token 细节仍按顺序继续展开
后续与本 package 直接相关的任务包括: 后续与本 package 直接相关的任务包括:
@@ -22,6 +22,10 @@
3. 设计 `sms_auth_event``wechat_auth_state` 3. 设计 `sms_auth_event``wechat_auth_state`
4. 落地 JWT claims、refresh cookie 与旧接口兼容 4. 落地 JWT claims、refresh cookie 与旧接口兼容
当前已冻结文档:
1. [../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
## 3. 边界约束 ## 3. 边界约束
1. `module-auth` 负责鉴权领域规则与模块级编排,不直接把供应商 SDK 逻辑写进主工程。 1. `module-auth` 负责鉴权领域规则与模块级编排,不直接把供应商 SDK 逻辑写进主工程。