# 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` 枚举中添加即可。