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