# 机器人全生命周期 - API 参考 (v3 终态)

> **v3 终态（2026-05-17）**：service 层 28 stage 状态机 + 13 Guard 函数 + CQRS 事件流。
> 主要变化：
> - `POST /:id/status` → 改为 `POST /:id/change-stage`，参数从 `targetStatus` 改为 `toStage`
> - 新增 `/:id/hold` / `/:id/unhold` / `/:id/move-location` / `/:id/events` / `/:id/allowed-next-stages`
> - 新增 L3 CRUD：`/purchase-orders` / `/sales-orders` / `/delivery-requests` / `/delivery-fulfillments` / `/payments` / `/service-tickets` / `/rentals`
> - 新增 per-robot 子资源：`/:robotId/quality-labels` / `/readiness` / `/inspections` / `/logistics-legs` / `/compliance`
> - 删除 `/:id/{identity,supply-chain,sales,finance,after-sales,compliance}` section 端点
> - **`/excel/*` 已删（v3 重构 commit a40d5ca2）**；v3 重做的 import 工具见下方 §5「数据导入工具」（M1 仅 `purchase-order`，M2/M3 扩展）

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

---

## v3 端点清单

### 核心 RobotUnit (`/api/v1/robot-manager`)

| Method | Path | 权限 | 说明 |
|---|---|---|---|
| GET | `/robot-manager` | read | 列表，支持 `currentStage` / `currentLocationId` / `currentCustomerId` / `isHeld` 等 v3 过滤参数 |
| POST | `/robot-manager` | create | 创建 unit + 自动建 Snapshot + 写第一条 `stage_changed` event |
| GET | `/robot-manager/:id` | read | 详情含 snapshot + 最近 50 条 events |
| PUT | `/robot-manager/:id` | update | 只更新不变属性（supplierSn / usageType / disposalType 等）|
| DELETE | `/robot-manager/:id` | delete | 软删 |

### Lifecycle 编排 (`/api/v1/robot-manager/:id/...`)

| Method | Path | 权限 | 说明 |
|---|---|---|---|
| POST | `/:id/change-stage` | change-status | body `{ toStage, reason?, version? }`；走 STAGE_TRANSITIONS 白名单 + 13 Guard + 同事务写 event + 刷 snapshot；版本冲突抛 409 `ROBOT_VERSION_CONFLICT`（08-error-codes.md） |
| POST | `/bulk-change-stage` | change-status | 批量；body `{ robotUnitIds[], toStage, reason? }` |
| POST | `/:id/hold` | change-status | body `{ holdReason }`；写 `held` event + snapshot.isHeld=true |
| POST | `/:id/unhold` | change-status | 写 `unheld` event |
| POST | `/:id/move-location` | change-status | body `{ toLocationId, reason? }`；写 `location_moved` event |
| GET | `/:id/events` | read | 事件流（默认最近 50）|
| GET | `/:id/allowed-next-stages` | read | 当前 stage 合法下一步列表（前端按钮 disable 用）|

### L3 业务表

**PurchaseOrder** (`/api/v1/robot-manager/purchase-orders`)
- GET list (filters: status, supplierId, search) / GET :id / POST (含 lines[]) / PUT :id / DELETE :id

**SalesOrder** (`/api/v1/robot-manager/sales-orders`)
- GET list / GET :id / POST (含 lines[] 区分 HARDWARE/SOFTWARE/COCREATION) / PUT :id / DELETE :id

**Delivery**:
- `GET/POST /delivery-requests`、`PUT /delivery-requests/:id` — 交付申请 CRUD
- `POST /delivery-fulfillments` — 创建履约（PGI 时机），**同事务写 `delivery_signed` event + 刷 Snapshot.warrantyStatus=ACTIVE + currentCustomerId**
- `PUT /delivery-fulfillments/:id` — 改 PGI 表单状态 / cost / 收入确认 / 发票状态

**Payment** (`/api/v1/robot-manager/payments`)
- GET list (filters: relatedType/Id, robotUnitId, direction, status)
- POST 创建（PAID 时关联 robot 自动写 `payment_collected` event）
- POST `/:id/mark-paid` body `{ paidAt }` — 标 PAID + 写 event

**ServiceTicket** (`/api/v1/robot-manager/service-tickets`)
- GET list (status / severity / robotUnitId) / GET :id
- POST 创建 — 自动写 `service_opened` event
- PUT :id — 状态切到 CLOSED 时自动写 `service_closed` event + closedAt
- POST `/:id/activities` body `{ activityType, payload? }` — 工单活动记录

**Rental** (`/api/v1/robot-manager/rentals`)
- GET list / GET :id（含 schedules）
- POST 创建 — 自动生成 N 期 RentalPaymentSchedule
- POST `/:id/terminate` — 改 status=TERMINATED

### Per-robot 子资源 (`/api/v1/robot-manager/:robotId/...`)

| Method | Path | 说明 |
|---|---|---|
| GET / PUT | `/:robotId/quality-labels` | 7 行/台；PUT upsert 单条；APPLIED/VERIFIED 写 `label_applied` event |
| GET / PUT | `/:robotId/readiness` | 1 行/台；10 件配件齐齐 + completedAt 时写 `readiness_completed` event |
| GET / POST | `/:robotId/inspections` | 0-N 行；POST 写 `inspection_logged` event |
| GET / POST | `/:robotId/logistics-legs` | 1-N 段物流 |
| GET / PUT | `/:robotId/compliance` | 1:1；upsert dateReady / stickerStatus / fccStatus / complianceNotes |

### Admin（Model/SKU/Config）

`/api/v1/robot-manager/admin/{models, skus, config}/...` — 标准 CRUD。
Customer / Supplier / Location / Partner / Attachment 的 admin 端点已迁到 [platform-master API](../platform-master/07-api.md)。

### Reports

| Method | Path | 说明 |
|---|---|---|
| GET | `/robot-manager/reports/inventory` | 库存按 v3 stage 聚合：`byStage` + `inWarehouse` + `available` + `inTransit` + `total` |
| GET | `/robot-manager/reports/sales` | 销售按 DeliveryFulfillment 聚合：`totalDelivered` + `totalRevenue` + `customerBreakdown[]` + `monthlyTrend: Array<{ month: 'YYYY-MM', count: number, revenue: number }>` |
| GET | `/robot-manager/reports/finance` | 财务：`revenue`（PAID inbound）/ `totalCost` / `margin` |

### Search

`GET /robot-manager/search?q=xxx` — 跨实体（unit / model / sku / supplier / customer / location）模糊搜索。

---

## 历史 v2 API（参考）

- **基地址**：`/api/v1/robot-manager`
- **认证**：所有端点（除 OPTIONS）需 JWT，`Authorization: Bearer <token>`
- **响应包装**：

```json
{
  "success": true,
  "data": <实际数据>,
  "message": "success",
  "timestamp": "2026-04-14T03:51:37.283Z",
  "path": "/api/v1/robot-manager/..."
}
```

错误响应：

```json
{
  "success": false,
  "error": { "code": "FORBIDDEN", "message": "..." },
  "timestamp": "..."
}
```

- **`:id`** 路径参数使用 UUID（v3/v4/v5 均可，NestJS `ParseUUIDPipe` 默认校验），非法 UUID 返回 400。所有 `id` 实际生成为 v4。
- **乐观锁**：所有 PUT 端点接受 `version` 字段，不匹配返回 409
- **不可变字段**：`metadata.poNumber` / `metadata.salesOrderId` 一旦设置不可修改，否则返回 400
- **分页**：list 类端点使用 `page` / `limit`，默认 1 / 20
- **数值字段**：metadata 中的 money/number 字段以 **number** 形式存储和返回（不再是 Prisma Decimal string）。`grossMargin` 由后端 `enrichResponse()` 计算，以 **number** 返回。业务实体的 `defaultPrice`/`defaultCost`/`creditLimit` 仍为 Prisma Decimal（string 序列化）。

---

## 1. 核心 RobotUnit

### 1.1 列表

```
GET /api/v1/robot-manager
```

权限：`robot-manager:read`

**Query parameters**：

| 参数 | 类型 | 说明 |
|---|---|---|
| `search` | string | 模糊搜索 ffsn |
| `currentStatus` | RobotStatus 或 string[] | 单值或数组 |
| `modelId` | UUID | 按型号过滤 |
| `skuId` | UUID | 按 SKU 过滤 |
| `supplierId` | UUID | 按供应商过滤 |
| `customerId` | UUID | 按客户过滤 |
| `locationId` | UUID | 按位置过滤 |
| `includeDeleted` | boolean | 是否包含软删除 |
| `page` | int | 页码（默认 1） |
| `limit` | int | 每页（默认 20） |
| `sortBy` | string | 排序字段（默认 `createdAt`） |
| `sortOrder` | `asc`/`desc` | 默认 `desc` |

**响应**：

```json
{
  "items": [
    {
      "id": "uuid",
      "ffsn": "FF-202604-00001",
      "currentStatus": "IN_STOCK",
      "model": { "id": "uuid", "code": "AEGIS", "name": "Aegis 系列" },
      "sku": { "id": "uuid", "code": "AEGIS-STD", "name": "Aegis Standard", "variant": "Standard" },
      "supplier": { "id": "uuid", "code": "AGBOT", "name": "Agbot Inc." },
      "customer": null,
      "location": { "id": "uuid", "code": "HQ_FZ", "name": "福州总部仓库", "typeCode": "INTERNAL" },
      "metadata": {
        "salesPriceHardware": 50000,
        "cost": 30000,
        "poNumber": "PO-2026-001",
        ...
      },
      "grossMargin": 20000,
      "version": 0,
      ...
    }
  ],
  "total": 6,
  "page": 1,
  "limit": 20,
  "totalPages": 1,
  "hasNext": false,
  "hasPrev": false
}
```

### 1.2 详情

```
GET /api/v1/robot-manager/:id
```

权限：`robot-manager:read`

**响应**：单个 RobotUnit，包含所有 joined 实体 + `serviceRecords` + `statusChangeLogs`。

`statusChangeLogs[i].changedByUser` 字段会自动富化为 `{ id, displayName, username }`。

### 1.3 创建

```
POST /api/v1/robot-manager
```

权限：`robot-manager:create`

**Body**（`CreateRobotUnitDto`）：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `modelId` | UUID | ✅ | 骨架 FK |
| `skuId` | UUID | ✅ | 骨架 FK |
| `supplierId` | UUID | | 骨架 FK |
| `customerId` | UUID | | 骨架 FK |
| `locationId` | UUID | | 骨架 FK |
| `metadata` | object | | 所有业务字段，key 由 FieldDef 定义 |

**metadata 示例**：
```json
{
  "supplierSn": "SN-001",
  "poNumber": "PO-2026-001",
  "purchasePrice": 30000,
  "salesPriceHardware": 50000,
  "cost": 28000,
  "usageTypeCode": "DEMO"
}
```

metadata 中的字段由 `RobotFieldDef` 驱动校验（type + required + dictCategory）。

**响应**：创建的 RobotUnit。FFSN 由系统按 `RobotSystemConfig.ffsn_rule` 自动生成。`currentStatus` 默认 `ORDERED`，自动写入一条初始 StatusChangeLog。`grossMargin` 由后端计算（salesPriceHardware + salesPriceSoftware - cost）。

### 1.4 全量更新（legacy）

```
PUT /api/v1/robot-manager/:id
```

权限：`robot-manager:update`（仅 RLE / Administrator）

**Body**（`UpdateRobotUnitDto`）：骨架 FK 可选 + `metadata` 对象 + `version` 乐观锁。

**推荐**：使用 Section 级端点替代（见 §2）。

### 1.5 软删除

```
DELETE /api/v1/robot-manager/:id
```

权限：`robot-manager:delete`

**响应**：`{ "message": "Robot unit deleted successfully" }`

---

## 2. Section 级更新（Phase 3 字段级权限）

每个 section 端点只接受对应 group 的 FieldDef 字段 + 骨架 FK。白名单由 `RobotFieldDef` 动态生成（group 过滤）。所有 section 端点都接受 `version` + `metadata`。

### 2.1 身份 section

```
PUT /api/v1/robot-manager/:id/identity
```

权限：`robot-manager:write:identity`

**Body**：`modelId?`, `skuId?`, `version`, `metadata: { supplierSn?, usageTypeCode?, importTypeCode?, trackingId?, issueTagCode? }`

### 2.2 供应链 section

```
PUT /api/v1/robot-manager/:id/supply-chain
```

权限：`robot-manager:write:supply-chain`

**Body**：`supplierId?`, `version`, `metadata: { poNumber?, purchaseDate?, purchasePrice?, eta?, arrivalDate?, deliveryStatusCode?, logisticsStatus? }`

### 2.3 销售 section

```
PUT /api/v1/robot-manager/:id/sales
```

权限：`robot-manager:write:sales`

**Body**：`customerId?`, `version`, `metadata: { salesOrderId?, salesPriceHardware?, salesPriceSoftware?, contractStatusCode?, contractLink?, deliveredDate?, ... }`

### 2.4 财务 section

```
PUT /api/v1/robot-manager/:id/finance
```

权限：`robot-manager:write:finance`

**Body**：`version`, `metadata: { cost?, paymentMethodCode?, paymentStatusCode?, revenueRecognizedAt?, invoiceStatus? }`

### 2.5 售后 section

```
PUT /api/v1/robot-manager/:id/after-sales
```

权限：`robot-manager:write:after-sales`

**Body**：`version`, `metadata: { warrantyStatusCode?, serviceRecordsNote?, customerFeedback? }`

### 2.6 合规 section

```
PUT /api/v1/robot-manager/:id/compliance
```

权限：`robot-manager:write:compliance`

**Body**：`version`, `metadata: { fccStatus?, importDeclarationType?, tariffType?, complianceNotes? }`

---

## 3. 状态变更

### 3.1 单设备状态变更

```
POST /api/v1/robot-manager/:id/status
```

权限：`robot-manager:change-status`（SupplyChain / Sales / RLE / Administrator）

返回码：`200 OK`（非 201）

**Body**（`StatusChangeDto`）：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `targetStatus` | RobotStatus | ✅ | 目标状态 |
| `remark` | string | 仅 CANCELLED 时必填 | 备注 |
| `locationId` | UUID | | 覆盖默认 location 联动 |

后端按状态机校验合法转换 + Guard 条件，必要时执行副作用（创建 ServiceRecord、清空客户、补齐 deliveredDate 等）。详见 [04-state-machine.md](04-state-machine.md)。

**响应**：更新后的完整 RobotUnit（包含 joined `model / sku / supplier / customer / location` + `serviceRecords[]` + `statusChangeLogs[]`，shape 与 §1.2 详情端点一致）。

### 3.2 批量状态变更

```
POST /api/v1/robot-manager/bulk-status-change
```

权限：`robot-manager:change-status`

返回码：`200 OK`

**Body**（`BulkStatusChangeDto`）：

```json
{
  "robotUnitIds": ["uuid1", "uuid2", "uuid3"],
  "targetStatus": "IN_STOCK",
  "remark": "optional"
}
```

**响应**：

```json
{
  "success": ["uuid1", "uuid2"],
  "failed": [{ "id": "uuid3", "reason": "非法状态转换：DELIVERED → IN_STOCK" }]
}
```

每台独立校验，失败的不影响其他。

---

## 4. 报表

### 4.1 库存报表

```
GET /api/v1/robot-manager/reports/inventory
```

权限：`robot-manager:read`

**响应**：

```json
{
  "byStatus": { "ORDERED": 1, "IN_TRANSIT": 1, "IN_STOCK": 1, ... },
  "physical": 2,        // IN_STOCK + RESERVED
  "available": 1,       // IN_STOCK
  "inTransit": 1,       // IN_TRANSIT + BONDED
  "total": 6
}
```

### 4.2 销售报表

```
GET /api/v1/robot-manager/reports/sales
```

权限：`robot-manager:read`

**响应**：

```json
{
  "totalDelivered": 1,
  "totalRevenue": 82000,
  "customerBreakdown": [
    { "customerName": "Tesla Inc.", "count": 1, "revenue": 82000 }
  ]
}
```

仅统计 `currentStatus = DELIVERED` 的设备。

### 4.3 财务报表

```
GET /api/v1/robot-manager/reports/finance
```

权限：`robot-manager:read`

**响应**：

```json
{
  "unitCount": 2,
  "revenue": 232000,
  "totalCost": 138000,
  "margin": 94000
}
```

**v2 简化口径**：`margin = SUM(salesPrice - cost)`，**任何状态都计入**，仅当 `salesPrice` 和 `cost` 都非 null 才计入。软删除不计入。

---

## 5. 数据导入工具（v3 — M1: PO；M2+ 扩展）

> 详细 PRD 见 [14-import-export-tool-prd.md](14-import-export-tool-prd.md)。本节是 v3 实际 endpoint 概览（仅 PO 类型已实施，其他类型在 M2/M3）。

### 5.0 设计概览

- **8 种 importer type**（kebab-case URL）：`purchase-order` / `robot-unit` / `master-model` / `master-sku` / `master-customer` / `master-supplier` / `master-location` / `service-ticket`
- **2 阶段流**：`POST .../preview` 上传 + 三层校验（解析 / 引用 / 业务规则） → `POST .../batches/:id/confirm` all-or-nothing 写库
- **状态机**：`PENDING → VALIDATING → VALIDATED → IMPORTING → COMPLETED/FAILED`，`SUPERSEDED` 用于同 user 同 fileHash 24h 内重新上传去重
- **per-user in-flight ≤ 1**（PRD §3.3 §7.2）；同 fileHash 24h idempotent
- **统一响应 wrapper**：`{ success: true, data: ... }` / `{ success: false, error: { code, message, ... } }`
- **错误码**全部 SCREAMING_SNAKE，详见 [08-error-codes.md §Import](08-error-codes.md)

### 5.1 模板下载

```
GET /api/v1/robot-manager/import/:type/template?locale=zh|en
```

权限：`robot-manager:read`

**Path**：`:type` ∈ {`purchase-order`, ...}（M1 仅实现 `purchase-order`）
**Query**：`locale` ∈ `zh` / `en`（默认 `zh`）
**响应**：`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- 文件名 `{type}-template-{schemaHash8}.xlsx`
- Header `X-Template-Schema-Hash: <16 字符 hash>`（用于检测前后端模板漂移）
- Header `Cache-Control: public, max-age=3600`
- 3 sheets：`Data`（表头）/ `字段说明 / Fields` / `示例 / Examples`

### 5.2 上传 + Preview 校验

```
POST /api/v1/robot-manager/import/:type/preview
```

权限：`robot-manager:create` | `@Auditable()`
**Content-Type**：`multipart/form-data`
**Body**：`file` 字段（.xlsx, ≤ 10 MB, ≤ 1000 行）
**护栏**：
- 文件超 10 MB → `IMPORT_FILE_TOO_LARGE`
- 行数 > 1000 → `IMPORT_ROW_LIMIT`
- buffer 不是合法 xlsx → `IMPORT_FILE_INVALID` / 空 sheet → `IMPORT_SHEET_MISSING` / 0 行 → `IMPORT_FILE_EMPTY`
- 同 user 已有 in-flight batch → `IMPORT_CONCURRENT_BATCH`
- 同 user 同 fileHash 24h 内已上传 → 返回已有 batch（`summary.deduped=true`），不重复校验

**响应**（201 Created）：

```json
{
  "success": true,
  "data": {
    "batchId": "uuid",
    "summary": {
      "totalRows": 10,
      "successRows": 8,
      "errorRows": 2,
      "warningRows": 0,
      "templateSchemaHash": "abc123def456...",
      "fileHash": "sha256...",
      "status": "VALIDATED",
      "deduped": false
    },
    "errorPreview": [
      {
        "rowNo": 3,
        "errorDetail": [
          { "rowNo": 3, "field": "skuCode", "code": "IMPORT_FK_NOT_FOUND",
            "params": { "row": "3", "field": "SKU 代码", "value": "X", "table": "SKU" },
            "severity": "ERROR" }
        ]
      }
    ]
  }
}
```

- `errorPreview` 是 top-50 ERROR 行明细，前端不需要再 GET `/batches/:id` 拉详情
- 完整 entries 通过 `GET /batches/:id` 拉（含 OK / WARNING 行 payload）

### 5.3 Confirm 写库

```
POST /api/v1/robot-manager/import/batches/:batchId/confirm
```

权限：`robot-manager:create` | `@Auditable()`
**前置**：batch.status = `VALIDATED` 且 batch.errorRows = 0
**机制**：
- **CAS 锁**：`UPDATE WHERE status=VALIDATED AND createdById=user RETURNING` → 命中则 status → `IMPORTING`，未命中抛 `IMPORT_BATCH_ALREADY_CONFIRMED`（既覆盖重复 confirm 也覆盖跨 user 越权）
- **重跑校验**防 TOCTOU：confirm 时再跑一遍 `validateReferences` + `validateBusinessRules`，drift 抛 `IMPORT_REFS_CHANGED_RETRY`（含 issue 列表）
- **all-or-nothing**：`prisma.$transaction` 包整批 OK 行写库；任一失败回滚 + `markFailed` + 抛 `IMPORT_EXECUTE_FAILED`

**响应**（201 Created）：

```json
{
  "success": true,
  "data": {
    "batchId": "uuid",
    "status": "COMPLETED",
    "completedAt": "2026-05-18T10:30:00Z",
    "successRows": 10,
    "sideEffects": { "recordCount": 3, "notes": "created 3 PO with 10 lines" }
  }
}
```

### 5.4 Batch 详情 + 列表

```
GET /api/v1/robot-manager/import/batches/:batchId
GET /api/v1/robot-manager/import/batches?type=<type>&scope=mine|all&page=1&limit=20
```

权限：`robot-manager:read`
**IDOR 防护**：非 admin 仅能看自己创建的 batch；找不到返 `404 IMPORT_BATCH_NOT_FOUND`（不返 403 防探测）
**`scope=all`** 仅 admin 可用
**Limit cap**：20 默认 / 50 上限

详情响应含 `entries: ImportBatchEntry[]`（rowNo / status / payload / errorDetail / entityIds）。

### 5.5 错误报告下载

```
GET /api/v1/robot-manager/import/batches/:batchId/error-report.xlsx
```

权限：`robot-manager:read`
**响应**：`.xlsx` 文件流，仅含 ERROR 行 + 完整 errorDetail，含 CSV/Formula injection 防御（`=` / `+` / `-` / `@` 起头单元格前置 `'`）
**前置**：batch.errorRows > 0，否则抛 `IMPORT_BATCH_NO_ERRORS`

### 5.6 Importer 实现状态

| Type | 状态 | 文件 |
|---|---|---|
| `purchase-order` | ✅ M1 已实现 | `backend/src/modules/robot-manager/import/purchase-order/purchase-order.importer.ts` |
| `robot-unit` | 🟡 M2 | — |
| `master-{model,sku,customer,supplier,location}` | 🟡 M3a | — |
| `service-ticket` | 🟡 M3b | — |

未实现 type 调 preview / confirm 会抛 `IMPORT_TYPE_NOT_IMPLEMENTED`。

---

## 6. Admin（管理台）

### 6.1 RobotFieldDef（字段定义 + 内联字典，v5 统一）

```
GET    /api/v1/robot-manager/admin/field-defs
GET    /api/v1/robot-manager/admin/field-defs/:id
POST   /api/v1/robot-manager/admin/field-defs
PUT    /api/v1/robot-manager/admin/field-defs/:id
DELETE /api/v1/robot-manager/admin/field-defs/:id
```

GET 权限：`robot-manager:read`
写权限：`robot-manager:manage:fields`

GET 支持 `?scope=unit|service_record|location` 和 `?enabledOnly=true` 过滤。

**v5 架构**：v4 之前的 `RobotOption` 表已删除。所有字段定义和字典选项统一通过 `RobotFieldDef` 管理，按 `scope` 区分：
- `scope=unit` — 设备业务字段（按 `group` 划分到 Identity/SC/Sales/Finance/After-Sales/Compliance 六个 Tab）
- `scope=service_record` — 服务记录字典（如 `serviceType`）
- `scope=location` — 位置字典（如 `locationType`）

**RobotFieldDef shape**：

```json
{
  "id": "uuid",
  "scope": "unit",
  "key": "salesPriceHardware",
  "label": "销售价格 - 硬件",
  "labelEn": "Sales Price - Hardware",
  "type": "money",
  "group": "sales",
  "sortOrder": 1,
  "required": false,
  "enabled": true,
  "showInList": true,
  "indexed": false,
  "options": [
    { "code": "PRE_SALE", "label": "售前" }
  ],
  "validation": {}
}
```

**写入校验**（错误码）：
- `ROBOT_FIELD_KEY_OR_LABEL_EN_REQUIRED` / `ROBOT_FIELD_KEY_GENERATION_FAILED` / `ROBOT_FIELD_KEY_MUST_START_WITH_LETTER`
- `ROBOT_FIELD_GROUP_REQUIRED_FOR_UNIT_SCOPE` / `ROBOT_FIELD_OPTIONS_REQUIRED_FOR_SELECT` / `ROBOT_FIELD_OPTION_REQUIRES_CODE_AND_LABEL` / `ROBOT_FIELD_OPTIONS_ONLY_FOR_SELECT`

### 6.2 RobotSystemConfig

```
GET    /api/v1/robot-manager/admin/config
GET    /api/v1/robot-manager/admin/config/:key
PUT    /api/v1/robot-manager/admin/config/:key
DELETE /api/v1/robot-manager/admin/config/:key
```

GET 权限：`robot-manager:read`
写权限：`robot-manager:manage:fields`

`PUT` body：`{ "value": <any>, "description": "可选" }`

主要 keys：`ffsn_rule`、`status_default_location`、`repair_auto_service_type`（详见 [06-data-model.md](06-data-model.md)）。

### 6.3 业务实体（Model / SKU / Supplier / Customer / Location）

每个实体都有相同的 5 个端点：

```
GET    /api/v1/robot-manager/admin/{resource}
GET    /api/v1/robot-manager/admin/{resource}/:id
POST   /api/v1/robot-manager/admin/{resource}
PUT    /api/v1/robot-manager/admin/{resource}/:id
DELETE /api/v1/robot-manager/admin/{resource}/:id
```

`{resource}` ∈ `models` / `skus` / `suppliers` / `customers` / `locations`

### 6.3.1 批量 Excel 导入导出（所有 5 个实体通用）

每个实体都有一套 Excel 端点：

```
GET  /api/v1/robot-manager/admin/{resource}/excel/export     # 导出全部为 xlsx
GET  /api/v1/robot-manager/admin/{resource}/excel/template   # 下载空白模板
POST /api/v1/robot-manager/admin/{resource}/excel/preview    # 上传 + 解析表头/样本（不导入）
POST /api/v1/robot-manager/admin/{resource}/excel/import     # 带 mapping + conflictStrategy 导入
```

权限：
- export / template → `robot-manager:read`
- preview / import → 对应 `manage:*`（models/skus 共享 `manage:models`；其余 `manage:{resource}`）

**Import body**（`multipart/form-data`）：
- `file` — .xlsx 文件
- `mapping` — JSON 字符串，`{fieldKey: excelColName}`
- `conflictStrategy` — `skip`（默认，code 已存在跳过）/ `update`（覆盖已有字段）/ `abort`（有冲突整体失败）

**Import 响应**：

```json
{ "total": 10, "created": 7, "updated": 0, "skipped": 3, "failed": 0, "errors": [] }
```

**字段映射**（按实体）：
- `model`: code / name / brand / description / enabled
- `sku`: code / name / **modelCode**（可填 Model code 或 name）/ variant / defaultPrice / defaultCost / description / enabled
- `supplier`: code / name / contactName / contactPhone / contactEmail / address / paymentTerms / leadTimeDays / enabled
- `customer`: code / name / industry / contactName / contactPhone / contactEmail / address / creditLimit / enabled
- `location`: code / name / typeCode / address / contact / **customerCode** / enabled

`modelCode` / `customerCode` FK 字段按 code 优先、name 兜底匹配。


GET 权限：`robot-manager:read`
写权限（v6 拆分 `admin` 为五个 `manage:*`）：
- `models` / `skus` → `robot-manager:manage:models`
- `suppliers` → `robot-manager:manage:suppliers`
- `customers` → `robot-manager:manage:customers`
- `locations` → `robot-manager:manage:locations`

**通用 Query**：`search`, `enabledOnly`

**通用写入规则**：
- `code` 为可选字段。POST 时留空则后端按 `{PREFIX}-{base36 时间戳+随机}` 自动生成，前缀约定：Model=`MDL`、Sku=`SKU`、Supplier=`SUP`、Customer=`CST`、Location=`LOC`
- `code` 数据库层保持 `@unique` 约束；PUT 不接受 `code` 字段（`UpdateInput` 已 `Omit<...,'code'>`）
- Excel 导入（§5.2）仍要求显式传 `code` 作为匹配键，不走自动生成

**Model 特有**：
- list 返回值含 `_count: { skus, robotUnits }`
- 不能删除有 robot 引用的 model

**Sku 特有**：
- 必须挂在某个 model 下：`POST` body 必须包含 `modelId`
- list 支持 `?modelId=` 过滤
- 切换 model 时校验 sku 是否属于新 model

**Location 特有**：
- 必须指定 `typeCode`（来自 System FieldDef `scope=location` key=`locationType` 的 options[].code）
- 可选关联 `customerId`（客户站点）

字段定义详见 [06-data-model.md](06-data-model.md) 业务实体表章节。

---

## 7. 附件

### 7.1 列出设备附件

```
GET /api/v1/robot-manager/:id/attachments
```

权限：`robot-manager:read`

响应：`RobotAttachment[]`，按 `uploadedAt desc`。

### 7.2 上传附件

```
POST /api/v1/robot-manager/:id/attachments
```

权限：`robot-manager:update`
Content-Type：`multipart/form-data`
Body：`file` 字段

限制：最大 50 MB。文件存到 `uploads/robot-attachments/{unitId}/{uuid}{ext}`。

错误码：
- `ROBOT_ATTACHMENT_NO_FILE` — 未上传文件
- `ROBOT_ATTACHMENT_TOO_LARGE` — 文件超 50 MB

### 7.3 下载附件

```
GET /api/v1/robot-manager/attachments/:attachmentId/download
```

权限：`robot-manager:read`
响应：原始文件流，带 `Content-Disposition: attachment`。

### 7.4 删除附件

```
DELETE /api/v1/robot-manager/attachments/:attachmentId
```

权限：`robot-manager:update`
成功返回 `{ success: true }`。磁盘文件 best-effort 删除。

---

## 8. 全局搜索

```
GET /api/v1/robot-manager/search?q=<keyword>
```

权限：`robot-manager:read`

说明：跨 units 和 archives（models/skus/suppliers/customers/locations）全局搜索。`q` 为空时返回空集合。

---

## 9. 错误码总览

| HTTP | 触发场景 |
|---|---|
| 400 | DTO 校验失败 / FK 不存在 / 字典 code 不合法 / poNumber 试图修改 / Guard 条件不满足 / 非法状态转换 |
| 401 | 未携带 JWT 或 token 失效 |
| 403 | 权限不足（缺 `robot-manager:write:*` 等） |
| 404 | 资源不存在或已软删除 |
| 409 | 乐观锁版本冲突 |
| 500 | 内部错误（已加 ParseUUIDPipe 防御 :id 路由非法 UUID） |
