docs: design auth identity table
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md):`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。
|
||||
- [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` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
|
||||
- [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
# `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/me` 的 `wechatBound` 依赖查询 `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_identity` 与 `user_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` | 是 | 枚举固定为 `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 最近更新时间。 |
|
||||
|
||||
补充说明:
|
||||
|
||||
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_name`、`avatar_url`、`meta_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 = phone` 且 `phone_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 = phone` 与 `provider = 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
|
||||
|
||||
### 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. 若 identity 不存在,则创建一条新的 `wechat` identity
|
||||
4. 若是首次微信登录,还要同步创建 `pending_bind_phone` 的 `user_account`
|
||||
|
||||
### 9.4 `POST /api/auth/wechat/bind-phone`
|
||||
|
||||
分两种情况:
|
||||
|
||||
1. 手机号未被其他账号使用
|
||||
- 当前账号写入 `phone` identity
|
||||
- 当前 `user_account` 转 `active`
|
||||
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_at`、`created_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/me` 的 `wechatBound` 不再依赖旧 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_account` 与 `auth_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`
|
||||
@@ -310,6 +310,10 @@ server-rs/
|
||||
|
||||
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
|
||||
|
||||
`auth_identity` 的 provider 约束、唯一键与手机号/微信身份写入规则,见:
|
||||
|
||||
- [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)
|
||||
|
||||
### B. 运行时主状态表
|
||||
|
||||
- `runtime_snapshot`
|
||||
|
||||
Reference in New Issue
Block a user