Files
Genarrative/docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

13 KiB

user_account 表设计

日期:2026-04-21

1. 文档目的

这份文档用于完成 M2 的第一条任务:设计 user_account

目标不是只列一组字段名,而是把以下内容一次钉死到可编码级别:

  1. user_account 在新鉴权体系中的唯一职责
  2. 它与当前 Node users 表的一一映射关系
  3. 它与后续 auth_identityrefresh_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 Option<String> 用户显式设置或重置密码后才写入;手机号/微信新建账号默认不可用密码登录。
password_login_enabled bool 是否允许密码登录;只有用户设置或重置密码后才为 true
token_version u32 access token 统一失效计数,默认 1
display_name String 账号展示名;密码账号默认用户名,手机号账号默认脱敏手机号,微信待绑定账号默认微信昵称或“微信旅人”。
login_provider String 当前账号的主登录归属,枚举固定为 passwordphonewechat
account_status String 账号状态,枚举固定为 activepending_bind_phonedisabled
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_disabledmerged_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 作用:系统账号唯一约束与内部排查,不作为前台密码登录入口
  2. primary_phone_e164 作用:支撑 POST /api/auth/entryPOST /api/auth/phone/loginPOST /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_identityrefresh_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 = disabledmerged_to_user_id 非空,则该记录视为“并入后保留痕迹”,不能再恢复为活跃账号。
  2. 不新增 merged 枚举,统一使用 disabled + status_reason_code = merged_into_existing_account 表达。

8. 字段写入规则

8.1 POST /api/auth/entry

写入规则:

  1. 只读取请求中的 phonepassword
  2. 先把 phone 归一化为 primary_phone_e164 后查询账号。
  3. 若手机号不存在,返回 401,不创建账号。
  4. 若账号存在但 password_login_enabled = falsepassword_hash = null,返回 401
  5. 若账号存在且已设置密码,校验 password_hash
  6. 校验成功后只更新登录会话与 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/logoutPOST /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_hashuser_account 里提前移走。
  2. 因为要做 refresh_session,就把 token_version 也移到会话表中。
  3. 因为微信待绑定账号壳当前无正式游戏数据,就继续走“物理删除账号”策略。
  4. 把手机号明细既放 auth_identity 又放多份等价主字段,导致唯一约束漂移。
  5. 为了图省事把 pending_bind_phonedisabled 混成一个状态。

13. 本任务完成定义

当以下条件满足时,设计 user_account 视为完成:

  1. user_account 的职责边界已经和 auth_identityrefresh_session 明确切开。
  2. 字段、唯一约束、状态枚举、写入规则已具体到可以直接编码。
  3. 已明确与当前 Node users 表的迁移关系。
  4. 后续 auth_identityrefresh_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