154 lines
4.0 KiB
Markdown
Raw Normal View History

# iOS 登录流程 —— 数据库设计
---
## 一、users 表
用户主表,存储用户基础信息。
```prisma
model User {
id String @id @default(cuid())
email String?
nickname String?
avatarUrl String?
role UserRole @default(USER)
status UserStatus @default(ACTIVE)
onboardingCompleted Boolean @default(false)
authAccounts AuthAccount[]
refreshTokens RefreshToken[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum UserRole {
USER
ADMIN
SUPER_ADMIN
}
enum UserStatus {
ACTIVE
DISABLED
DELETED
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String (cuid) | 主键 |
| `email` | String? | 邮箱可选Apple 登录首次可能提供) |
| `nickname` | String? | 昵称 |
| `avatarUrl` | String? | 头像 URL |
| `role` | UserRole | 角色,默认 USER |
| `status` | UserStatus | 状态,默认 ACTIVE |
| `onboardingCompleted` | Boolean | 是否完成引导,默认 false |
| `authAccounts` | 关联 | 一对多关联 auth_accounts |
| `refreshTokens` | 关联 | 一对多关联 refresh_tokens |
---
## 二、auth_accounts 表
记录用户通过什么方式provider登录支持一个用户绑定多个登录方式。
```prisma
model AuthAccount {
id String @id @default(cuid())
userId String
provider AuthProvider
providerUserId String
email String?
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, providerUserId])
@@index([userId])
}
enum AuthProvider {
DEV
APPLE
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String (cuid) | 主键 |
| `userId` | String | 关联 users 表 |
| `provider` | AuthProvider | 登录提供商DEV / APPLE |
| `providerUserId` | String | 提供商侧的用户唯一 ID |
| `email` | String? | 提供商侧邮箱 |
| `@@unique([provider, providerUserId])` | 约束 | 同一个提供商的用户唯一 |
**查找逻辑**
```
Apple 登录时:
provider = APPLE
providerUserId = identityToken 里校验出来的 sub
→ 如果不存在,创建 User + AuthAccount
→ 如果存在,直接找到对应 User
```
---
## 三、refresh_tokens 表
**重要refreshToken 不要明文存数据库,只存 hash。**
```prisma
model RefreshToken {
id String @id @default(cuid())
userId String
tokenHash String
expiresAt DateTime
revokedAt DateTime?
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String (cuid) | 主键,同时写入 JWT payload 作为 `tokenId` |
| `userId` | String | 关联 users 表 |
| `tokenHash` | String | refreshToken 的 hash 值SHA-256 |
| `expiresAt` | DateTime | 过期时间 |
| `revokedAt` | DateTime? | 撤销时间(登出时设置) |
**刷新时的校验链**
```
1. 解析 refreshToken JWT拿到 userId + tokenId
2. 查 refresh_tokens 表,找到对应记录
3. 对比 tokenHash
4. 确认 revokedAt 为 null未撤销
5. 确认 expiresAt 未过期
6. 签发新的 accessToken可选轮换新的 refreshToken
```
**登出时**:将对应记录的 `revokedAt` 设为当前时间。
---
## 四、ER 关系总结
```
User (1) ──── (N) AuthAccount 一个用户可有多种登录方式
User (1) ──── (N) RefreshToken 一个用户可有多个活跃 refreshToken多设备
```
- 用户与登录方式是解耦的:用户是一个独立实体,通过 `auth_accounts` 关联到具体的第三方身份。
- 这种设计天然支持未来扩展更多登录方式(如 Google、微信等只需在 `AuthProvider` 枚举中添加即可。