Files
Genarrative/docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md
2026-04-29 20:56:59 +08:00

13 KiB
Raw Blame History

auth_identity 表设计

日期:2026-04-21

1. 文档目的

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

目标是把“账号主实体”和“外部登录身份”彻底拆清楚,让后续编码时不会再出现:

  1. 微信身份字段继续堆进 user_account
  2. 手机号既是账号字段又没有对应身份行
  3. 微信待绑定壳账号并入正式手机号账号时,不知道到底迁哪些记录

2. 当前基线

当前 Node 后端已经有一张 auth_identities 表,但范围还不完整:

  1. 当前只真实存了 wechat 身份
  2. 手机号登录仍主要依赖 users.phone_number
  3. /api/auth/mewechatBound 依赖查询 auth_identities
  4. 微信登录后的“已有正式账号 / 首次待绑定账号 / 绑定后归并”三种分支,都已经在 authService.ts 中真实发生

当前 Node auth_identities 字段基线:

  1. id
  2. user_id
  3. provider
  4. provider_uid
  5. provider_unionid
  6. display_name
  7. avatar_url
  8. is_verified
  9. meta_json
  10. created_at
  11. updated_at

这说明:

  1. auth_identity 在现有系统里已经不是新概念,而是微信登录链路的既有事实。
  2. Rust 重写时不能再把它降级成“可有可无的附属表”。
  3. 但它也还没有扩展到手机号身份层,因此本轮需要把“手机号是否入表、如何入表”钉死。

3. 与 user_account 的边界

auth_identityuser_account 的固定边界如下。

3.1 user_account 负责

  1. 账号主键与主状态
  2. password_hash
  3. token_version
  4. display_name
  5. primary_phone_e164
  6. login_provider

3.2 auth_identity 负责

  1. 一个账号绑定了哪些外部身份
  2. 每个 provider 的唯一主体键
  3. provider 级验证状态
  4. provider 资料快照
  5. provider 绑定时间与最近一次使用时间
  6. 身份迁移时的归属变更

3.3 不允许的漂移

  1. 不把 password_hash 放进 auth_identity
  2. 不把 refresh token hash 放进 auth_identity
  3. 不让 auth_identity 取代 user_account 成为账号主表
  4. 不再只给微信建 identity 行,而让手机号继续做“无 identity 的特殊分支”

4. provider 范围

当前阶段 auth_identity.provider 固定只支持:

  1. phone
  2. wechat

说明:

  1. password 当前仍留在 user_account.password_hash,不进入 auth_identity
  2. 后续若补 OIDC 或更多社交登录,再在新任务里追加 provider不在本轮预支。

5. 字段设计

字段名 类型 必填 说明
identity_id String 主键,建议沿用 authi_* 前缀。
user_id String 归属账号 ID外键指向 user_account.user_id
provider String 枚举固定为 phonewechat
provider_uid String provider 主主体键;phone 时固定等于 phone_e164wechat 时固定存 provider 返回的稳定 UID。
provider_unionid Option<String> wechat 使用;有值时作为更高优先级唯一键。
phone_e164 Option<String> phone provider 使用,统一存 E.164
display_name Option<String> provider 侧显示名快照;wechat 可用昵称,phone 可为空。
avatar_url Option<String> provider 侧头像;当前主要给 wechat 用。
is_verified bool 当前 identity 是否已完成可信校验。
is_primary bool 当前 identity 是否是该 provider 在本账号下的主身份。
meta_json Option<String> provider 扩展元信息 JSON 字符串。
bound_at String 首次绑定到该账号的 UTC RFC3339 时间。
last_login_at Option<String> 最近一次由该 identity 驱动完成交互式登录成功的时间。
created_at String UTC RFC3339 创建时间。
updated_at String UTC RFC3339 最近更新时间。

补充说明:

  1. 当前阶段统一继续使用 UTC RFC3339 字符串时间,先与现有 Node 表与调试习惯对齐。
  2. meta_json 当前建议直接存 JSON 字符串,避免后续第一版 schema 先引入过宽的嵌套类型。
  3. is_primary 当前虽然大多是冗余字段,但必须先保留,为后续多 identity/同 provider 扩展留稳定位置。

6. provider 专属约束

6.1 phone provider

固定规则:

  1. provider_uid = phone_e164
  2. provider_unionid = null
  3. phone_e164 != null
  4. is_verified = true
  5. is_primary = true

当前阶段约束:

  1. 一个账号最多只允许一条活跃 phone identity
  2. 它必须与 user_account.primary_phone_e164 一致

6.2 wechat provider

固定规则:

  1. provider_uid 必填
  2. provider_unionid 可空,但若存在则优先作为稳定唯一键
  3. phone_e164 = null
  4. display_nameavatar_urlmeta_json 可按登录回调刷新
  5. is_verified = true

当前阶段约束:

  1. 一个账号最多只允许一条活跃 wechat identity
  2. 微信首次登录但未绑定手机号时,也必须先创建这条 identity 行

7. 唯一约束与索引

7.1 必须具备的唯一约束

  1. identity_id 主键唯一
  2. (provider, provider_uid) 全局唯一
  3. (provider, provider_unionid)provider_unionid != null 时全局唯一
  4. (provider, phone_e164)provider = phonephone_e164 != null 时全局唯一

7.2 必须具备的查询索引

  1. (user_id, provider) 作用:支撑 /api/auth/me、账号中心与 provider 检查
  2. provider_uid 作用:支撑微信回调身份查找
  3. provider_unionid 作用:支撑微信跨应用稳定身份查找
  4. phone_e164 作用:支撑手机号登录、手机号换绑与手机号冲突检查

8. 与 user_account 的一致性规则

8.1 手机号 identity 与账号主手机号必须一致

只要 user_account.primary_phone_e164 非空,就必须满足:

  1. 同一 user_id 下存在一条 provider = phone 的 identity
  2. 该行 phone_e164 = user_account.primary_phone_e164
  3. 该行 provider_uid = user_account.primary_phone_e164
  4. 该行 is_primary = true
  5. 该行 is_verified = true

8.2 login_provider 不等于 identity.provider

必须明确:

  1. user_account.login_provider 表示账号主归属方式
  2. auth_identity.provider 表示账号下挂了哪一种外部身份

因此一个手机号正式账号在绑定微信后,完全允许出现:

  1. user_account.login_provider = phone
  2. 同时存在 provider = phoneprovider = wechat 两条 identity

8.3 待绑定微信账号允许没有手机号 identity

若:

  1. user_account.account_status = pending_bind_phone
  2. user_account.primary_phone_e164 = null

则允许:

  1. 当前账号只有一条 wechat identity
  2. 不存在 phone identity

9. 写入场景设计

9.1 POST /api/auth/entry

当前阶段不写 auth_identity

原因:

  1. 密码登录仍由 user_account.password_hash 承担
  2. 本轮不引入 password provider identity
  3. 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或陶泥号作为登录身份
  4. 密码登录不创建账号,新账号只由手机号验证码登录创建

9.2 POST /api/auth/phone/login

写入规则:

  1. 若账号不存在,则先创建 user_account
  2. 同时创建一条 provider = phone identity
  3. 若账号已存在,则校验手机号一致并更新 last_login_at
  4. bound_at 只在首次绑定时写入

9.3 GET /api/auth/wechat/callback

写入规则:

  1. 先按 provider_unionid 查;没有再按 provider_uid
  2. 若 identity 已存在,则更新资料快照并写 last_login_at
  3. 若本次是按 provider_unionid 命中,但 provider_uid 已变化,则必须把新的 provider_uid 一并回写为最新主映射
  4. 若 identity 不存在,则创建一条新的 wechat identity
  5. 若是首次微信登录,还要同步创建 pending_bind_phoneuser_account

9.4 POST /api/auth/wechat/bind-phone

分两种情况:

  1. 手机号未被其他账号使用
    • 当前账号写入 phone identity
    • 当前 user_accountactive
  2. 手机号已绑定已有正式账号
    • 不新建第二条 phone identity
    • 把当前壳账号下的 wechat identity 迁移到目标正式账号
    • 当前壳账号标记为并入

9.5 POST /api/auth/phone/change

写入规则:

  1. 更新当前账号唯一一条 phone identity 的 phone_e164
  2. 同步更新 provider_uid
  3. 更新 bound_at 不变
  4. 更新 last_login_at 不变
  5. updated_at = 当前时间

说明:

  1. 当前阶段手机号换绑不保留“旧手机号 identity 历史行”。
  2. 历史审计交给 auth_audit_log,不让 identity 表承担日志表职责。

10. 迁移场景设计

10.1 从当前 Node auth_identities 迁移

wechat identity 直接一比一迁移:

Node 列 新字段
id identity_id
user_id user_id
provider provider
provider_uid provider_uid
provider_unionid provider_unionid
display_name display_name
avatar_url avatar_url
is_verified is_verified
meta_json meta_json
created_at bound_atcreated_at
updated_at updated_at

last_login_at 初次迁移规则:

  1. 先回填为 updated_at

10.2 从当前 Node users.phone_number 反向补 phone identity

凡是当前 users.phone_number 非空的账号,都要补一条 phone identity

  1. provider = phone
  2. provider_uid = phone_number
  3. phone_e164 = phone_number
  4. display_name = null
  5. avatar_url = null
  6. is_verified = phone_verified_at != null
  7. is_primary = true
  8. bound_at = phone_verified_at ?? created_at
  9. last_login_at = updated_at

10.3 初次迁移的兼容结论

迁移完成后,必须满足:

  1. 每个带手机号的账号都有一条 phone identity
  2. 每个微信已绑定账号都有一条 wechat identity
  3. /api/auth/mewechatBound 不再依赖旧 Node 仓储逻辑,而能直接从 auth_identity 派生

11. 读模型约束

auth_identity 至少要支撑以下读需求:

11.1 /api/auth/me

需要派生:

  1. wechatBound
  2. bindingStatus
  3. phoneNumberMasked

其中:

  1. phoneNumberMasked 最终以 user_account.primary_phone_e164 为准
  2. wechatBound 以是否存在 provider = wechat identity 为准

11.2 /api/auth/wechat/start/api/auth/wechat/callback

需要支撑:

  1. 微信 callback 身份查找
  2. 首次登录 identity 创建
  3. 绑定后 identity 迁移

11.3 账号安全页

未来至少会依赖:

  1. 当前账号绑定了哪些 provider
  2. 是否完成手机号正式归属
  3. 微信头像与昵称展示

12. reducer / service 落地约束

12.1 module-auth reducer 层

必须至少具备这些命令入口:

  1. create_phone_identity
  2. upsert_phone_identity_after_phone_change
  3. create_wechat_identity
  4. touch_identity_last_login
  5. refresh_wechat_identity_profile
  6. move_identity_to_user

12.2 Axum 应用层

固定负责:

  1. 从微信 provider 拿到 uid / unionid / 昵称 / 头像
  2. 决定微信 callback 应该命中已有 identity 还是创建新 identity
  3. 在手机号绑定或换绑成功后,同步调用 user_accountauth_identity 相关 reducer

13. 不允许的设计漂移

后续实现时禁止出现以下情况:

  1. 继续让手机号登录只写 user_account.primary_phone_e164,不写 auth_identity
  2. 把微信 unionid 丢到 meta_json 里,而不是单独做唯一约束字段
  3. 微信壳账号并入正式账号时直接删除 wechat identity 再重建,导致 bound_at 与历史 identity ID 丢失
  4. 为图省事,把不同 provider 的唯一键都塞成一个模糊 subject 字段而不保留微信/手机号的显式结构

14. 本任务完成定义

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

  1. provider 范围、字段与唯一约束已明确到可以直接编码。
  2. 手机号 identity 与微信 identity 的写入规则都已固定。
  3. 已和 user_account 明确好一致性关系。
  4. 微信 identity 迁移与手机号换绑的更新策略已经明确。

15. 依据文件

  1. server-node/src/routes/authRoutes.ts
  2. server-node/src/auth/authService.ts
  3. server-node/src/repositories/authIdentityRepository.ts
  4. server-node/src/repositories/userRepository.ts
  5. server-node/src/db/migrations.ts
  6. docs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.md
  7. docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md
  8. docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md