# 数据对外开放接口层 - 数据模型

> **module**: db-external-access
> **doc_type**: DataModel
> **status**: Active
> **owner**: FFOA Team
> **upstream_docs**: 01-prd.md
> **last_verified**: 2026-04-24

---

## 概览

本模块新增 2 张表，位于独立 schema 文件 `backend/prisma/schema/platform_data_open.prisma`：

| 表名 | 说明 |
|------|------|
| `DataApiKey` | API Key 管理：存储 Key 的元数据与哈希 |
| `DataAccessLog` | 访问审计日志：每次调用自动写入，不可删除 |

不新增业务表，数据读取直接查询现有业务 schema（只读）。

---

## DataApiKey 表

```prisma
// ⏳ 待实现 - platform_data_open.prisma

model DataApiKey {
  id          String    @id @default(uuid())
  name        String                        // 可读名称，如 "报表系统-生产"
  consumer    String                        // 接入方标识，如 "bi-platform"
  keyHash     String    @unique             // SHA-256(明文key)，不存明文
  keyPrefix   String                        // 明文前 8 位，用于列表展示识别
  dataRoles   String[]                      // 数据角色 code 数组，如 ["org-readonly"]
  rateLimit   Int       @default(100)       // req/min
  isActive    Boolean   @default(true)
  lastUsedAt  DateTime?
  expiresAt   DateTime?                     // null = 永不过期
  createdBy   String                        // 创建者 userId
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  logs        DataAccessLog[]

  @@map("data_api_keys")
}
```

**字段说明**：

| 字段 | 约束 | 说明 |
|------|------|------|
| `keyHash` | UNIQUE, NOT NULL | SHA-256 哈希，服务端每次验证时重新计算比对 |
| `keyPrefix` | NOT NULL | 明文 key 的前 8 位，仅用于管理列表中让管理员识别是哪个 Key |
| `dataRoles` | `String[]` | PostgreSQL text 数组，枚举值见 PRD 授权章节 |
| `rateLimit` | DEFAULT 100 | 每分钟请求上限，0 = 不限 |
| `expiresAt` | NULLABLE | 为 null 时永不过期；到期后请求返回 401 |
| `lastUsedAt` | NULLABLE | 每次成功鉴权后异步更新（不阻塞请求链路） |

---

## DataAccessLog 表

```prisma
// ⏳ 待实现 - platform_data_open.prisma

model DataAccessLog {
  id           String   @id @default(uuid())
  apiKeyId     String
  apiKey       DataApiKey @relation(fields: [apiKeyId], references: [id])

  method       String                        // GET / POST 等
  path         String                        // 请求路径，如 /data/organizations
  query        String?                       // 查询参数（JSON 字符串，脱敏后）
  statusCode   Int                           // HTTP 状态码
  duration     Int                           // 响应耗时 ms
  resultCount  Int       @default(0)         // 返回数据条数
  ip           String?                       // 来源 IP

  createdAt    DateTime  @default(now())

  @@index([apiKeyId])
  @@index([createdAt])
  @@map("data_access_logs")
}
```

**字段说明**：

| 字段 | 说明 |
|------|------|
| `query` | 存储 JSON 字符串，记录分页、筛选参数；如含敏感参数（手机号等）则在写入前脱敏 |
| `resultCount` | 实际返回的数据条数，用于审计"拿了多少数据" |
| `duration` | 服务端总耗时（从接收请求到发出响应），单位 ms |

**保留策略**：
- 日志默认保留 **180 天**（⏳ 待实现：定时清理任务，按 `createdAt` 删除过期记录）
- 不支持手动删除单条日志，只支持批量过期清理

---

## 实体关系

```
DataApiKey (1)
    └──── DataAccessLog (N)   apiKeyId FK
```

---

## 迁移文件命名（待实现时遵守）

- 文件名：`{timestamp}_add_data_open_module`
- 只包含 `DataApiKey` 和 `DataAccessLog` 两张表的 CREATE TABLE
- 不修改任何现有业务表

---

## 索引策略

| 表 | 索引 | 原因 |
|------|------|------|
| `DataApiKey` | `keyHash` UNIQUE | 每次请求鉴权时按 hash 查找 |
| `DataAccessLog` | `apiKeyId` | 按 Key 查询该 Key 的访问历史 |
| `DataAccessLog` | `createdAt` | 时间范围查询 + 定时清理 |
