13 KiB
13 KiB
auth_identity 表设计
日期:2026-04-21
1. 文档目的
这份文档用于完成 M2 的第二条任务:设计 auth_identity。
目标是把“账号主实体”和“外部登录身份”彻底拆清楚,让后续编码时不会再出现:
- 微信身份字段继续堆进
user_account - 手机号既是账号字段又没有对应身份行
- 微信待绑定壳账号并入正式手机号账号时,不知道到底迁哪些记录
2. 当前基线
当前 Node 后端已经有一张 auth_identities 表,但范围还不完整:
- 当前只真实存了
wechat身份 - 手机号登录仍主要依赖
users.phone_number /api/auth/me的wechatBound依赖查询auth_identities- 微信登录后的“已有正式账号 / 首次待绑定账号 / 绑定后归并”三种分支,都已经在
authService.ts中真实发生
当前 Node auth_identities 字段基线:
iduser_idproviderprovider_uidprovider_unioniddisplay_nameavatar_urlis_verifiedmeta_jsoncreated_atupdated_at
这说明:
auth_identity在现有系统里已经不是新概念,而是微信登录链路的既有事实。- Rust 重写时不能再把它降级成“可有可无的附属表”。
- 但它也还没有扩展到手机号身份层,因此本轮需要把“手机号是否入表、如何入表”钉死。
3. 与 user_account 的边界
auth_identity 与 user_account 的固定边界如下。
3.1 user_account 负责
- 账号主键与主状态
password_hashtoken_versiondisplay_nameprimary_phone_e164login_provider
3.2 auth_identity 负责
- 一个账号绑定了哪些外部身份
- 每个 provider 的唯一主体键
- provider 级验证状态
- provider 资料快照
- provider 绑定时间与最近一次使用时间
- 身份迁移时的归属变更
3.3 不允许的漂移
- 不把
password_hash放进auth_identity - 不把 refresh token hash 放进
auth_identity - 不让
auth_identity取代user_account成为账号主表 - 不再只给微信建 identity 行,而让手机号继续做“无 identity 的特殊分支”
4. provider 范围
当前阶段 auth_identity.provider 固定只支持:
phonewechat
说明:
password当前仍留在user_account.password_hash,不进入auth_identity。- 后续若补 OIDC 或更多社交登录,再在新任务里追加 provider,不在本轮预支。
5. 字段设计
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
identity_id |
String |
是 | 主键,建议沿用 authi_* 前缀。 |
user_id |
String |
是 | 归属账号 ID,外键指向 user_account.user_id。 |
provider |
String |
是 | 枚举固定为 phone、wechat。 |
provider_uid |
String |
是 | provider 主主体键;phone 时固定等于 phone_e164,wechat 时固定存 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 最近更新时间。 |
补充说明:
- 当前阶段统一继续使用 UTC RFC3339 字符串时间,先与现有 Node 表与调试习惯对齐。
meta_json当前建议直接存 JSON 字符串,避免后续第一版 schema 先引入过宽的嵌套类型。is_primary当前虽然大多是冗余字段,但必须先保留,为后续多 identity/同 provider 扩展留稳定位置。
6. provider 专属约束
6.1 phone provider
固定规则:
provider_uid = phone_e164provider_unionid = nullphone_e164 != nullis_verified = trueis_primary = true
当前阶段约束:
- 一个账号最多只允许一条活跃
phoneidentity - 它必须与
user_account.primary_phone_e164一致
6.2 wechat provider
固定规则:
provider_uid必填provider_unionid可空,但若存在则优先作为稳定唯一键phone_e164 = nulldisplay_name、avatar_url、meta_json可按登录回调刷新is_verified = true
当前阶段约束:
- 一个账号最多只允许一条活跃
wechatidentity - 微信首次登录但未绑定手机号时,也必须先创建这条 identity 行
7. 唯一约束与索引
7.1 必须具备的唯一约束
identity_id主键唯一(provider, provider_uid)全局唯一(provider, provider_unionid)在provider_unionid != null时全局唯一(provider, phone_e164)在provider = phone且phone_e164 != null时全局唯一
7.2 必须具备的查询索引
(user_id, provider)作用:支撑/api/auth/me、账号中心与 provider 检查provider_uid作用:支撑微信回调身份查找provider_unionid作用:支撑微信跨应用稳定身份查找phone_e164作用:支撑手机号登录、手机号换绑与手机号冲突检查
8. 与 user_account 的一致性规则
8.1 手机号 identity 与账号主手机号必须一致
只要 user_account.primary_phone_e164 非空,就必须满足:
- 同一
user_id下存在一条provider = phone的 identity - 该行
phone_e164 = user_account.primary_phone_e164 - 该行
provider_uid = user_account.primary_phone_e164 - 该行
is_primary = true - 该行
is_verified = true
8.2 login_provider 不等于 identity.provider
必须明确:
user_account.login_provider表示账号主归属方式auth_identity.provider表示账号下挂了哪一种外部身份
因此一个手机号正式账号在绑定微信后,完全允许出现:
user_account.login_provider = phone- 同时存在
provider = phone与provider = wechat两条 identity
8.3 待绑定微信账号允许没有手机号 identity
若:
user_account.account_status = pending_bind_phoneuser_account.primary_phone_e164 = null
则允许:
- 当前账号只有一条
wechatidentity - 不存在
phoneidentity
9. 写入场景设计
9.1 POST /api/auth/entry
当前阶段不写 auth_identity。
原因:
- 密码登录仍由
user_account.password_hash承担 - 本轮不引入
passwordprovider identity - 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或百梦号作为登录身份
- 密码登录不创建账号,新账号只由手机号验证码登录创建
9.2 POST /api/auth/phone/login
写入规则:
- 若账号不存在,则先创建
user_account - 同时创建一条
provider = phoneidentity - 若账号已存在,则校验手机号一致并更新
last_login_at bound_at只在首次绑定时写入
9.3 GET /api/auth/wechat/callback
写入规则:
- 先按
provider_unionid查;没有再按provider_uid查 - 若 identity 已存在,则更新资料快照并写
last_login_at - 若本次是按
provider_unionid命中,但provider_uid已变化,则必须把新的provider_uid一并回写为最新主映射 - 若 identity 不存在,则创建一条新的
wechatidentity - 若是首次微信登录,还要同步创建
pending_bind_phone的user_account
9.4 POST /api/auth/wechat/bind-phone
分两种情况:
- 手机号未被其他账号使用
- 当前账号写入
phoneidentity - 当前
user_account转active
- 当前账号写入
- 手机号已绑定已有正式账号
- 不新建第二条
phoneidentity - 把当前壳账号下的
wechatidentity 迁移到目标正式账号 - 当前壳账号标记为并入
- 不新建第二条
9.5 POST /api/auth/phone/change
写入规则:
- 更新当前账号唯一一条
phoneidentity 的phone_e164 - 同步更新
provider_uid - 更新
bound_at不变 - 更新
last_login_at不变 updated_at = 当前时间
说明:
- 当前阶段手机号换绑不保留“旧手机号 identity 历史行”。
- 历史审计交给
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_at、created_at |
updated_at |
updated_at |
last_login_at 初次迁移规则:
- 先回填为
updated_at
10.2 从当前 Node users.phone_number 反向补 phone identity
凡是当前 users.phone_number 非空的账号,都要补一条 phone identity:
provider = phoneprovider_uid = phone_numberphone_e164 = phone_numberdisplay_name = nullavatar_url = nullis_verified = phone_verified_at != nullis_primary = truebound_at = phone_verified_at ?? created_atlast_login_at = updated_at
10.3 初次迁移的兼容结论
迁移完成后,必须满足:
- 每个带手机号的账号都有一条
phoneidentity - 每个微信已绑定账号都有一条
wechatidentity /api/auth/me的wechatBound不再依赖旧 Node 仓储逻辑,而能直接从auth_identity派生
11. 读模型约束
auth_identity 至少要支撑以下读需求:
11.1 /api/auth/me
需要派生:
wechatBoundbindingStatusphoneNumberMasked
其中:
phoneNumberMasked最终以user_account.primary_phone_e164为准wechatBound以是否存在provider = wechatidentity 为准
11.2 /api/auth/wechat/start 与 /api/auth/wechat/callback
需要支撑:
- 微信 callback 身份查找
- 首次登录 identity 创建
- 绑定后 identity 迁移
11.3 账号安全页
未来至少会依赖:
- 当前账号绑定了哪些 provider
- 是否完成手机号正式归属
- 微信头像与昵称展示
12. reducer / service 落地约束
12.1 module-auth reducer 层
必须至少具备这些命令入口:
create_phone_identityupsert_phone_identity_after_phone_changecreate_wechat_identitytouch_identity_last_loginrefresh_wechat_identity_profilemove_identity_to_user
12.2 Axum 应用层
固定负责:
- 从微信 provider 拿到
uid / unionid / 昵称 / 头像 - 决定微信 callback 应该命中已有 identity 还是创建新 identity
- 在手机号绑定或换绑成功后,同步调用
user_account与auth_identity相关 reducer
13. 不允许的设计漂移
后续实现时禁止出现以下情况:
- 继续让手机号登录只写
user_account.primary_phone_e164,不写auth_identity - 把微信
unionid丢到meta_json里,而不是单独做唯一约束字段 - 微信壳账号并入正式账号时直接删除
wechatidentity 再重建,导致bound_at与历史 identity ID 丢失 - 为图省事,把不同 provider 的唯一键都塞成一个模糊
subject字段而不保留微信/手机号的显式结构
14. 本任务完成定义
当以下条件满足时,设计 auth_identity 视为完成:
- provider 范围、字段与唯一约束已明确到可以直接编码。
- 手机号 identity 与微信 identity 的写入规则都已固定。
- 已和
user_account明确好一致性关系。 - 微信 identity 迁移与手机号换绑的更新策略已经明确。
15. 依据文件
server-node/src/routes/authRoutes.tsserver-node/src/auth/authService.tsserver-node/src/repositories/authIdentityRepository.tsserver-node/src/repositories/userRepository.tsserver-node/src/db/migrations.tsdocs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.mddocs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.mddocs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md