# AI 审批编排 — 数据模型

> **版本**: v1.0-draft
> **最后更新**: 2026-05-19
> **维护者**: 后端团队
> **关联**: [01-prd.md](./01-prd.md) §5.1-5.5 / [07-api.md](./07-api.md) / [04-state-machine.md](./04-state-machine.md)

---

## 📋 概述

### Schema 信息

- **Schema 名称**: `corp_approval`（与 approval-engine 共用，避免跨 schema FK）
- **业务域**: AI 审批策略编排 + 推荐日志 + 校准指标
- **核心表数量**: 5 个（ApprovalPolicy / ApprovalPolicyHistory / RiskTierRegistry / AiRecommendationLog / CalibrationMetricsSnapshot）

### 设计原则

- **标准字段强制**：所有表含 `id` / `createdAt` / `updatedAt` / `createdById` / `organizationId`（CLAUDE.md「标准字段」）
- **partial unique 优于联合主键**：业务唯一性约束用 `WHERE is_active=true` 部分索引，允许历史 inactive 记录共存
- **按月分区高频写**：`AiRecommendationLog` 用 PG range partition by month
- **物化优先**：高频读 metric 走 `CalibrationMetricsSnapshot` 表，5 分钟 cron 增量刷新
- **跟 standards/16 关系**：本模块表归 L3 业务交易（事实存储 + 合规审计），不属 L4 元数据驱动（详 01-prd §3）

### 跨模块字段引用

| 字段 | 来源 |
|---|---|
| `form_template_key` | form-engine `FormDefinition.key` |
| `organization_id` | platform_master `Organization.id` |
| `approval_task_id` | approval-engine `ApprovalTask.id` |
| `agent_session_id` | agent `AgentSession.id` |
| `risk_tier_id` | 本模块 `RiskTierRegistry.id` |
| audit 反向关联 | 本模块不直接持 `audit_log_id`；audit-system 通过 `(approval_task_id, AuditAction='AI_RECOMMENDED'/'AI_ADOPTED'/'AI_OVERRIDDEN'/'AI_UNAVAILABLE')` 反向查询关联推荐 |

---

## 🗄️ 表结构设计

### 表 1: `approval_policies` (`ApprovalPolicy`)

Policy-as-Code 主表（详 01-prd §5.1）。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | UUID | ✅ | 主键 |
| `form_template_key` | VARCHAR(64) | ✅ | 业务模板 key（如 `'expense'`） |
| `organization_id` | UUID | ✅ | 组织 ID；`'__GLOBAL__'` UUID 表示全局兜底 |
| `risk_tier_id` | INT | ✅ | FK → `risk_tier_registry.tier_id`（1/2/3） |
| `ai_mode` | ENUM | ✅ | `OFF / RECOMMEND / RECOMMEND_WITH_REVIEW / RECOMMEND_WITH_HARD_GATE / SUGGEST_ONLY`（默认 `OFF` fail-safe） |
| `confidence_threshold` | DECIMAL(3,2) | ✅ | 0.00-1.00；默认 0.70 |
| `monthly_ai_call_budget_usd` | DECIMAL(10,2) | ✅ | 月度预算（USD）；超限自动降级 `ai_mode=OFF` |
| `cross_org_visibility` | ENUM | ✅ | `NONE / HIERARCHICAL / GLOBAL`；默认 `NONE`；控制历史相似 RAG 召回范围 |
| `reviewer_set` | JSONB | ❌ | required reviewer 角色列表；JSON 数组 |
| `effective_from` | TIMESTAMPTZ | ✅ | 生效起始时间 |
| `effective_to` | TIMESTAMPTZ | ❌ | 生效结束时间；NULL = 仍生效 |
| `is_active` | BOOLEAN | ✅ | 默认 `true`；策略变更时旧记录置 `false` |
| `created_by_id` | UUID | ✅ | 标准字段 |
| `created_at` | TIMESTAMPTZ | ✅ | 标准字段 |
| `updated_at` | TIMESTAMPTZ | ✅ | 标准字段 |

**约束**:
- PK = `id`
- **partial unique** `(form_template_key, organization_id) WHERE is_active=true`（保证同 key+org 同一时刻最多 1 条 active）
- FK `risk_tier_id` → `risk_tier_registry.tier_id`
- CHECK `confidence_threshold BETWEEN 0.00 AND 1.00`
- CHECK `monthly_ai_call_budget_usd >= 0`
- CHECK `effective_to IS NULL OR effective_to > effective_from`

**索引**:
- partial unique 上述
- `(form_template_key)`（按模板查询）
- `(organization_id, is_active)`（按组织查询）

**策略变更流程**（事务）:
1. SELECT 旧 active 记录
2. UPDATE 旧记录 SET `is_active=false`, `effective_to=now()`
3. INSERT 新 active 记录
4. INSERT 到 `approval_policy_history`（before/after snapshot）
5. COMMIT

**Prisma schema 片段**:

```prisma
model ApprovalPolicy {
  id                       String   @id @default(uuid()) @db.Uuid
  formTemplateKey          String   @map("form_template_key") @db.VarChar(64)
  organizationId           String   @map("organization_id") @db.Uuid
  riskTierId               Int      @map("risk_tier_id")
  aiMode                   AiMode   @default(OFF) @map("ai_mode")
  confidenceThreshold      Decimal  @default(0.70) @map("confidence_threshold") @db.Decimal(3, 2)
  monthlyAiCallBudgetUsd   Decimal  @map("monthly_ai_call_budget_usd") @db.Decimal(10, 2)
  crossOrgVisibility       CrossOrgVisibility @default(NONE) @map("cross_org_visibility")
  reviewerSet              Json?    @map("reviewer_set")
  effectiveFrom            DateTime @map("effective_from") @db.Timestamptz
  effectiveTo              DateTime? @map("effective_to") @db.Timestamptz
  isActive                 Boolean  @default(true) @map("is_active")
  createdById              String   @map("created_by_id") @db.Uuid
  createdAt                DateTime @default(now()) @map("created_at") @db.Timestamptz
  updatedAt                DateTime @updatedAt @map("updated_at") @db.Timestamptz

  riskTier                 RiskTierRegistry @relation(fields: [riskTierId], references: [tierId])
  history                  ApprovalPolicyHistory[]

  // Prisma 不支持 partial unique with WHERE clause；
  // partial unique via raw SQL: CREATE UNIQUE INDEX ... WHERE is_active=true（详「数据迁移」段）
  // schema.prisma 仅声明常规 index，运行时 partial unique 依赖 raw migration
  @@index([formTemplateKey, organizationId])
  @@index([formTemplateKey])
  @@index([organizationId, isActive])
  @@map("approval_policies")
  @@schema("corp_approval")
}

enum AiMode {
  OFF
  RECOMMEND
  RECOMMEND_WITH_REVIEW
  RECOMMEND_WITH_HARD_GATE
  SUGGEST_ONLY
}

enum CrossOrgVisibility {
  NONE
  HIERARCHICAL
  GLOBAL
}
```

> **注**：Prisma 当前不直接支持 `WHERE` partial unique；需用 raw migration SQL（详「数据迁移」）。

---

### 表 2: `approval_policy_history` (`ApprovalPolicyHistory`)

策略本身的版本审计 trail（详 01-prd §5.1.3）。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | UUID | ✅ | 主键 |
| `policy_id` | UUID | ✅ | FK → `approval_policies.id`（指向原 policy，软删后仍可查） |
| `revision_number` | INT | ✅ | 同 policy_id 内自增 |
| `before_snapshot` | JSONB | ❌ | 变更前完整字段 JSON（INSERT 时为 NULL） |
| `after_snapshot` | JSONB | ✅ | 变更后完整字段 JSON |
| `change_type` | ENUM | ✅ | `CREATE / UPDATE / DEACTIVATE / AUTO_BUDGET_DOWNGRADE` |
| `change_reason` | TEXT | ❌ | 人写理由（UI 表单收集） |
| `changed_by_id` | UUID | ✅ | 触发变更的用户；AUTO 类型 = 系统账号 UUID |
| `changed_at` | TIMESTAMPTZ | ✅ | 变更时间 |
| `organization_id` | UUID | ✅ | 标准字段 |
| `created_at` | TIMESTAMPTZ | ✅ | 标准字段 |

**约束**:
- PK = `id`
- FK `policy_id` → `approval_policies.id` ON DELETE CASCADE
- UNIQUE `(policy_id, revision_number)`

**索引**:
- `(policy_id, revision_number DESC)`（按 policy 倒序查 history）
- `(changed_at DESC)`（全局时间轴查询）
- `(change_type, changed_at DESC)`（按变更类型筛选；AUTO_BUDGET_DOWNGRADE 告警查询）

**告警查询**：某 `(form_template, organization_id)` 24 小时内 `change_type IN (CREATE, UPDATE)` ≥ 3 次 → 告警 AI Ops（防策略变更滥用 / 回环抖动）。

---

### 表 3: `risk_tier_registry` (`RiskTierRegistry`)

3 行固定配置（详 01-prd §5.2）。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tier_id` | INT | ✅ | 主键；取值 `1 / 2 / 3` |
| `name` | VARCHAR(32) | ✅ | `'低风险'` / `'中风险'` / `'高风险'` |
| `irreversibility_max` | INT | ✅ | 不可逆性评分上限 0-10 |
| `blast_radius_max` | INT | ✅ | 影响半径评分上限 0-10 |
| `compliance_max` | INT | ✅ | 合规敏感度评分上限 0-10 |
| `confidence_required_min` | DECIMAL(3,2) | ✅ | 置信度下限 0.00-1.00 |
| `default_ui_mode` | ENUM | ✅ | 该 tier 的默认 UI mode 提示（写策略时参考；非约束） |
| `created_at` | TIMESTAMPTZ | ✅ | 标准字段 |
| `updated_at` | TIMESTAMPTZ | ✅ | 标准字段 |

**约束**:
- PK = `tier_id`
- CHECK `tier_id IN (1, 2, 3)`
- 4 维评分上限 CHECK `BETWEEN 0 AND 10`
- `confidence_required_min` CHECK `BETWEEN 0.00 AND 1.00`

**种子数据**（seed.ts 首次创建）:

| tier_id | name | irreversibility_max | blast_radius_max | compliance_max | confidence_required_min | default_ui_mode |
|---|---|---|---|---|---|---|
| 1 | 低风险 | 3 | 3 | 3 | 0.65 | RECOMMEND |
| 2 | 中风险 | 6 | 6 | 6 | 0.70 | RECOMMEND_WITH_REVIEW |
| 3 | 高风险 | 10 | 10 | 10 | 0.75 | RECOMMEND_WITH_HARD_GATE |

**注**：本表 v1.0 不开放新增行（仅 IT 管理员 / AI Ops 改阈值，不增 tier 数）。Phase 2 若需 4 级以上 tier 再扩。

---

### 表 4: `ai_recommendation_logs` (`AiRecommendationLog`)

每个审批 task × AI 调用 → 推荐 + 置信度 + 推理链 + 修改 diff 全链路（详 01-prd §5.4）。**高频写表，按月分区**。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | UUID | ✅ | 主键 |
| `approval_task_id` | UUID | ✅ | FK → approval-engine `ApprovalTask.id` |
| `form_template_key` | VARCHAR(64) | ✅ | 反规范化便于分区 + 查询 |
| `policy_id` | UUID | ✅ | FK → `approval_policies.id`（用哪条策略生成） |
| `risk_tier_id` | INT | ✅ | 反规范化（policy 改 tier 不影响历史推荐） |
| `agent_session_id` | UUID | ❌ | FK → agent `AgentSession.id`（Phase 1 异步触发可能没 session） |
| `turn_id` | UUID | ❌ | FK → agent turn |
| `idempotency_key` | VARCHAR(128) | ✅ | `hash(approval_task_id + turn_id + prompt_hash)`；2 分钟窗口复用 |
| `status` | ENUM | ✅ | `PENDING / READY / ADOPTED / OVERRIDDEN / IGNORED / FAILED`（详 04-state-machine） |
| `failure_reason` | ENUM | ❌ | `LLM_TIMEOUT / PARSE_ERROR / BUDGET_EXCEEDED / MODEL_ROUTER_EXHAUSTED / COMPLIANCE_BLOCKED / PII_SAFEGUARD_VIOLATION / AI_UNAVAILABLE_OTHER`；status=FAILED 时必填 |
| `recommended_action` | ENUM | ❌ | `APPROVE / REJECT / RETURN`；status=READY 时必填 |
| `agent_confidence` | DECIMAL(4,3) | ❌ | LLM self-report 置信度 0.000-1.000；Phase 1 必填 |
| `calibrated_confidence` | DECIMAL(4,3) | ❌ | Phase 2 ensemble 校准后置信度；v1.0 默认 NULL |
| `effective_ux_mode` | AiMode | ❌ | per-recommendation 渲染模式（status=READY 时必填；详 01-prd §5.4.2 step 7 降级表） |
| `reasoning_chain` | TEXT | ❌ | LLM 推理链；max 4KB；TOAST 压缩；status=READY 时必填 |
| `risk_indicators` | JSONB | ❌ | 风险点数组 `[{ name, severity, snippet }]`；用于反 rubber-stamp checkbox |
| `historical_similar_task_ids` | UUID[] | ❌ | 历史相似 Top 3 task ID（按 organization_id 隔离 + Policy.cross_org_visibility 控制） |
| `model_calls` | JSONB | ❌ | LLM 调用 trace `[{ call_id, model, tokens_in, tokens_out, duration_ms, cost_usd }]` |
| `total_cost_usd` | DECIMAL(8,4) | ❌ | 本次推荐累计成本 |
| `total_duration_ms` | INT | ❌ | 全流程毫秒数 |
| `adopted_at` | TIMESTAMPTZ | ❌ | 审批人采纳时间（status=ADOPTED 时填） |
| `adopted_by_id` | UUID | ❌ | 采纳的审批人（status=ADOPTED 时填） |
| `overridden_at` | TIMESTAMPTZ | ❌ | 审批人推翻时间 |
| `overridden_by_id` | UUID | ❌ | 推翻的审批人 |
| `modification_diff` | JSONB | ❌ | 结构化 diff `{ from: {action, ...}, to: {action, ...} }`；status=OVERRIDDEN 必填 |
| `modification_reason` | TEXT | ❌ | 推翻理由人写文本（Tier 2/3 要求 ≥ 20 字） |
| `reading_duration_ms` | INT | ❌ | 审批人推理链区域 hover 总秒数；反 rubber-stamp 行为分析（详 01-prd §7.3） |
| `scroll_distance_px` | INT | ❌ | 推理链区域 scroll 距离 |
| `ai_unavailable_reason` | VARCHAR(64) | ❌ | **写入路径独立于 `failure_reason`**：`failure_reason` 仅 status=FAILED 时由系统写（详上）；`ai_unavailable_reason` 由审批人 IGNORE 路径写（值 `'USER_IGNORED'`）或冗余字符串镜像 failure_reason 用于 audit-system 跨表 SQL 查询便利。**不重复存储语义**：FAILED 时 `failure_reason` 是真理 + `ai_unavailable_reason` 镜像；IGNORED 时仅 `ai_unavailable_reason` 有值（`'USER_IGNORED'`） |
| `created_at` | TIMESTAMPTZ | ✅ | 标准字段 + **分区键** |
| `organization_id` | UUID | ✅ | 标准字段 |

**约束**:
- PK = `(id, created_at)`（分区表 PK 必含分区键）
- UNIQUE `(idempotency_key, created_at)`（同分区幂等）
- FK `approval_task_id` → `approval_tasks.id`
- FK `policy_id` → `approval_policies.id`
- FK `risk_tier_id` → `risk_tier_registry.tier_id`
- CHECK `agent_confidence BETWEEN 0.000 AND 1.000`
- CHECK `calibrated_confidence IS NULL OR calibrated_confidence BETWEEN 0.000 AND 1.000`
- CHECK `(status = 'FAILED') = (failure_reason IS NOT NULL)`
- CHECK `status IN ('PENDING','FAILED') OR (recommended_action IS NOT NULL AND agent_confidence IS NOT NULL AND reasoning_chain IS NOT NULL AND effective_ux_mode IS NOT NULL)`
  - **语义**：仅 `PENDING / FAILED` 允许 4 字段为 NULL；`READY / ADOPTED / OVERRIDDEN / IGNORED` 必须 4 字段全非空（READY 转入终态时这些字段保留，详 04-state-machine）

**分区策略**:
- **PG range partition by `created_at` (per month)**
- 命名：`ai_recommendation_logs_y2026m05` / `y2026m06` ...
- 一次性预建未来 6 个月分区 + cron 每月初新建后 7 个月
- **> 6 个月**：归档到对象存储（详「数据生命周期」）

**索引**:
- `(approval_task_id, created_at DESC)` 单审批查推荐
- `(idempotency_key, created_at DESC)` 幂等查询
- `(form_template_key, status, created_at DESC)` 校准 dashboard 按模板 + 状态
- `(organization_id, status, created_at DESC)` 按组织
- `(status, created_at DESC) WHERE status='FAILED'` failure rate 告警

**字段语义层级（关键不变量）**:
- `ai_mode`（Policy 级配置） → 决定"模板默认 UX 是什么"
- `effective_ux_mode`（per-recommendation） → 决定"这条推荐实际 UX 渲染"，**枚举集合 ⊆ ai_mode**，不引入新值；详降级表（01-prd §5.4.2 step 7）

---

### 表 5: `calibration_metrics_snapshot` (`CalibrationMetricsSnapshot`)

物化校准指标（详 01-prd §5.5）。**5 分钟 cron 增量刷新**。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | UUID | ✅ | 主键 |
| `form_template_key` | VARCHAR(64) | ✅ | 维度 |
| `organization_id` | UUID | ✅ | 维度；`'__GLOBAL__'` 表示全组织聚合 |
| `risk_tier_id` | INT | ✅ | 维度 |
| `hour_bucket` | TIMESTAMPTZ | ✅ | 时间桶（按小时对齐） |
| `total_recommendations` | INT | ✅ | 总推荐数 |
| `adopted_count` | INT | ✅ | ADOPTED 数 |
| `overridden_count` | INT | ✅ | OVERRIDDEN 数 |
| `ignored_count` | INT | ✅ | IGNORED 数 |
| `failed_count` | INT | ✅ | FAILED 数 |
| `modification_rate` | DECIMAL(5,4) | ✅ | `overridden_count / (adopted + overridden)`；NaN → 0 |
| `time_to_decision_p50_ms` | INT | ❌ | 审批人查看 → 决策 p50 |
| `time_to_decision_p99_ms` | INT | ❌ | p99 |
| `confidence_bucket_low` | INT | ✅ | confidence < 0.5 数 |
| `confidence_bucket_mid_low` | INT | ✅ | 0.5-0.7 |
| `confidence_bucket_mid_high` | INT | ✅ | 0.7-0.85 |
| `confidence_bucket_high` | INT | ✅ | > 0.85 |
| `total_cost_usd` | DECIMAL(10,4) | ✅ | 本桶累计成本 |
| `last_refreshed_at` | TIMESTAMPTZ | ✅ | 上次刷新时间 |
| `created_at` | TIMESTAMPTZ | ✅ | 标准字段 |

**约束**:
- PK = `id`
- UNIQUE `(form_template_key, organization_id, risk_tier_id, hour_bucket)`
- CHECK `modification_rate BETWEEN 0.0000 AND 1.0000`

**索引**:
- 上述 UNIQUE 自动建索引
- `(hour_bucket DESC, form_template_key)` Dashboard 时间轴查询

**告警阈值**（cron 跑完即检查；详 01-prd §5.5.2）:
- `modification_rate > 0.30` 24h 滚动 → 告警
- `failed_count > 10` 1h → 告警
- `confidence_bucket_low / total > 0.40` → 告警
- `total_cost_usd / monthly_budget > 0.90` → 告警

---

## 🔗 修改现有表（跨模块声明）

本模块**声明扩展需求**，实施在对应模块的 06-data-model：

### `FormVersion.schema._ai` 子节点（form-engine 实施）

JSON 顶层 `_ai` 节点（详 01-prd §5.3）。**不动 Prisma schema**（schema 字段是 JSON），但 form-engine 需在 §06-data-model 文档说明 `_ai` 节点 schema + frozen 字段语义 + sensitive_fields 强制清单。

### `FormTemplate.source` + `origin`（form-management 实施）

新增 2 字段：

```prisma
model FormTemplate {
  // ... 现有字段
  source        FormSource @default(DESIGNER) @map("source")
  origin        Json?      @map("origin")     // 生成元数据：{ aiSpecId?, prompt?, modelVersion?, templateId? }
}

enum FormSource {
  DESIGNER
  AI
  TEMPLATE
}
```

历史 FormTemplate 一次性 batch UPDATE 设 `source='DESIGNER'`；分批 1000/批 + 限流 + 失败重试。

### `AuditLog` 6 字段 + AuditAction enum（audit-system 实施）

新增字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `agent_confidence` | DECIMAL(4,3) | AI 推荐置信度（LLM self-report） |
| `routing_tier` | INT | RiskTierRegistry.tier_id；记录走哪个 tier |
| `modified_action` | VARCHAR(32) | 审批人推翻后的动作 |
| `modification_diff` | JSONB | 结构化 diff |
| `time_to_decision_ms` | INT | 审批人查看 → 决策毫秒数 |
| `ai_unavailable_reason` | VARCHAR(64) | AI 不可用原因 |

`AuditAction` enum 新增 4 值：
- `AI_RECOMMENDED`
- `AI_ADOPTED`
- `AI_OVERRIDDEN`
- `AI_UNAVAILABLE`

**哈希链分段迁移**（详 audit-system 06-data-model）:
- 旧 chain `schema_version='pre-ai-ext'` 冻结，不重算
- 新写记录 `schema_version='ai-ext-v1'` 起新 chain
- 校验脚本分段执行；事件 `HASH_CHAIN_BROKEN` 不跨段触发

---

## 🔢 枚举定义（汇总）

```typescript
enum AiMode {
  OFF = 'OFF',
  RECOMMEND = 'RECOMMEND',
  RECOMMEND_WITH_REVIEW = 'RECOMMEND_WITH_REVIEW',
  RECOMMEND_WITH_HARD_GATE = 'RECOMMEND_WITH_HARD_GATE',
  SUGGEST_ONLY = 'SUGGEST_ONLY',
}

enum CrossOrgVisibility {
  NONE = 'NONE',
  HIERARCHICAL = 'HIERARCHICAL',
  GLOBAL = 'GLOBAL',
}

enum AiRecommendationStatus {
  PENDING = 'PENDING',         // 已创建，等待 LLM 调用
  READY = 'READY',             // LLM 返回，等待审批人决策
  ADOPTED = 'ADOPTED',         // 审批人采纳
  OVERRIDDEN = 'OVERRIDDEN',   // 审批人修改后采纳 / 推翻
  IGNORED = 'IGNORED',         // 审批人忽略（不选 AI 推荐）
  FAILED = 'FAILED',           // LLM 调用失败 / budget 超限 / PII 阻断
}

enum AiFailureReason {
  LLM_TIMEOUT,
  PARSE_ERROR,
  BUDGET_EXCEEDED,
  MODEL_ROUTER_EXHAUSTED,
  COMPLIANCE_BLOCKED,
  PII_SAFEGUARD_VIOLATION,
  AI_UNAVAILABLE_OTHER,
}

enum ApprovalPolicyChangeType {
  CREATE,
  UPDATE,
  DEACTIVATE,
  AUTO_BUDGET_DOWNGRADE,
}

enum FormSource {
  DESIGNER,
  AI,
  TEMPLATE,
}
```

---

## 📊 ER 关系图

```
                    +------------------------+
                    | risk_tier_registry     |
                    | (3 rows: 1/2/3)        |
                    +-----------+------------+
                                |
                                | tier_id
                                |
                +---------------+----------------+
                |                                |
                v                                v
+---------------+---------+            +---------+--------------+
| approval_policies       |            | ai_recommendation_logs |
| (partial unique on      |            | (PG range partition    |
|  is_active=true)        |            |  by month)             |
+---------+---------------+            +---------+--------------+
          |                                      |
          | policy_id                            |
          |                                      | approval_task_id
          v                                      |
+---------+----------+                           v
| approval_policy_   |              +--------------------+
| history            |              | approval_tasks     |
| (audit trail)      |              | (approval-engine)  |
+--------------------+              +--------------------+

           +--------------------+
           | calibration_metrics|     <-- 5min cron 物化
           | _snapshot          |     <-- 来源: ai_recommendation_logs
           +--------------------+

  跨模块（声明扩展，不在本模块实施）:
  - form_versions.schema._ai (form-engine)
  - form_templates.source/origin (form-management)
  - audit_logs.{agent_confidence, routing_tier, ...} (audit-system)
```

---

## 🗓️ 数据生命周期

| 表 | 保留 | 归档 |
|---|---|---|
| `approval_policies` | 永久（is_active=false 也保留） | 不归档 |
| `approval_policy_history` | 7 年（合规） | 7 年后归档对象存储；保留 audit trail |
| `risk_tier_registry` | 永久 | 不归档 |
| `ai_recommendation_logs` | 6 个月 | > 6 月迁对象存储；`reasoning_chain` + `model_calls` 字段优先压缩 |
| `calibration_metrics_snapshot` | 2 年（趋势分析） | > 2 年聚合到日粒度归档 |

**归档脚本**（独立 cron 每月跑）:
- 扫旧分区 → 导出 parquet → 上传 S3 (按 organization_id 分目录) → `DETACH PARTITION` → 验证 → `DROP TABLE`
- 失败不删；告警 AI Ops

---

## 📈 体量估算

| 表 | 行数（1 年）| 单行 | 总 |
|---|---|---|---|
| `approval_policies` | ~50 × 5 = 250 active + ~500 history | 2KB | < 2MB |
| `approval_policy_history` | ~500/年 | 4KB（JSONB snapshot）| ~2MB/年 |
| `risk_tier_registry` | 3 | 0.5KB | < 2KB |
| `ai_recommendation_logs` | **1000 审批/天 × 5 模板 × 365 = 1.8M 行/年** | 4KB（reasoning_chain TOAST 压缩后均值）| **~7GB/年** + 索引 ≈ 9GB/年 |
| `calibration_metrics_snapshot` | 5 模板 × 24h × 365 = ~44K/年 | 0.5KB | < 25MB/年 |

**热点**：`ai_recommendation_logs` 是绝对大头；分区 + 归档兜底。

---

## 🚚 数据迁移（Prisma migration）

### Migration 1: 5 张新表 + enum + 种子（单 PR 单 migration）

按 CLAUDE.md 「每次提交最多包含一个迁移文件」：合并所有 schema 变更进单一 migration。

```sql
-- 1. enum 定义
CREATE TYPE corp_approval.ai_mode AS ENUM ('OFF', 'RECOMMEND', 'RECOMMEND_WITH_REVIEW', 'RECOMMEND_WITH_HARD_GATE', 'SUGGEST_ONLY');
CREATE TYPE corp_approval.cross_org_visibility AS ENUM ('NONE', 'HIERARCHICAL', 'GLOBAL');
CREATE TYPE corp_approval.ai_recommendation_status AS ENUM ('PENDING', 'READY', 'ADOPTED', 'OVERRIDDEN', 'IGNORED', 'FAILED');
CREATE TYPE corp_approval.ai_failure_reason AS ENUM ('LLM_TIMEOUT', 'PARSE_ERROR', 'BUDGET_EXCEEDED', 'MODEL_ROUTER_EXHAUSTED', 'COMPLIANCE_BLOCKED', 'PII_SAFEGUARD_VIOLATION', 'AI_UNAVAILABLE_OTHER');
CREATE TYPE corp_approval.approval_policy_change_type AS ENUM ('CREATE', 'UPDATE', 'DEACTIVATE', 'AUTO_BUDGET_DOWNGRADE');

-- 2. risk_tier_registry (含 seed)
CREATE TABLE corp_approval.risk_tier_registry (...);
INSERT INTO corp_approval.risk_tier_registry VALUES (1, '低风险', 3, 3, 3, 0.65, 'RECOMMEND'), (2, ...), (3, ...);

-- 3. approval_policies
CREATE TABLE corp_approval.approval_policies (...);
CREATE UNIQUE INDEX approval_policies_active_uniq ON corp_approval.approval_policies (form_template_key, organization_id) WHERE is_active=true;

-- 4. approval_policy_history
CREATE TABLE corp_approval.approval_policy_history (...);

-- 5. ai_recommendation_logs（分区表）
CREATE TABLE corp_approval.ai_recommendation_logs (...) PARTITION BY RANGE (created_at);
CREATE TABLE corp_approval.ai_recommendation_logs_y2026m06 PARTITION OF corp_approval.ai_recommendation_logs FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- ... 预建 6 个月
CREATE OR REPLACE FUNCTION create_next_month_partition() ... (cron 每月跑)

-- 6. calibration_metrics_snapshot
CREATE TABLE corp_approval.calibration_metrics_snapshot (...);

-- 7. top 5 form_template 起步 ApprovalPolicy seed（按 01-prd 附录 A）
INSERT INTO corp_approval.approval_policies (form_template_key, organization_id, ...) VALUES
  ('expense', '__GLOBAL__'::uuid, 1, 'RECOMMEND', 0.70, 1000, ...),
  ('business-trip', '__GLOBAL__'::uuid, 1, 'RECOMMEND', 0.70, 800, 'HIERARCHICAL', ...),
  -- ...
```

**注**：
- Prisma 不原生支持 partial unique with WHERE → schema.prisma 仅声明常规 `@@index([form_template_key, organization_id])`（不写 @@unique）；partial unique 由 raw migration `CREATE UNIQUE INDEX ... WHERE is_active=true` 保证；应用层在 service 写入路径额外做 active-row 互斥校验作为双层防御
- 分区表 PG 12+ 支持；FFOA 当前 PG 版本需确认 ≥ 12（已 ✓）
- 跨模块字段扩展（FormVersion / FormTemplate / AuditLog）由对应模块各自 migration 提交，**不在本 migration 内**

---

## ❗️待确认项

- [ ] PG 版本确认 ≥ 12（支持 partition BY RANGE）
- [ ] `__GLOBAL__` 兜底组织如何在 Organization 表表达：单独 UUID 还是 nullable organization_id？**推荐**：固定 UUID `00000000-0000-0000-0000-000000000000` + 系统级保护
- [ ] `ai_recommendation_logs` 是否对 `agent_session_id` 加 FK：agent 模块的 AgentSession 可能跨 schema，跨 schema FK 性能开销大 → **推荐**：不加 FK，仅留字段引用
- [ ] CalibrationMetricsSnapshot 物化 cron 实现位置：本模块自建 cron / 复用 audit-system 物化基建 → **推荐**：本模块自建（5 分钟 cron 独立部署）

---

## 🔗 关联

- [01-prd.md](./01-prd.md) §5.1-5.5
- [07-api.md](./07-api.md)
- [04-state-machine.md](./04-state-machine.md)
- approval-engine [`06-data-model.md`](../approval-engine/06-data-model.md)
- audit-system [`06-data-model.md`](../audit-system/06-data-model.md)
- form-engine [`06-data-model.md`](../form-engine/06-data-model.md)
- standards [`04-database-architecture.md`](../../standards/04-database-architecture.md) 标准字段
- standards [`16-data-layering-and-metadata-policy.md`](../../standards/16-data-layering-and-metadata-policy.md) L3 业务交易归类
- standards [`20-database-review-checklist.md`](../../standards/20-database-review-checklist.md) 22 条 schema review 清单
