# 数据库设计 Review Checklist

> **最后更新**: 2026-05-18
> **定位**: schema review / migration review / audit 的执行清单。AI 和人工都按本清单逐项检查。
> **关联**:
> - [04-database-architecture.md](./04-database-architecture.md) — multi-schema、命名、标准字段（本清单复用）
> - [16-data-layering-and-metadata-policy.md](./16-data-layering-and-metadata-policy.md) — L1/L2/L3 分层与不立 L4（本清单复用）
> - 触发场景：新 schema PR / 大批 migration / 模块 audit / 上线前数据架构 sanity check

---

## 使用方法

1. **新 schema 写完** → 作者自查（22 条全过）
2. **PR review** → reviewer 按本清单点检，关键违反需 block
3. **模块 audit** → 用 `Plan` agent 跑全量 audit，按本清单分类 finding（参考 §附录 C）
4. **migration review** → 配合 §4 重点看"迁移安全"

每条原则给出：**判据** / **违反信号** / **修复模式** / **严重度等级**。

---

## 一、项目特有原则（11 条，来自 standard 04 + 16）

### 原则 1 · Multi-Schema 业务域隔离 🔴

**判据**：按业务域分 Postgres schema（`robot_manager` / `platform_master` / `platform_iam` 等 18+ 个）。

**违反信号**：
- 新业务表落在已有的"通用" schema 下，没建独立 schema
- 多个不相关业务实体混在一个 schema 里

**修复**：按业务域拆 schema，独立 `@@schema("xxx")`，新建 `prisma/schema/xxx.prisma`。

**例外**：跨模块复用的主数据放 `platform_master`（L1）。

---

### 原则 2 · 跨 schema **不**建 Prisma `@relation` 🔴 **铁律**

**判据**：模块间数据通过 UUID 关联 + service 层 enrich 查询，不在 DB 层建 FK constraint。

**违反信号**：
```prisma
// ❌ 在 platform_iam.User 上加跨 schema relation
model User {
  ticketsCreated  Ticket[] @relation("TicketCreator")  // ← Ticket 在 platform_tickets schema
}
```

**修复**：
```prisma
// ✅ Ticket 端只存 UUID
model Ticket {
  createdById  String  @db.Uuid  // FK -> platform_iam.users.id
  // 不加 @relation
}
```

Service 层用 batchEnrich pattern：列表拉完 → 提取 userIds → 单次 `userService.batchByIds(ids)` → 注入。

**为什么**：跨 schema FK 让任何模块的独立部署/迁移/drop 都会被卡。

---

### 原则 3 · 标准字段 5 项强制 🔴

**判据**：所有新建业务表必须有：

| 字段 | 类型 | 作用 |
|---|---|---|
| `id` | `@db.Uuid @id @default(uuid())` | 主键 |
| `createdAt` | `@db.Timestamptz(3)` | 创建时间 |
| `updatedAt` | `@db.Timestamptz(3) @updatedAt` | 更新时间 |
| `createdById` | `@db.Uuid` | **DataScope SELF** |
| `organizationId` | `@db.Uuid` | **DataScope ORGANIZATION**；即便单租户也建 |

按需可选：`departmentId` / `regionId` / `deletedAt` / `version`

**违反信号**：
- 缺 `organizationId`（最常见的灾难）
- 字典/审计表用 `organizationId String? @default("default")` —— 类型对，但默认值错（应为 NULL）

**修复**：建表时一次性加齐；存量表分两阶段迁移（加列 + 回填）。

---

### 原则 4 · 命名禁止同义词漂移 🔴

**判据**：

| ❌ 同义词 | ✅ 标准 |
|---|---|
| `creatorId` / `authorId` / `ownerId` / `submittedBy` / `userId` | `createdById` |
| `orgId` / `tenantId` / `tenant_id` | `organizationId` |
| `created_by`（缺 `Id` 后缀） | `created_by_id`（Prisma `createdById`） |
| ID 列 `String` 无 `@db.Uuid` | 统一 `@db.Uuid` |
| 时间戳 `DateTime` 无精度 | 统一 `@db.Timestamptz(3)` |

**为什么**：DataScope 装饰器默认按标准字段名映射，命中即零配置；不命中要写 `fields` override 满天飞。

**修复**：rename 列 + Prisma 字段，走"加新列→回填→删旧列"两阶段迁移（不在单次迁移删列）。

---

### 原则 5 · JSONB 仅用于非结构化 / 半结构化 🟠

**判据**：
- ✅ JSONB 合法用法：表单字段配置 / 事件 payload / 动态扩展属性 / 配置 schema
- ❌ JSONB 反例：结构化业务字段全塞一个 `metadata Json` catch-all

**违反信号**：
- `RobotUnit.metadata Json` 当 60+ 业务字段存储 → 应该独立列
- 一个 JSONB 字段被多个业务规则读写 → 拆出来

**修复**：结构化字段 → 独立列；业务规则 schema 化。

**例外**（standard 16 §4.6）：表单引擎 / 工作流引擎本身就是元数据驱动产品（如 `platform_form.FormVersion.schema`），JSONB 是核心载体，加注释说明"L4 例外案例"。

---

### 原则 6 · L1/L2/L3 三层架构 + 单向依赖 🟠

**判据**：
```
L3  Transactional   业务流程产生（PO / SO / Event）
L2  Domain Master   模块核心实体（Model / Sku / RobotUnit）
L1  Platform Master 跨模块复用（Customer / Supplier / Currency）
```

**单向依赖**：L1 不依赖 L2/L3；L2 只依赖 L1；L3 依赖 L1+L2。反向引用必须走事件流。

**违反信号**：
- L1 主数据表引用 L3 业务表的 FK
- 模块 A 直接 `import` 模块 B 的 Prisma client 写 B 的 L2 实体

**修复**：剥离循环依赖；跨模块写改事件流（见原则 8）。

---

### 原则 7 · L2 核心实体"状态 vs 不变属性"分离 🟠

**判据**：L2 表只放**不变属性**；状态走 Event Sourcing + CQRS Snapshot。

**违反信号**：
```prisma
// ❌ 状态挂在 L2 实体上
model RobotUnit {
  currentStatus     RobotStatus    // ← 会变，污染所有 join
  currentLocationId String
}
```

**修复**：
```prisma
// ✅ RobotUnit 只放不变属性
model RobotUnit {
  ffsn   String
  skuId  String
  // ...
}
// 状态走事件流 + Snapshot
model RobotLifecycleEvent { /* 不可变事件源 */ }
model RobotUnitSnapshot   { /* CQRS read model */ }
```

**为什么**：外部模块 join 时不会因为状态变更而污染查询；完整历史可重放可审计。

---

### 原则 8 · 跨模块写 L2 走事件流，不直写 🟠

**判据**：模块 A 想写模块 B 的 L2 实体 → 必须 emit 事件，B 自己消费。

**违反信号**：
```typescript
// ❌ salesModule 直接写 robotModule 的实体
await salesModule.prisma.robotUnit.update({...});
```

**修复**：
```typescript
// ✅ emit + subscribe
salesModule.emitEvent({ type: 'sales_reserved', robotUnitId, salesOrderId });
robotManagerModule.subscribe('sales_reserved', async (e) => {
  await prisma.robotUnitSnapshot.update({...});
});
```

---

### 原则 9 · 默认不立 L4 元数据驱动层 🟠 **项目特有决策**

**判据**：业务规则在代码 + 类型系统，**不**存表里。

**违反信号**：
- `FieldDefinition` 表存字段定义（业务字段配置在 schema 不在数据）
- `StatusDefinition` 表存状态枚举（用 Prisma enum）
- `GuardDefinition` 表存校验规则（用代码 + 类型）

**修复**：删元数据表，把规则上移到代码。

**例外**：表单引擎 / 工作流引擎本身就是 metadata-driven 产品（详见 standard 16 §4.6）。

---

### 原则 10 · 一次提交一个迁移文件 🟡

**判据**：每次 git commit 最多 1 个 `prisma/migrations/` 新增目录。多 schema 变更合并成 1 个迁移。

**违反信号**：单 commit 含 2+ migration 目录。

**修复**：merge migration files；或拆 commit。

---

### 原则 11 · 不在单次迁移删列 / 改类型 🟡

**判据**：上线零停机要求。删列/改类型必须分多次迁移：**加新列 → 迁移数据 → 删旧列**。

**违反信号**：单 migration 含 `DROP COLUMN` + 已有数据。

**修复**：分阶段迁移，中间版本兼容新旧两列。

---

## 二、业界通用原则（11 条）

### 原则 12 · 第三范式（3NF） 🟠

**判据**：
- 1NF：原子值（不嵌套）
- 2NF：完全函数依赖（非键字段不依赖部分主键）
- 3NF：消除传递依赖（非键字段之间不互相决定）
- BCNF：每个 FD 的左部都是超键

**违反信号**：
- 派生值物化（`grossMargin = revenue - cost` 还存一份）
- 财务字段挂业务表（`DeliveryFulfillment.cost`）
- 同一信息在多个表冗余存储

**修复**：派生值算不存；多表冗余拆出独立 ledger / view。

**反范式时机**：仅在性能瓶颈 + 测过基线后做，加注释说明。

---

### 原则 13 · 单一事实源（Single Source of Truth） 🟠

**判据**：一个字段一个权威位置。

**违反信号**：
- snapshot 派生字段 + metadata 手填镜像（双写漂移坑）
- 同一业务事实在 A 表和 B 表各存一份

**修复**：派生用 view / 物化视图 / 应用层算，不冗余存；或明确"哪个是权威"（master），另一个是 cache（命名带 `cached_`）。

---

### 原则 14 · 业务约束落 DB 🟠

**判据**：声明式约束写 schema，不靠应用层 enforce：
- `NOT NULL` / `@unique` / `@@unique`
- `CHECK` 约束（raw SQL migration）
- FK constraint（同 schema 内）

**违反信号**：
- `rating Int?` 但业务规则是 1-5（应 `CHECK (rating BETWEEN 1 AND 5)`）
- `quantity Int` 但业务规则 >0（应 `CHECK (quantity > 0)`）
- 同 schema 内的关联表只存 UUID 不建 FK relation（应建）

**修复**：raw SQL migration 加 CHECK constraint；同 schema 关联补 `@relation`。

---

### 原则 15 · 类型精确 🟠

**判据**：

| 业务概念 | ✅ 类型 | ❌ 反例 |
|---|---|---|
| 金额 | `Decimal(14, 2)` 或 `Decimal(14, 4)` | `Float` / `Double`（浮点丢精度） |
| ID | UUID v4 / v7 (`@db.Uuid`) | 自增 Int / String 无类型 |
| 时间戳 | `@db.Timestamptz(3)` 带时区 | `DateTime` naive / `BIGINT` 时间戳 |
| 枚举 | Prisma enum 编译时 | `String` + 应用层 CHECK |
| 货币代码 | `@db.VarChar(3)` ISO 4217 | `String` 无长度 |
| 地理坐标 | `Float`（业界惯例）/ PostGIS | OK，但加注释 |

**违反信号**：业务字段类型错配（如 `cost Float`）。

---

### 原则 16 · 索引设计 🟡

**判据**：
- 所有 FK 列必加索引（join 性能）
- 复合索引列序：**等值 → 范围 → 排序**
- 高频查询 + low cardinality 字段考虑 partial index（`WHERE deleted_at IS NULL`）
- 索引数量 ≤ 5/表（写性能权衡，超过要说明）

**违反信号**：
- FK 字段无索引（join 全表扫描）
- 单表 10+ 索引（写放大严重）
- `@@index` 列序错（先 timestamp 后 status）

**修复**：合并冗余索引；删低使用率索引（看 `pg_stat_user_indexes`）。

---

### 原则 17 · 软删除一致 🟡

**判据**：
- 业务实体统一用 `deletedAt @db.Timestamptz(3)`
- 所有读路径 `WHERE deletedAt IS NULL`
- 唯一约束 + 软删除并存：用 partial unique index `WHERE deleted_at IS NULL`

**违反信号**：
- `isActive Boolean` 跟 `deletedAt` 混用
- 软删除一个 code 后无法复用（因为 unique constraint 还在）

**修复**：
```sql
-- 不是 @unique
CREATE UNIQUE INDEX customers_code_unique
  ON customers (code) WHERE deleted_at IS NULL;
```

**注意**：`enabled Boolean`（启用/停用）跟 `deletedAt`（删除）语义不同，可并存。

**partial unique 跟 Prisma `@unique` 共存的维护约定**：

Prisma PSL 不支持 partial unique（[issue #3387](https://github.com/prisma/prisma/issues/3387) 开 5+ 年未实现）。
项目选择：DB 层用 raw SQL 建 partial unique（命名 `<table>_<col>_active_key`），Prisma schema 保留普通 `@unique` 装饰器（让 client 类型层 `findUnique`/`upsert` 继续可用）。

**副作用**：`prisma migrate dev` 会主动检测到 drift 并提议「DROP partial unique → CREATE 普通 unique」"修复"回 schema 状态——**这正是要拦的反模式**（误 accept 后软删合规悄悄失效，且回不去）。

**守门**：CI + pre-commit hook（`scripts/ops/check-partial-unique-guard.sh`）扫 staged migration 文件，发现 `DROP INDEX *_active_key` 直接 fail。

**改 partial unique 的正确流程**：
1. `prisma migrate diff --script` 看 plan
2. 手编 migration.sql，去掉自动生成的 "DROP INDEX *_active_key" 段
3. 真要改 partial unique 语义（业务决策）→ `--mode warn` 跳过 guard + commit message 说明

---

### 原则 18 · 乐观锁单一 version 🟡

**判据**：一个聚合根**一个** `version` 字段。CQRS write/read 分离时可有双 version，但**必须注释明示哪个 API 用哪个**。

**违反信号**：
- 同一聚合根多个 version 字段无注释（开发者会传错）
- 资金敏感表（SalesOrder / PaymentRecord）没有 version

**修复**：
- 双 version 加 `///` 注释说明
- 资金敏感表补 `version Int @default(0)`

**参考**：[`.learnings/2026-05-17-robot-unit-vs-snapshot-version.md`](../../.learnings/2026-05-17-robot-unit-vs-snapshot-version.md)

---

### 原则 19 · 审计追溯（事件源） 🟡

**判据**：金融 / 合规场景必须 Event Sourcing：
- 写不可变事件流（append-only）
- 派生 snapshot（CQRS read model）
- 历史可重放、可审计、可时间旅行

**违反信号**：
- 资金 / 合规字段直接 update 主表，无事件流
- 状态变化没记录"谁在何时改了什么"

**修复**：建 Event 表 + Snapshot；改用事件投影模式。

---

### 原则 20 · 跨服务边界不建 FK 🔴（跟原则 2 一致）

**判据**：微服务 / 业务域边界不建 DB-level FK constraint，应用层 enrich。

**违反信号**：见原则 2。

---

### 原则 21 · 不可逆操作有 deletedAt / archivedAt 标记 🟡

**判据**：删除 / 归档保留历史，业务上"软删除"语义清楚于"硬删除"。

**违反信号**：业务表直接 hard delete，无追溯。

**修复**：加 `deletedAt`；hard delete 改 soft delete。

---

### 原则 22 · 表名 / 字段名表达业务含义 🟢

**判据**：
- ✅ `purchase_order_lines` / `placeholder_sn_orig` / `gross_margin`
- ❌ `data1` / `info` / `temp` / `misc`

**违反信号**：5W1H 抽象命名（`who/what/when/where/why/how` 业务含义不明）。

**修复**：rename + 注释。

---

## 三、Review 优先级与触发场景

### 何时跑全量 audit

- 模块上线前 / quarter audit
- standard 04 或 16 更新后回查
- 引入新模块（先 audit reference 模块如 `robot_manager`）

### 何时跑增量 review（只看本次改动）

- PR 含 schema 变更
- 新 migration 文件
- 新建业务表

### 严重度 → 行动

| 严重度 | 含义 | 行动 |
|---|---|---|
| 🔴 严重 | 违反铁律 / 数据完整性危险 | **block PR**，必须修后才能 merge |
| 🟠 高 | 违反核心原则 / 影响性能 | 短期内修（≤ 2 周） |
| 🟡 中 | 违反业界通用 / 不影响功能 | 后续 PR 处理（≤ 1 季度） |
| 🟢 低 | 建议性 / 注释加强 | 顺手补 |

---

## 四、迁移安全 checklist（独立小清单）

migration PR 额外检查：

- [ ] 单 commit 最多 1 个 migration 文件
- [ ] 不在单次迁移删列 / 改类型（分阶段）
- [ ] 加新列 → 默认值或 NULL（避免锁表）
- [ ] 大表 backfill 用 batch（避免长事务）
- [ ] 加索引用 `CREATE INDEX CONCURRENTLY`（10M+ 行表）
- [ ] DROP / RENAME 操作前检查 prod env 现状（standard 04 §5a 生产只读铁律）
- [ ] migration 名清晰：`<verb>_<noun>`（如 `add_organization_id_to_tickets`）

---

## 附录

### A. 项目标杆 schema

**`robot_manager.prisma` 是 v3 重构后的 reference 实现**。新写 schema 前先读它的：
- 跨 schema FK 清单（顶部注释 §跨 schema FK 清单）
- L2 状态分离（`RobotUnit` 不变属性 + `RobotLifecycleEvent` + `RobotUnitSnapshot`）
- 标准字段齐全（每个业务表 5 项）
- 双 version trade-off 注释（`RobotUnit.version` vs `Snapshot.version`）

### B. 已知历史债（standard 04/16 立稿前的模块）

这些模块需要单独 P0 修复 PR：
- `corp_request` / `platform_devtracker` / `platform_tickets` / `platform_feedback` / `platform_audit` / `platform_form`：缺 `organizationId` / 命名漂移
- `platform_iam.User`：70+ 跨 schema `@relation` 违反原则 2
- `platform_ai` / `platform_knowledge`：Float 存金额

### C. Audit 模板

用 `Plan` agent 跑全量 audit：

```
Prompt: 在 <repo> 对所有 prisma schema 文件做 audit，按 docs/standards/20-database-review-checklist.md 的 22 条原则检查。输出格式：

🔴 严重 / 🟠 高 / 🟡 中 / 🟢 低 分类
每条 finding: [原则 N] 严重度 · 文件 行号 · 违反描述 · 影响 · 修复 · 工作量 S/M/L
```

---

## 相关文档

- [04-database-architecture.md](./04-database-architecture.md) — Multi-Schema / 命名 / 标准字段（原则 1-3）
- [16-data-layering-and-metadata-policy.md](./16-data-layering-and-metadata-policy.md) — 三层架构 / 不立 L4（原则 6-9）
- [`.agents/skills/database-main/references/database-standards.md`](../../.agents/skills/database-main/references/database-standards.md) — 详细 standards
- [`.learnings/2026-05-17-robot-unit-vs-snapshot-version.md`](../../.learnings/2026-05-17-robot-unit-vs-snapshot-version.md) — 双 version 坑
