# Robot Manager — 数据分层方案（面向平台未来）

> **created**: 2026-05-16
> **status**: 决策已拍板（7 项 + L4 取舍），待 PR 拆分实施
> **作者**: chentao + Claude
> **遵循 standard**: [`docs/standards/16-data-layering-and-metadata-policy.md`](../../../standards/16-data-layering-and-metadata-policy.md)（本文档是该 standard 在 robot-manager 模块的具体实例化）
> **关联**:
> - [汇总-v3-v5-业务方迭代.md](汇总-v3-v5-业务方迭代.md) — 业务方第二轮迭代
> - [需求分析.md](需求分析.md) — 业务需求事实源
> - [实现差距报告.html](实现差距报告.html) — v1 时点 gap（本文档替代它做"v3/v5 时点 gap"）
> - `backend/prisma/schema/robot_manager.prisma` — 当前实现

---

## 1. 这份文档解决什么

业务方迭代到 v3/v5 后，robot-manager 模块当前 schema（11 个 `Robot*` model）面临两个问题：

1. **Customer / Supplier / Location 等主数据被关在 `robot_manager` schema 里**，将来销售、服务、财务模块要复用得到处 join 不同 schema，或者每个模块各自重建一份。
2. **RobotUnit 当前是"状态机表"**（`currentStatus` / `customerId` / `locationId` 都挂在 RobotUnit 本身），不变属性和状态混在一起；外部模块引用 RobotUnit 时无法干净拿到"机器人个体"概念，状态变更会污染所有 join 它的查询。

这份文档把整个 robot-manager 重新分到**业界事实标准的四层架构**下（SAP / Salesforce / NetSuite 同款），并把跨模块复用的部分从 robot-manager 模块剥离到**平台公共层 (platform-master)**，作为后续 FFOA 所有模块的共享底座。

---

## 2. 设计前提（核心抉择）

讨论后确定，全部沉淀到 standard `16-data-layering-and-metadata-policy.md`，本模块直接遵循。

| # | 抉择 | 选择 | 理由摘要 |
|---|---|---|---|
| 1 | 分几层？ | **3 层**（L1 平台公共 / L2 领域主 / L3 业务交易），**默认不立 L4 元数据驱动层** | AI 时代加字段成本 30 分钟，元数据驱动绕开"工程师瓶颈"的原始价值失效；强类型 schema 反而对 AI 协作更友好。详见 standard §4 |
| 2 | RobotUnit 拆？ | **拆**：不变属性留 RobotUnit，状态进 RobotLifecycleEvent，物化 `RobotUnitSnapshot` | 外部模块干净引用 RobotUnit；状态变更不污染 join |
| 3 | 跨模块主数据放哪？ | **平台公共层**（独立 schema `platform_master`）| 所有模块共享底座；其他模块只 FK 引用不改 schema |
| 4 | "人"维度怎么处理？ | 内部员工用现有 `User`（platform_iam）；外部联系人挂主数据下的 `contacts[]` 子表 | 不重复建 Person 主数据 |
| 5 | 跨模块写 RobotUnit | **走 LifecycleEvent**（不直改 prisma client）| 写权责归位 + 跨模块竞态消失 + 事件天然审计 |
| 6 | 字典立独立表 vs 通用 Dictionary | **混合**：有专属字段（Currency.decimals）或被强类型 FK 频繁引用的立独立表；纯枚举入 `Dictionary(category, code, label)` | 类型安全 ↔ 重复劳动的平衡 |
| 7 | RobotUnitSnapshot VIEW vs 物化表 | **物化表** + service 层事件同事务刷 | 千级以上规模查询 0 join；事件溯源 CQRS 标准做法 |
| 8 | SystemConfig 立刻泛化？ | **本期不动**（保留 `RobotSystemConfig` 模块内）| YAGNI；第二个模块要时再泛化也便宜 |
| 9 | Note / Tag 多态表本期立？ | **本期不立**，各表加 `notes String?` 字段；Attachment 多态保留 | 当前没有跨模块复用 |
| 10 | v5 历史数据导入要不要写审计事件？| **写**（`event_type=imported_from_v5`）| LifecycleEvent 是事实源，事件流不能有空白 |

---

## 3. 三层架构总览

```
┌────────────────────────────────────────────────────────────────┐
│ L3  Transactional Data（业务/交易 · 流程产生）                  │
│     PurchaseOrder / RobotLifecycleEvent / SalesOrder /         │
│     DeliveryRequest / PaymentRecord / ServiceTicket /          │
│     QualityLabelRecord                                         │
└────────────────────────────────────────────────────────────────┘
                            ▲
                            │ FK 引用主数据
                            │
┌────────────────────────────────────────────────────────────────┐
│ L2  Domain Master Data（领域主数据 · 模块专属、慢变）           │
│     Model / Sku / RobotUnit（仅不变属性）                       │
└────────────────────────────────────────────────────────────────┘
                            ▲
                            │ FK 引用平台公共主数据
                            │
┌────────────────────────────────────────────────────────────────┐
│ L1  Platform Master Data（平台公共主数据 · 跨模块复用）         │
│  L1a Master:    Customer / Supplier / Partner / Location       │
│  L1b Reference: Currency / Country / TariffType / LabelType    │
│  L1c Common:    Attachment（多态附件）                          │
└────────────────────────────────────────────────────────────────┘
```

**依赖方向严格单向**：L1 ↓ 不依赖 L2/L3；L2 依赖 L1；L3 依赖 L1 + L2。反向引用（如销售模块写 robot 状态）必须通过事件流（`RobotLifecycleEvent.relatedType/Id`），不走 FK。

> **L4 元数据驱动层为何不立**：业界 SAP/Salesforce 的元数据驱动是为了绕开"代码变更慢、需工程师"的瓶颈；AI 时代加字段 30 分钟，原始价值失效，反而损失类型安全 + AI 协作效率 + 调试链路。字段全部强类型落 prisma schema、状态用 enum、状态转换硬编码常量。完整论证 + 加回触发条件见 [standard §4](../../../standards/16-data-layering-and-metadata-policy.md#4-默认不立-l4元数据驱动层)。

### 物理位置约定

| 层 | Prisma schema 文件 | Postgres schema | 模块代码位置 |
|---|---|---|---|
| L1 | `prisma/schema/platform_master.prisma`（新建）| `platform_master` | `backend/src/modules/platform-master/` |
| L2 | `prisma/schema/robot_manager.prisma`（保留 + 瘦身）| `robot_manager` | `backend/src/modules/robot-manager/` |
| L3 | `prisma/schema/robot_manager.prisma`（同上）| `robot_manager` | `backend/src/modules/robot-manager/` |

---

## 4. L1 平台公共层（platform-master）

### 4.1 L1a 主数据（Master Data）

#### `Customer` — 客户

跨模块复用：robot-manager / sales / service / finance / contract

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | UUID | 主键 |
| `code` | String unique | 业务编码（如 `C-2026-001`） |
| `name` | String | 客户名称 |
| `type` | enum `CustomerType` | `B2B` / `B2C` / `INTERNAL` |
| `industry` | String? | 行业（自由填或引 Industry 字典） |
| `country` | String | FK → Country.code |
| `taxId` | String? | 税号 / 统一社会信用代码 |
| `creditLimit` | Decimal? | 信用额度 |
| `currency` | String | 默认结算币种，FK → Currency.code |
| `organizationId` | UUID | DataScope 标准字段 |
| `createdAt/updatedAt/createdById/deletedAt` | — | 标准字段 |
| `metadata` | Json | 模块扩展字段（如 robot-manager 的 mentor/mentee）|

**子表**：
- `CustomerContact[]` — 外部联系人（一对多）：`name / role / phone / email / isPrimary`
- `CustomerAddress[]` — 地址（一对多）：发货地址、账单地址等

#### `Supplier` — 供应商

跨模块复用：robot-manager / procurement / parts / logistics / finance

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | UUID | |
| `code` | String unique | |
| `name` | String | |
| `type` | enum `SupplierType` | `MANUFACTURER` / `PARTS` / `LOGISTICS` / `SERVICE` |
| `country` | String | FK → Country.code |
| `currency` | String | 结算币种 |
| `paymentTerms` | String? | |
| `leadTimeDays` | Int? | |
| `organizationId` / 标准字段 | — | |

**子表**：`SupplierContact[]`、`SupplierBankAccount[]`（财务模块用）

#### `Partner` — 合作伙伴

跨模块复用：robot-manager / sales / customs / training

| 字段 | 说明 |
|---|---|
| `id` / `code` / `name` | |
| `role` | enum `PartnerRole`：`SALES_AGENT` / `MENTOR` / `CUSTOMS_BROKER` / `TRAINING_PARTNER` |
| `country` / `currency` | |
| 标准字段 + `metadata` | |

**子表**：`PartnerContact[]`

#### `Location` — 地点

跨模块复用：robot-manager / inventory / logistics / service / site-attendance

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | UUID | |
| `code` | String unique | |
| `name` | String | |
| `type` | enum `LocationType` | `WAREHOUSE` / `CUSTOMER_SITE` / `FACTORY` / `BONDED_ZONE` / `IN_TRANSIT` / `SERVICE_CENTER` |
| `country` | String | FK → Country.code |
| `address` | String | |
| `latitude/longitude` | Decimal? | 可选地理坐标 |
| `parentLocationId` | UUID? | 层级（如仓库 → 库位）|
| `customerId` | UUID? | 客户站点回引 Customer（保留当前实现思路） |
| 标准字段 + `metadata` | — | |

---

### 4.2 L1b 字典 / 参考数据（Reference Data）

字典数据**全局静态**，编码即标识，跨所有模块。

| 表 | 字段 | 用途 |
|---|---|---|
| `Currency` | `code (USD/CNY/EUR) / symbol / decimals / name` | 金额本地化 |
| `Country` | `code (CN/US/UAE) / iso3 / name / region` | 客户/供应商/物流国家 |
| `Region` | `code / name / countries[]` | 国际化区域分组（已有部分实现于 corp_hr）|
| `UnitOfMeasure` | `code (PCS/KG/L) / name / category` | 物料计量 |
| `TariffType` | `code / name / hsCode` | 跨境关税类型 |
| `DeclarationType` | `code / name` | 进口申报类型 |
| `LabelType` | `code (FCC/BATTERY_SN/SN_BODY/...) / name / scope` | 质量标签类型，被 QualityLabelRecord 引用 |
| `Industry` | `code / name` | 客户行业字典（可选） |

**实现方式**：统一一张 `Dictionary` 表（`category` + `code` 联合主键）vs 每个字典一张表？
- **推荐每个字典一张表**：类型安全、可加专属字段（如 Currency.decimals）、Prisma 强类型 FK
- 仅纯文本字典（无专属字段、纯枚举）可合并到 `Dictionary(category, code, label)`

### 4.3 L1c 通用业务对象（Common）

跨主数据/业务表都会用，多态。

#### `Attachment` — 附件（多态）

替代当前的 `RobotAttachment`，泛化为可挂在任意实体上。

| 字段 | 说明 |
|---|---|
| `id` | |
| `ownerType` | String（`robot_unit` / `customer` / `purchase_order` / ...）|
| `ownerId` | UUID |
| `filename` / `mimeType` / `size` / `storagePath` | |
| `category` | String?（合同 / 发票 / 验收单 / 图片 / ...） |
| `uploadedById` / `uploadedAt` | |

**索引**：`(ownerType, ownerId)`

#### `Note` — 备注（多态，可选）

如果不想给每张业务表都加 `notes` 字段，可建多态 Note 表（同 Attachment 模式）。本期可不立，等多模块都要 notes 时再立。

#### `Tag` — 标签（多态，可选）

预留。本期不实现。

---

## 5. 字段 / 状态 / 流程的强类型落地（替代 L4 元数据驱动）

**按 standard 16 §4 默认不立 L4**。robot-manager 的具体落地：

### 5.1 字段：强类型 prisma schema

v5 Master 82 列字段全部落 prisma：
- **不变属性** → `RobotUnit` 骨架字段（见 §6.3）
- **业务流程产生的字段** → 各业务表骨架字段（见 §7，如 `RobotLifecycleEvent.payload` / `PurchaseOrder.totalAmount` / `QualityLabelRecord.labelTypeCode`）
- **少量真正不定型字段** → `metadata` JSONB 兜底，但**不再配套 FieldDefinition 定义表**

当前 `RobotFieldDef` 表里 31 个字段定义，迁移时全部硬翻译成 prisma 字段（不变属性进 RobotUnit、生命周期字段进对应业务表）；表本身在 PR3 之后删除。

### 5.2 状态：Prisma enum

21 个 stage 用一个 enum：

```prisma
enum RobotLifecycleStage {
  PO_CREATED
  IN_PRODUCTION
  READY_TO_SHIP
  IN_TRANSIT
  // ... 21 个
}
```

i18n 标签放前端 `locales/{lang}/robot-manager.json`，不进 DB。

### 5.3 状态转换：service 层硬编码常量

```typescript
const STAGE_TRANSITIONS: Record<RobotLifecycleStage, RobotLifecycleStage[]> = {
  PO_CREATED: [IN_PRODUCTION, CANCELLED],
  IN_PRODUCTION: [READY_TO_SHIP, ...],
  // ...
}
```

### 5.4 控制门 G1-G6：service 层方法

```typescript
class RobotGuardService {
  checkG1_ReadyToShip(unit: RobotUnit): GuardResult { ... }
  checkG2_CustomsCleared(unit: RobotUnit): GuardResult { ... }
  // ...
}
```

### 5.5 加回元数据驱动的明确触发条件

见 [standard 16 §4.6](../../../standards/16-data-layering-and-metadata-policy.md#46-加回-l4-的明确触发条件满足任一)。**robot-manager 当前不命中任一条**。

---

## 6. L2 Robot-Manager 模块主数据

### 6.1 `Model` — 机器人型号

基本对齐当前 `RobotModel`，**改名 + 移出 `Robot` 前缀**（按 schema 隔离表名重复不冲突）。

| 字段 | 当前实现 | 调整 |
|---|---|---|
| `id / code / name / brand / description / imageUrl / enabled / metadata` | ✅ | 不变 |
| `specSummary` | ❌ | 新增（用于产品手册简介） |
| `retiredAt` | ❌ | 新增（型号停产标记，影响新 PO 校验） |

### 6.2 `Sku` — SKU（型号 + 变体）

对齐当前 `RobotSku`，关键调整：

| 字段 | 调整 |
|---|---|
| `id / modelId / code / name / variant / defaultPrice / defaultCost / enabled / metadata` | 不变 |
| `usageType` | 新增（commercial / showcase / pilot / internal — v5 Master 字段）|
| `currency` | 新增 FK → `Currency.code`（替代隐式 USD） |

### 6.3 `RobotUnit` — 机器人个体（**仅不变属性**）

**核心拆分**：当前 RobotUnit 的"状态字段"全部移出。

| 字段 | 当前实现 | 调整 |
|---|---|---|
| `id` / `organizationId` / `version` | ✅ | 保留 |
| `ffsn` | ✅ | 保留（主键身份）|
| `placeholderSnOrig` | ❌ | **新增 v3**（占位 SN 激活后留痕）|
| `supplierSn` | metadata | **从 metadata 提升为骨架字段**（07 RECEIVED 扫码激活，关键身份字段）|
| `modelId` / `skuId` | ✅ | 保留 |
| `manufactureDate` | metadata | **从 metadata 提升**（不变属性）|
| `purchaseOrderId` | ❌ | **新增 FK** → `PurchaseOrder`（机器人来源的不变追溯）|
| `purchaseOrderLineId` | ❌ | **新增 FK** → `PurchaseOrderLine` |
| `originalSupplierId` | ❌ | **新增 FK** → `Supplier`（出厂供应商，跟当前持有方区分）|
| `retiredAt` | ❌ | **新增**（资产终止时点，类似软删但有业务语义）|
| `metadata` (Json) | ✅ | 保留，存"模块定义的扩展字段" |
| **以下字段移出** | | |
| ~~`currentStatus`~~ | ✅ | **移除**，由 `RobotLifecycleEvent` 最新事件或 `RobotUnitSnapshot.currentStatus` 派生 |
| ~~`supplierId`~~（当前持有方）| ✅ | **移除**（出厂供应商用 `originalSupplierId`；变化的"持有方/服务方"通过事件追） |
| ~~`customerId`~~（当前客户）| ✅ | **移除**，由 `RobotUnitSnapshot.currentCustomerId` 派生（最新交付 / 销售事件）|
| ~~`locationId`~~（当前位置）| ✅ | **移除**，由 `RobotUnitSnapshot.currentLocationId` 派生（最新位置事件）|

#### `RobotUnitSnapshot` — 物化"当前状态"（可选但强烈建议）

避免每次查 RobotUnit 都要 join 最新 LifecycleEvent。事件触发时 service 层同步更新 snapshot。

| 字段 | 说明 |
|---|---|
| `robotUnitId` (PK + FK) | 1:1 RobotUnit |
| `currentStatus` | 当前生命周期 stage（21 个之一）|
| `currentLocationId` | FK → Location |
| `currentCustomerId` | FK → Customer（已交付的话）|
| `currentSalesOrderId` | FK → SalesOrder（已预留 / 已售）|
| `lastEventId` | FK → RobotLifecycleEvent |
| `lastEventAt` | DateTime |
| `derivedAt` | 物化时间 |

**索引**：`currentStatus`、`currentLocationId`、`currentCustomerId`（替代当前 RobotUnit 上的这些索引）

> 也可以做**视图（VIEW）**而不是物理表，性能允许的话视图更优。后续 PR3 决定。

---

## 7. L3 Robot-Manager 模块业务表

### 7.1 采购侧

#### `PurchaseOrder` — 采购单（PO）

| 字段 | 说明 |
|---|---|
| `id / poNo (unique)` | 业务单号 |
| `supplierId` | FK → `platform_master.Supplier` |
| `currency` | FK → Currency |
| `totalAmount` | Decimal |
| `status` | enum `PurchaseOrderStatus`（草稿 / 已下单 / 部分到货 / 已到货 / 关闭）|
| `orderedAt` / `expectedAt` / `closedAt` | |
| `organizationId` / 标准字段 | |

#### `PurchaseOrderLine` — 采购单行

| 字段 | 说明 |
|---|---|
| `id / poId / skuId / lineNo` | |
| `quantity / unitPrice / totalPrice / currency` | |
| `placeholderPattern` | 占位 SN 命名规则（如 `{poNo}-LINE-{NNN}`）|
| `expectedAt` | |

> 触发节点 **01 PO_CREATED**：业务方在此节点为本 PO Line 在 `RobotUnit` 表生成 N 行占位机器人（`ffsn = placeholder, placeholderSnOrig = placeholder`），状态 = `ORDERED`。

### 7.2 生命周期事件侧

#### `RobotLifecycleEvent` — 机器人生命周期事件（事件源）

替代当前 `RobotStatusChangeLog`，升级为**全事件流**（不仅状态变更）。

| 字段 | 说明 |
|---|---|
| `id` | |
| `robotUnitId` | FK |
| `eventType` | enum：`status_changed` / `location_moved` / `handed_over` / `sn_activated` / `label_applied` / `quality_issued` / `payment_collected` / `delivery_signed` / `service_opened` / `service_closed` |
| `fromStatus` / `toStatus` | 仅 status_changed |
| `fromLocationId` / `toLocationId` | 仅 location_moved |
| `actorUserId` | FK → User（操作人）|
| `partnerId` | FK → Partner（如交接到 partner）|
| `customerId` | FK → Customer（如交付给客户）|
| `relatedType` / `relatedId` | 关联的业务表（PO / SO / DeliveryRequest / ServiceTicket）|
| `payload` (Json) | 事件专属负载 |
| `occurredAt` | |
| `recordedAt` | 录入时间（≠ occurredAt） |
| `notes` | 自由备注 |

**索引**：`(robotUnitId, occurredAt DESC)`、`(eventType, occurredAt)`、`(relatedType, relatedId)`

> **统一事件流**：状态变更、位置变更、扫码激活、标签贴附、销售预留、交付签收、服务开关单全部走这一张表。
> 优势：可重放、可审计、跨实体追溯（"这台机器人的全部历史"一句 SQL）

### 7.3 销售侧

#### `SalesOrder` — 销售订单 / 合同

| 字段 | 说明 |
|---|---|
| `id / soNo` | |
| `customerId` / `salesPersonId` (User) / `mentorId` / `menteeId` (Partner) | |
| `currency / totalAmount / contractStatus` | 合同状态：草拟 / 已签 / 已废 |
| `signedAt / closedAt` | |
| `status` | enum `SalesOrderStatus`：预留 / 待审 / 已审 / 履行中 / 已完成 / 已取消 |

#### `SalesOrderLine`

| 字段 | 说明 |
|---|---|
| `soId / lineNo` | |
| `robotUnitId` | FK 到具体机器人（**12 RESERVED** 时确定）|
| `salesPrice / discount / netAmount / currency` | |
| `expectedDeliveryDate` | |

### 7.4 交付侧

#### `DeliveryRequest` — 交付请求

| 字段 | 说明 |
|---|---|
| `id / deliveryNo` | |
| `customerId / salesOrderId` | |
| `requestType` | enum `DeliveryRequestType`（业务方定义）|
| `expectedDate` | |
| `status` | enum `DeliveryRequestStatus`：申请 / 已审 / 履行中 / 已完成 |

#### `DeliveryFulfillment` — 交付履行

| 字段 | 说明 |
|---|---|
| `id / deliveryRequestId / robotUnitId` | |
| `deliveredAt / signedAt` | |
| `acceptanceStatus` | 草签 / 已签 / 退回 |
| `signedFormAttachmentId` / `acceptanceFormAttachmentId` | FK → Attachment |

### 7.5 财务侧

#### `PaymentRecord` — 支付记录

| 字段 | 说明 |
|---|---|
| `id / paymentNo` | |
| `relatedType / relatedId` | 关联 PO 或 SO |
| `direction` | enum：`inbound`（收）/ `outbound`（付）|
| `amount / currency / paymentMethod` | |
| `paidAt` | |
| `attachmentIds[]` | 凭证 |

### 7.6 售后侧

#### `ServiceTicket` — 服务工单

替代当前 `RobotServiceRecord`，升级为支持多次跟踪。

| 字段 | 说明 |
|---|---|
| `id / ticketNo` | |
| `robotUnitId / customerId` | |
| `issueType` | FK → Dictionary(scope=service_issue_type) 或 ServiceIssueType 表 |
| `severity` | enum：`P0/P1/P2/P3` |
| `status` | enum `ServiceTicketStatus`：新建 / 处理中 / 待客户确认 / 已解决 / 已关闭 |
| `openedAt / closedAt` | |
| `openedById / resolvedById` | User |

**子表**：`ServiceTicketActivity[]` — 工单跟进记录（comment / status_change / handover）

#### `QualityLabelRecord` — 质量标签记录

替代"09 UNDER_MODIFICATION 7 个 Label 字段塞 metadata"的方案。

| 字段 | 说明 |
|---|---|
| `id / robotUnitId` | |
| `labelTypeCode` | FK → `LabelType.code` (`FCC` / `BATTERY_SN` / `SN_BODY` / `SN_REMOTE` / `INSPECTION_SHEET` / `SHIPPING_BOX` / `BODY_MADE_IN_CN`) |
| `status` | enum：`pending / applied / verified / rejected` |
| `appliedAt / verifiedAt` | |
| `appliedById / verifiedById` | |
| `attachmentId` | 标签照片 |

---

## 8. 跨模块引用关系（DAG）

```
Currency, Country, LabelType, TariffType  (L1b 字典)
        ▲
        │ FK 引用
        │
Customer, Supplier, Partner, Location, Attachment  (L1a 主数据)
        ▲                                ▲
        │                                │
        │              ┌─────────────────┘
        │              │
        │     Model ← Sku                     (L2 模块主数据)
        │      ▲       ▲
        │      │       │
        │      └───────┴── RobotUnit          (L2 模块主数据 · 不变属性)
        │                   ▲      ▲
        │                   │      │
        │                   │     RobotUnitSnapshot  (L2/L3 物化)
        │                   │
        │                   │
        └── PurchaseOrder, SalesOrder, DeliveryRequest, PaymentRecord,
            ServiceTicket, QualityLabelRecord, RobotLifecycleEvent  (L3 业务表)

   状态 enum / 状态转换常量 / 控制门 service 方法  ← 代码层（不在数据层）
   ↑ 替代 L4 元数据驱动，强类型 + AI 协作友好
```

**关键约束**：
1. L1 不依赖 L2/L3
2. L2 只依赖 L1
3. L3 依赖 L1 + L2
4. 反向引用（跨模块写）必须通过事件（`RobotLifecycleEvent.relatedType/Id`）而不是 FK

---

## 9. 跟当前实现的 Gap

当前 robot-manager 共 **11 个 model**；目标分层后约 **25-30 个 model**（多出 ~15 个）。

### 9.1 表级 Gap

| 当前表 | 目标位置 | 动作 |
|---|---|---|
| `RobotFieldDef` | → **删除**（不立 L4 元数据驱动），31 个字段定义硬翻译成强类型 prisma 字段（落 RobotUnit 骨架 / 业务表）| **删表** |
| `RobotSystemConfig` | 保留模块内（决策 8：本期不泛化）| **不动** |
| `RobotModel` | → `robot_manager.Model` | **改名瘦身**，加 `specSummary / retiredAt` |
| `RobotSku` | → `robot_manager.Sku` | **改名**，加 `usageType / currency` |
| `RobotSupplier` | → `platform_master.Supplier` | **迁移**，拆出 `SupplierContact[]` |
| `RobotCustomer` | → `platform_master.Customer` | **迁移**，拆出 `CustomerContact[]` `CustomerAddress[]` |
| `RobotLocation` | → `platform_master.Location` | **迁移**，加 `parentLocationId / latitude/longitude` |
| `RobotUnit` | → `robot_manager.RobotUnit` (瘦身) + `RobotUnitSnapshot`（新）| **拆**，状态字段移出 |
| `RobotServiceRecord` | → `robot_manager.ServiceTicket` + `ServiceTicketActivity[]` | **改名 + 升级** |
| `RobotStatusChangeLog` | → `robot_manager.RobotLifecycleEvent` | **改名 + 升级为全事件流** |
| `RobotAttachment` | → `platform_master.Attachment` | **迁移 + 多态化**（`ownerType/Id`） |

### 9.2 新增表

L1 平台公共层（共 ~11 个新表）：
- `Partner` / `CustomerContact` / `SupplierContact` / `PartnerContact` / `CustomerAddress`
- `Currency` / `Country` / `Region` / `UnitOfMeasure` / `TariffType` / `DeclarationType` / `LabelType` / `Industry`

L3 业务表（共 ~11 个新表）：
- `PurchaseOrder` / `PurchaseOrderLine`
- `SalesOrder` / `SalesOrderLine`
- `DeliveryRequest` / `DeliveryFulfillment`
- `PaymentRecord`
- `QualityLabelRecord`
- `ServiceTicketActivity`（子表）
- `RobotLifecycleEvent`（替代 StatusChangeLog 但语义升级）

L4 元数据：**全部不立**（决策 1）。状态用 prisma enum `RobotLifecycleStage`、转换用 service 层常量、控制门用 service 层方法、字段用强类型 prisma 字段。

### 9.3 字段级 Gap（关键）

| 维度 | 当前 | 目标（v3/v5）|
|---|---|---|
| 状态机 | RobotStatus enum 10 个 | **enum `RobotLifecycleStage` 21 个 stage**（直接扩 enum + migration）|
| 字段定义 | RobotFieldDef 31 个 + scope=unit | **全部翻译成强类型 prisma 字段**（不变属性进 RobotUnit / 流程字段进各业务表）；RobotFieldDef 表删除 |
| Placeholder SN | ❌ | `RobotUnit.placeholderSnOrig` + service 层"占位 → 激活"状态机副作用 |
| Supplier SN | metadata 里 | 提升为骨架字段 |
| PO 关联 | ❌ | `RobotUnit.purchaseOrderId/LineId` FK |
| 外部联系人 | 单字段 `contactName/Phone/Email` | `CustomerContact[]` 一对多 |
| 附件 | RobotAttachment 单态 | Attachment 多态（owner_type/id）|
| 状态转换 | 硬编码于 service 层 | 保持硬编码，扩到 21 stage |
| 控制门 | 4 个 service 方法 | 扩到 G1-G6 + 验证门，仍是 service 方法 |

---

## 10. 迁移路径（拆 PR）

**绝不一次大重写**。拆 5 个 PR，每个 PR 都能独立 merge + 部署 + 回退。

### PR1 — 立 L1b 字典 + 扩 enum（零侵入）

**目标**：先把"不依赖任何业务表"的底座搭好，不动现有 robot_manager 业务逻辑。

- 新建 `prisma/schema/platform_master.prisma`
  - L1b 字典：`Currency` / `Country` / `Region` / `UnitOfMeasure` / `LabelType`（独立表）+ `Dictionary(category, code, label)`（合并纯枚举：DeclarationType / TariffType / Industry / ServiceIssueType — 决策 6）
- 扩 `RobotStatus` enum → 21 个 stage（更名为 `RobotLifecycleStage`，旧值平迁），现有 10 个值兼容映射
- seed 字典初始数据（USD/CNY/EUR、CN/US/UAE、7 个 LabelType 等）
- **不动** robot_manager 业务表结构，现有功能不受影响

**风险**：低。回退 = 删表 + 回滚 enum migration。

### PR2 — 立 L1a 主数据 + 迁移 Customer/Supplier/Location

**目标**：把跨模块复用的主数据从 robot_manager 搬到 platform_master。

- 新建 `Customer / Supplier / Partner / Location / Attachment` + 各自 contacts/addresses 子表
- 写**数据迁移脚本**：`RobotCustomer → Customer` / `RobotSupplier → Supplier` / `RobotLocation → Location` / `RobotAttachment → Attachment`，保留 ID（uuid 不变）
- `RobotUnit` 的 `customerId/supplierId/locationId` FK 改指向 `platform_master.*`
- 删除 `RobotCustomer / RobotSupplier / RobotLocation / RobotAttachment` 表

**风险**：中。迁移脚本需测好。回退 = 反向迁移 + 改回 FK。**强制 L1c 数据快照验证**。

### PR3 — RobotUnit 拆分 + LifecycleEvent 全事件流 + 删除 RobotFieldDef

**目标**：把状态从 RobotUnit 移出，引入 `RobotLifecycleEvent` 和 `RobotUnitSnapshot`，**同时把 RobotFieldDef 的 31 个动态字段硬翻译进 prisma schema**。

- 新建 `RobotUnitSnapshot`（**物化表，决策 7**），同事务刷新
- `RobotStatusChangeLog → RobotLifecycleEvent`：扩 `eventType` 字段，加 `relatedType/Id`，老数据全量迁移（eventType=status_changed）
- `RobotUnit` 移除 `currentStatus / customerId / locationId` 字段（值挪到 snapshot）
- `RobotUnit` 新增 `placeholderSnOrig / supplierSn / manufactureDate / purchaseOrderId / purchaseOrderLineId / originalSupplierId / retiredAt`
- **把 `RobotFieldDef` 现有 31 个字段定义全部翻译成 prisma 强类型字段**：不变属性进 RobotUnit 骨架；流程产生字段先暂留 RobotUnit.metadata，PR4 把它们移到对应业务表
- service 层改造：所有状态变更走 LifecycleEvent，同步刷 snapshot
- frontend 列表/详情查询从 RobotUnit join snapshot
- 删除 `RobotFieldDef` 表 + 配套动态校验/渲染代码（统一改强类型）

**风险**：高（动 RobotUnit 是核心实体 + 删动态字段层）。**强制 L0a/L0b/L0c + L1 集成测试全跑 + L2 MCP 走改动页**。

### PR4 — 立 L3 业务表（PO / SO / Delivery / Payment / Service / QualityLabel）

**目标**：把 v5 mapping 暴露的业务实体补齐。

- 新建 8 张业务表
- `RobotUnit.purchaseOrderId/LineId` 在此 PR 之前先允许 null，本 PR 把 PO 数据迁入后回填
- `RobotServiceRecord → ServiceTicket + ServiceTicketActivity`（迁移）
- service 层加各业务表的 CRUD + 状态机
- frontend 加各业务表管理页（可分多个 sub-PR）

**风险**：中。新增不破坏现有。前端工作量大，**可拆 PR4a/4b/4c 按业务侧分开（采购 / 销售 / 售后）**。

### PR5 — 历史数据从 v5 Master 导入（一次性）

**目标**：把 v5 的 172 行历史数据导入 RobotUnit + 关联表 + **审计事件**（决策 10）。

- 写一次性导入脚本（不进 prisma migration，进 `scripts/data-migration/`）
- 每台机器人导入时**同时插入一条 `RobotLifecycleEvent`**：`eventType=imported_from_v5, occurredAt=migration_run_at, payload={source:'v5', master_row_id}`
- 在 UAT 环境验证后再生产跑
- 跑完归档脚本到 `scripts/data-migration/archive/`

**风险**：中。一次性脚本，跑前 dry-run + 跑后核对总数。

---

## 11. 决策记录

10 项关键决策已拍板（见 §2 表）。简表：

| # | 决策 | 拍板结果 |
|---|---|---|
| 1 | 分层数 | **3 层**（L1/L2/L3），默认不立 L4 元数据驱动 |
| 2 | RobotUnit 状态分离 | **拆**：不变属性留 + LifecycleEvent + Snapshot |
| 3 | 跨模块主数据位置 | **平台公共层 `platform_master`** |
| 4 | "人"维度 | 内部员工用 `User`；外部联系人挂主数据下 `contacts[]` |
| 5 | 跨模块写 RobotUnit | **走 LifecycleEvent**，不直改 prisma client |
| 6 | 字典策略 | **混合**：专属字段独立表 / 纯枚举合并 Dictionary |
| 7 | RobotUnitSnapshot | **物化表** + 同事务刷新 |
| 8 | SystemConfig 泛化 | **本期不动**，保留模块内 |
| 9 | Note/Tag 多态表 | **本期不立**，各表加 `notes String?` |
| 10 | v5 导入审计事件 | **写**：每台一条 `event_type=imported_from_v5` |

完整原则文档：[`docs/standards/16-data-layering-and-metadata-policy.md`](../../../standards/16-data-layering-and-metadata-policy.md)

---

## 12. 下一步

PR 顺序：PR1 字典 + enum 扩 → PR2 主数据迁移 → PR3 RobotUnit 拆 + 删 FieldDef → PR4 业务表 → PR5 历史导入。每个 PR 独立可 merge/部署/回退。

下一步动作：
1. 本文档 + standard 16 + CLAUDE.md/AGENTS.md 改动入 review（拆 2 个 PR：standard/CLAUDE/AGENTS 作为项目契约单独 PR；本模块业务文档单独 PR）
2. 两 PR merge 后，开 PR1 实施分支
