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