# 机器人全生命周期 - 数据模型 (v3 终态)

> **v3 终态（2026-05-17）**：放弃 v2 metadata JSONB / RobotFieldDef 元数据驱动方案，改为
> **3 层强类型 + CQRS 架构**：
> - L1 platform_master（5 字典 + 5 主数据 + Attachment 多态）
> - L2 robot_manager 核心（RobotUnit 仅不变属性 + RobotUnitSnapshot 物化当前状态 + RobotLifecycleEvent 事件源）
> - L3 robot_manager 业务表（PO/SO/Delivery/Payment/ServiceTicket/Rental + 5 个 per-robot 子表）
>
> 详细字段映射见 [最终字段设计.md](business-analysis/最终字段设计.md)。

> **module**: robot-manager
> **doc_type**: DataModel
> **status**: Active (v3)
> **owner**: FFOA Team
> **upstream_docs**: 01-prd.md, 04-state-machine.md
> **last_verified**: 2026-05-17

---

## v3 架构总览

```
┌──────────────────────────────────────────────────────────┐
│ L1 平台公共层 (platform_master schema)                   │
│ ─ L1a 主数据：Customer / Supplier / Partner / Location   │
│   + Attachment 多态                                      │
│ ─ L1b 字典：Currency / Country / GeoRegion / UoM         │
│   / Dictionary（label_type / tariff_type / service_      │
│   issue_type / declaration_type / industry 5 个 category）│
└────────────────┬─────────────────────────────────────────┘
                 │ 跨 schema FK（DB 层）
┌────────────────▼─────────────────────────────────────────┐
│ L2 模块主数据 (robot_manager schema 核心)                │
│ ─ RobotModel / RobotSku / RobotSystemConfig             │
│ ─ RobotUnit（仅不变属性：ffsn / model / usage / sap...） │
│ ─ RobotUnitSnapshot（1:1，CQRS read model，乐观锁）      │
│ ─ RobotLifecycleEvent（不可变事件流，CQRS write side）   │
└────────────────┬─────────────────────────────────────────┘
                 │ FK + event 投影
┌────────────────▼─────────────────────────────────────────┐
│ L3 业务表 (robot_manager schema)                         │
│ ─ PurchaseOrder + Line                                   │
│ ─ SalesOrder + Line                                      │
│ ─ DeliveryRequest + DeliveryFulfillment                  │
│ ─ PaymentRecord                                          │
│ ─ ServiceTicket + Activity                               │
│ ─ RentalAgreement + RentalPaymentSchedule                │
│ ─ Per-robot 子表 (5)：QualityLabel / PackageReadiness /  │
│   InspectionRecord / LogisticsLeg / ComplianceCheck      │
│ ─ RobotImportAudit（v5 历史导入用）                       │
└──────────────────────────────────────────────────────────┘
```

## RobotUnit (仅不变属性)

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | UUID | PK |
| `organizationId` | UUID | DataScope |
| `ffsn` | String unique@org | FF 内部主键 |
| `ffsnDisplay` / `placeholderSnOrig` / `supplierSn` | String? | 占位 / 显示 / 供方 SN |
| `modelId` / `skuId` | UUID FK | 物料 |
| `usageType` | enum `RobotUsageType` | SALES / RND / MARKETING / CAPITAL / PRODUCT / TO_AGIBOT / LAUNCH_EVENT |
| `purchaseOrderId` / `purchaseOrderLineId` | UUID FK? | 采购追溯 |
| `originalSupplierId` | UUID FK? | 出厂供应商（→ platform_master.suppliers）|
| `manufactureDate` | Date? | |
| `retiredAt` / `disposalType` / `disposalNotes` | — | CLOSED 时填 |
| `sapMaterialNo` | String? | SAP 集成 anchor |
| `version` | Int | 乐观锁 |

可变状态全部走 [RobotUnitSnapshot](#robotunitsnapshot-物化当前状态) + [RobotLifecycleEvent](#robotlifecycleevent-不可变事件流)。

## RobotUnitSnapshot (物化当前状态)

CQRS read model，1:1 RobotUnit。由 LifecycleEvent 投影刷新（同事务）。乐观锁 version 防并发。

关键字段：`currentStage` / `isHeld` / `holdReason` / `currentLocationId` / `currentCustomerId` / `currentSalesOrderId` / `currentDeliveryRequestId` / `currentSpecialistId` / `physicalProductStatus` / `daysReadyForDelivery` / `warrantyStatus` / `lastEventId` / `lastEventAt` / `version`。

## RobotLifecycleEvent (不可变事件流)

CQRS write side，append-only。

15 种 `eventType`：
- `stage_changed`（最常见）
- `held` / `unheld`
- `location_moved`
- `sn_activated` / `usage_type_changed`
- `label_applied` / `inspection_logged` / `readiness_completed`
- `payment_collected` / `delivery_signed`
- `service_opened` / `service_closed`
- `imported_from_v5`
- `note_added`

每种事件 → Snapshot 字段刷新 map：见 [最终字段设计.md §5.5](business-analysis/最终字段设计.md#55-事件--snapshot-字段投影表service-层实现该-map)。

## L3 业务表清单

| 表 | 用途 | 关键字段 |
|---|---|---|
| `PurchaseOrder` + Line | 采购单（节点 01）| poNo / supplierId / sapPoNo / lines.skuId/quantity/unitPrice |
| `SalesOrder` + Line | 销售单（节点 12）| soNo / customerId / d365SoId / contractStatus / lines.lineType(HW/SW/COCREATION) |
| `DeliveryRequest` | 交付申请（节点 14 前）| deliveryNo / salesOrderId / requestType(WILL_CALL/3PL/WHITE_GLOVE) |
| `DeliveryFulfillment` | 交付履约（节点 15 PGI）| sapPgiDocNo / signedFormStatus / acceptanceFormStatus / cost / revenue / invoiceStatus |
| `PaymentRecord` | 付款记录（双向 INBOUND/OUTBOUND）| paymentNo / relatedType+Id / paymentMethod(8 种) / paymentStatus(5 种) |
| `ServiceTicket` + Activity | 售后工单 | ticketNo / severity(P0-P3) / issueTypeCode(Dict) / 自动写 service_opened/closed event |
| `RentalAgreement` + Schedule | 租赁合约 + 月分期 | startAt/endAt → 自动生成 N 期 schedule |
| `QualityLabelRecord` | 质量标签（7 行/台 节点 09）| labelTypeCode(Dict) / status(PENDING/APPLIED/VERIFIED/REJECTED) |
| `RobotPackageReadiness` | 入库就绪（1 行/台 节点 11）| 10 件配件 boolean / specialistId / 10 件齐齐自动 readiness_completed event |
| `InspectionRecord` | 入库检查（0-N 节点 08）| inspectionNo / inspectorId / issue / issueTagCode |
| `LogisticsLeg` | 物流段（1-N 节点 04）| legNo / fromLocation/toLocation / logisticsStatus(5 状态) |
| `ComplianceCheck` | 合规（1:1 节点 03/05/06）| dateReady / stickerStatus / fccStatus |
| `RobotImportAudit` | v5 导入审计（1:1 仅 v5 导入的）| sources / conflictCount / recordStatus |

外部系统 anchor 列：所有 SAP/D365 同步靶向字段（`sapPoNo` / `sapPgiDocNo` / `sapMaterialNo` / `sapClearingDocNo` / `d365SoId`）已就位，未来集成不改 schema。

## 跨 schema 引用策略

`RobotUnit.originalSupplierId` / `Snapshot.currentLocationId/currentCustomerId/...` 这种跨 schema FK：
- DB 层建 FK constraint（PR2 实施时补全 organization_id / created_by_id 的 FK 等）
- Prisma 层**不写 @relation**（避免类型推导污染）
- Service 层显式批量 join 避免 N+1

详见 [standard 16 §1.5](../../standards/16-data-layering-and-metadata-policy.md)。

---

## 历史 v2 数据模型（参考）
- **1 个 Prisma enum**：`RobotStatus`（状态机不可配置化）
- **系统配置 `RobotSystemConfig`**：FFSN 规则 + 状态默认 location + REPAIR 自动 ServiceType

---

## 实体关系概览

```
RobotFieldDef (31 rows)      定义 metadata 的 key/type/group/校验规则
       |
       v
RobotUnit                     骨架列 + metadata JSONB
  ├─→ RobotModel             (FK: modelId)
  ├─→ RobotSku               (FK: skuId, 挂在 Model 下)
  ├─→ RobotSupplier          (FK: supplierId, 可空)
  ├─→ RobotCustomer          (FK: customerId, 可空)
  ├─→ RobotLocation          (FK: locationId, 可空)
  ├── RobotServiceRecord[]   (1:N)
  └── RobotStatusChangeLog[] (1:N)

RobotOption                   统一字典（10 categories）
RobotSystemConfig             系统配置
```

---

## RobotUnit 列定义

| 列名 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | |
| organizationId | UUID | 组织隔离 |
| ffsn | String | FF 序列号，系统生成 |
| currentStatus | RobotStatus enum | 核心状态机 |
| modelId | UUID FK → RobotModel | |
| skuId | UUID FK → RobotSku | |
| supplierId | UUID? FK → RobotSupplier | |
| customerId | UUID? FK → RobotCustomer | |
| locationId | UUID? FK → RobotLocation | |
| metadata | JSONB default `{}` | **所有业务字段存储在此** |
| version | Int default 0 | 乐观锁 |
| createdAt/updatedAt | Timestamptz | |
| createdBy/updatedBy | UUID | |
| deletedAt | Timestamptz? | 软删除 |

---

## RobotFieldDef（动态字段定义）

| 列名 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | |
| key | String unique | metadata 中的 JSON key |
| label | String | 中文标签 |
| labelEn | String? | 英文标签 |
| type | String | text / number / date / datetime / money / select / url / textarea / bool |
| group | String | identity / supply-chain / sales / finance / compliance / after-sales |
| sortOrder | Int | 组内排序 |
| required | Boolean | 是否必填 |
| dictCategory | String? | 仅 type=select 时有效，指向 RobotOption.category |
| validation | JSON | 扩展校验（min/max/pattern/maxLength） |
| showInList | Boolean | 列表页是否默认显示 |
| indexed | Boolean | 预留：是否建表达式索引 |
| enabled | Boolean | 禁用 = 软删除 |

### 默认 31 个字段分布

| Group | 字段数 | 关键字段 |
|-------|--------|----------|
| identity | 5 | supplierSn, trackingId, importTypeCode, usageTypeCode, issueTagCode |
| supply-chain | 6 | poNumber, purchaseDate, purchasePrice, eta, arrivalDate, logisticsStatus |
| sales | 10 | salesOrderId, salesPriceHardware, salesPriceSoftware, contractStatusCode, deliveredDate... |
| finance | 3 | cost, revenueRecognizedAt, invoiceStatus |
| compliance | 4 | fccStatus, importDeclarationType, tariffType, complianceNotes |
| after-sales | 3 | warrantyStatusCode, serviceRecordsNote, customerFeedback |

---

## 辅助实体（不变）

### RobotModel / RobotSku / RobotSupplier / RobotCustomer / RobotLocation

与 v2 相同，不赘述。

**关于 `code` 字段**：数据库层仍为 `@unique` 非空，但 API 层 `CreateXxxInput.code` 为可选。用户通过管理台创建时若留空，service 层按前缀自动生成（`MDL` / `SKU` / `SUP` / `CST` / `LOC`），避免业务人员为不对外流通的实体（如内部仓库位置）强制编号。

**Excel 导入匹配规则**（v3）：模板里 FK 列**只接 code 不接 name**（详见 14-import-export-tool-prd.md §4.3 Sheet 规格）。v2 时代曾支持 "name 兜底"，v3 重做时回退为严格 code 匹配 + Excel cell comment 引导 + data validation 下拉，避免歧义。

### RobotOption（统一字典）

10 个 category：USAGE_TYPE / IMPORT_TYPE / DELIVERY_STATUS / CONTRACT_STATUS / PAYMENT_METHOD / PAYMENT_STATUS / WARRANTY_STATUS / SERVICE_TYPE / ISSUE_TAG / LOCATION_TYPE

### RobotSystemConfig

| key | 说明 |
|-----|------|
| ffsn_rule | FFSN 生成规则（prefix/dateFormat/seqLength/resetPeriod） |
| status_default_location | 状态→默认 locationCode 映射 |
| repair_auto_service_type | 进入 REPAIR 时自动创建的 ServiceRecord 类型 |

---

## 索引策略

- 骨架列：Prisma 自动建索引（organizationId + currentStatus/modelId/customerId/supplierId/locationId/deletedAt）
- JSONB 热点字段：预留 `RobotFieldDef.indexed=true`，可按需建表达式索引：
  ```sql
  CREATE INDEX CONCURRENTLY idx_robot_units_meta_cost
    ON robot_manager.robot_units (((metadata->>'cost')::numeric));
  ```

---

## 财务计算公式

```
salesPrice = metadata.salesPriceHardware + metadata.salesPriceSoftware
grossMargin = salesPrice - metadata.cost
```

后端 `enrichResponse()` 在返回时自动计算 `grossMargin`。
报表 SQL 使用 `(metadata->>'salesPriceHardware')::numeric` 等 JSONB 表达式。

---

## ImportBatch / ImportBatchEntry（v3 Import 工具，2026-05-18 加）

> 详细字段定义 + DataScope + 跨 schema FK 处理 + 生命周期策略见
> [14-import-export-tool-prd.md §5](14-import-export-tool-prd.md)（事实源）。
> 本节摘要核心结构，避免重复维护。

### ImportBatch — 通用导入批次

| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID PK | |
| type | enum ImportBatchType | PURCHASE_ORDER / ROBOT_UNIT / MASTER_* / SERVICE_TICKET |
| status | enum ImportBatchStatus | PENDING / VALIDATING / VALIDATED / IMPORTING / COMPLETED / FAILED / SUPERSEDED |
| fileName | VarChar(255) | sanitize 后入库 |
| fileHash | VarChar(64) | SHA256 流式 hash，防 TOCTOU + 同文件 idempotent |
| templateSchemaHash | VarChar(16) | 防模板/schema 漂移 |
| totalRows / successRows / errorRows / warningRows | Int | preview 后填充 |
| startedAt / completedAt | Timestamptz | |
| **confirmedById** | UUID (FK → platform_iam.users.id) | 谁点 confirm（可能 ≠ createdById，admin 也能） |
| confirmedAt | Timestamptz | |
| clientIp / userAgent | VarChar | 审计回放 |
| errorSummary | JSONB | top-level 错误摘要 |
| organizationId / createdAt / updatedAt / createdById / deletedAt | 标准字段 | |

**DataScope**：`@DataScope('robot-manager:import-batch', { default: SELF, override: { admin: ORGANIZATION } })`

### ImportBatchEntry — 每行处理记录

| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID PK | |
| batchId | UUID FK → ImportBatch | onDelete Cascade |
| rowNo | Int | Excel 行号 |
| status | enum ImportEntryStatus | OK / ERROR / WARNING |
| entityIds | UUID[] | 支持一行扇出多 entity；ERROR 行为空 |
| payload | JSONB | 原始 Excel 行（**PII 字段 mask 后存**）；**confirm 90 天后 null 化**；包含 confirmedAt 后 30 天的归档窗口 |
| payloadHash | VarChar(64) | SHA256(canonical payload)，防离线篡改 |
| errorDetail | JSONB | `{ field, code, params }[]`；**禁止存预渲染 message**（i18n key 由前端按 locale 渲染） |
| createdAt / deletedAt | 标准字段 | |

### 跨 schema FK 处理

按 [standard 04 §原则 2「跨 schema 不建 Prisma @relation」](../../standards/04-database-architecture.md#原则-2--跨-schema-不建-prisma-relation-🔴-铁律)，
仅存 UUID + service 层 enrich：
- `ImportBatch.createdById` → `platform_iam.users.id`
- `ImportBatch.confirmedById` → `platform_iam.users.id`

robot_manager.prisma 顶部「跨 schema FK 清单」注释段需同步加这两条（M1 PR-A）。

### 不动的表

- `RobotImportAudit`：保留给 v5 历史导入，1:1 RobotUnit 语义不变，新 import 路径**不写入**
- `ImportRecordStatus` enum：保留给 RobotImportAudit

