# 20260730 定制履约落地接口文档

## 一、整体业务流设计

### 1.1 设计目标

本次落地围绕定制眼镜履约主流程展开，目标是把以下能力串成一条可操作、可追踪、可人工干预的后端流程：

```text
客户处方维护
  -> 订单创建时复用或改写处方
  -> 生成订单处方快照
  -> 拆解定制履约单元和部件
  -> 镜框锁库或采购
  -> 镜片委外加工
  -> 镜片接收入库
  -> 部件齐套后生成组装委外
  -> 自动发料出库
  -> 组装供应商发货
  -> 接收成品入库
  -> 后续发货计划和快递任务
```

其中客户处方只是下单时的复用来源，真正用于生产、委外、供应商展示和后续追溯的是订单处方快照。

### 1.2 核心数据关系

| 数据 | 作用 |
| --- | --- |
| `optical_prescription` | 客户历史处方档案，用于下单选择和复用 |
| `sale_order_detail.extra.optical` | 销售订单明细上的本次订单处方快照 |
| `custom_order_item.params_snapshot` | 定制履约主记录上的生产参数快照 |
| `custom_item_component.params_snapshot` | 镜框、左镜片、右镜片等部件参数快照 |
| `optical_component_product_rule` | 根据镜片属性、处方范围、镜片选项匹配镜片产品和供应商 |
| `product.frame_*` | 镜框产品的眼镜尺寸、颜色、厂家信息、销售状态、供应状态等定制字段 |
| `entrust` | 镜片加工委外单和组装委外单 |
| `entrust_sla_rule` | 委外工单 SLA 规则，按供应商、部件类型、镜片属性等条件匹配 |
| `custom_process_log` | 定制履约时间线，记录异常、暂停、恢复、回退、人工推进等操作 |

### 1.3 订单处方与履约拆解

ERP 创建订单或红人寄样订单时，前端可以先维护客户处方，也可以直接在订单明细上填写本次处方。保存订单明细处方后，后端会：

1. 将处方写入 `sale_order_detail.extra.optical`。
2. 生成或更新 `custom_order_item.params_snapshot`。
3. 拆解 `custom_item_component`，默认包含镜框、左镜片、右镜片。
4. 镜框部件读取产品 `frame_*` 字段形成尺寸快照。
5. 镜片部件根据订单快照匹配 `optical_component_product_rule`，得到镜片产品和默认供应商。
6. 生成默认履约工序，例如镜框准备、镜片委外、镜片入库、组装、质检、成品入库、发货。

如果定制履约已经进入委外、采购、锁库或入库环节，后端不允许直接重建处方部件，需要先走异常处理、回退或补偿流程。

### 1.4 审核后的准备逻辑

销售订单审核通过后，后端按 `custom_item_component` 生成准备单据：

1. 镜框部件优先检查库存。
2. 有可用库存时，生成 `repository_freeze_log` 锁定库存。
3. 库存不足时，生成采购计划明细，等待采购人员后续采购。
4. 镜片部件生成镜片加工委外单 `entrust.type = 1`。
5. 镜片委外单默认审核通过，进入待接单或待生产状态。
6. 委外单价格优先取部件 `product_id + supplier_id` 对应的 `supplier_product.price`。

准备流程会尊重异常和暂停状态。订单下任一定制履约对象存在未处理异常，或同一对象最新日志为 `pause` 且未 `resume`，后端会阻断自动生成采购、委外和后续单据。

### 1.5 镜片委外到货入库

镜片供应商完成生产后，在委外单上填写物流单号并发货。ERP 接收入库时：

1. 调用委外单接收入库接口。
2. 后端生成入库单并审核通过。
3. 增加镜片产品库存。
4. 联动 `custom_item_component.status`，将对应镜片部件标记为已入库或已齐套。
5. 同步委外成本到销售订单明细成本字段。

供应商只能看到与自身委外单相关的镜片定制参数和必要产品信息，不能看到原始销售单号、销售价格、SKU 价格等敏感信息。

### 1.6 部件齐套后的组装委外

镜框、左镜片、右镜片齐套后，本期流程支持生成组装委外，而不是直接走内部组装：

1. 前端选择组装供应商，调用生成组装委外接口。
2. 后端校验部件是否齐套、是否存在异常或暂停。
3. 生成 `entrust.type = 2` 的组装委外单，产品为成品眼镜。
4. 组装委外单默认审核通过。
5. 后端自动生成发料出库单，将镜框和镜片出库给组装供应商。
6. `custom_item_component.assemble_entrust_id` 记录组装委外单关联。
7. `custom_item_process.process_type = assemble` 关联该组装委外单。

组装委外发料出库是自动过程，库存扣减由现有 `RepositoryReceipt` 审核逻辑完成。

### 1.7 组装完成与成品入库

组装供应商完成后，仍复用委外单发货和接收入库接口：

1. 供应商或内部人员在组装委外单填写物流单号并发货。
2. ERP 接收组装委外单入库。
3. 入库对象是成品眼镜，不再重复消耗镜框和镜片。
4. 后端联动完成组装工序，并尝试推进后续质检和成品入库节点。
5. 成品入库后，后续可继续生成发货计划和快递任务。

需要注意：组装委外发料时已经扣减镜框和镜片库存，所以组装成品接收入库时只能增加成品库存，不能再按旧内部组装逻辑二次扣减部件库存。

### 1.8 异常、暂停与状态汇总

异常事实来源仍在履约明细层：

| 层级 | 字段 |
| --- | --- |
| 定制履约单元 | `custom_order_item.abnormal_status`、`abnormal_reason` |
| 定制部件 | `custom_item_component.is_abnormal`、`abnormal_reason` |
| 定制工序 | `custom_item_process.is_abnormal`、`abnormal_reason` |
| 操作留痕 | `custom_process_log.event`、`remark` |

订单层只新增一个总标记：

```text
sale_order.custom_abnormal_status
```

该字段只用于订单列表快速筛选“有未处理定制异常的订单”，不承接具体异常原因。异常原因、处理过程和操作人仍从定制履约明细和 `custom_process_log` 查看。

暂停不新增字段，MVP 阶段通过 `custom_process_log` 记录。后端判断同一 `target_type + target_id` 的最新日志，如果是 `pause` 且未被 `resume` 覆盖，则认为该对象处于暂停中。

### 1.9 委外 SLA

委外单生成或审核通过后，后端会按以下条件匹配 SLA 规则：

1. 公司。
2. 供应商，可为空表示不限供应商。
3. 部件类型，例如 `lens_left`、`lens_right`、`lens_pair`。
4. 范围类型，例如 `lens`、`assemble`、`all`。
5. `match_conditions` 中配置的镜片属性、处方范围、镜片选项等动态条件。

匹配后写入委外单的接单、生产、发货、入库截止时间。SLA 扫描接口会把将超时和已超时状态写回 `entrust.sla_status`，超时的委外会联动对应部件进入异常待处理。

### 1.10 前端页面建议

前端可以按以下页面组织：

| 页面 | 主要能力 |
| --- | --- |
| 客户处方页 | 维护客户历史处方，供订单下单复用 |
| 订单明细处方弹窗 | 选择客户处方、手动改写本订单处方、触发履约拆解 |
| 定制履约详情页 | 展示履约单元、部件、工序、处方快照、异常时间线 |
| 镜框产品属性页 | 通过产品表 `frame_*` 字段维护镜框尺寸、颜色、厂家信息、状态等 |
| 委外单列表 | 镜片委外和组装委外统一查看，支持发货、接收入库、SLA 状态 |
| 组装委外操作区 | 部件齐套后选择组装供应商，生成组装委外 |
| 异常与调整面板 | 暂停、恢复、标记异常、解除异常、手动推进、工序回退 |
| SLA 规则页 | 维护委外时效规则，按镜片属性和供应商灵活配置 |

## 二、客户处方

### 查询客户处方选项

```http
GET /admin/optical-prescription/options?customer_id=1001
```

用途：订单创建或红人寄样下单时选择客户历史处方。

### 维护客户处方档案

复用通用接口：

```http
GET  /admin/optical-prescription/index?customer_id=1001
GET  /admin/optical-prescription/detail?id=12
POST /admin/optical-prescription/add
POST /admin/optical-prescription/edit
```

关键字段：`customer_id`、`od_sph`、`od_cyl`、`od_axis`、`os_sph`、`os_cyl`、`os_axis`、`pd`、`pd_right`、`pd_left`、`prescription_type`、`lens_group`、`lens_option`。

## 三、订单处方与定制履约拆解

### 保存订单明细处方

```http
POST /admin/sale-order-detail/saveOpticalPrescription
```

请求示例：

```json
{
  "sale_order_detail_id": 2001,
  "prescription_mode": "use_customer_prescription",
  "optical_prescription_id": 12,
  "lens": {
    "group": "防蓝光",
    "option": "1.60",
    "type": "prescription_lens"
  },
  "save_as_customer_prescription": false,
  "rebuild_custom_fulfillment": true
}
```

`prescription_mode`：

| 值 | 说明 |
| --- | --- |
| `use_customer_latest` | 使用客户最新处方 |
| `use_customer_prescription` | 使用指定客户处方 |
| `manual_override` | 手动填写本订单处方 |

响应重点：

```json
{
  "sale_order_detail_id": 2001,
  "custom_order_item_id": 3001,
  "component_count": 3,
  "component_abnormal_count": 0,
  "process_count": 7
}
```

### 保存定制履约处方

```http
POST /admin/custom-order-item/saveOpticalPrescription
```

用途：已生成 `custom_order_item` 但尚未进入委外、采购、库存环节时，修改订单处方并重建部件。

## 四、镜框眼镜属性

镜框尺寸和颜色通过 `product` 表新增 `frame_*` 字段维护，不再新增 `optical_product_profile`，也不再复用 `extra_field/product_extra`。

复用产品详情和编辑接口：

```http
GET  /admin/product/detail?id=3
POST /admin/product/edit
```

`edit` 请求示例：

```json
{
  "id": 3,
  "optical_product_type": 10,
  "packaging_cost": "1.50",
  "frame_lens_width": "54",
  "frame_lens_height": "40",
  "frame_width": "138",
  "frame_bridge_width": "18",
  "frame_temple_length": "135",
  "frame_lens_diagonal": "56",
  "frame_size_display": "54□18-135",
  "frame_size_unit": "mm",
  "frame_manufacturer": "ABC Optical",
  "frame_factory_item_no": "F1234",
  "frame_factory_color_no": "C01",
  "frame_color": "Black",
  "frame_original_price": "99.00",
  "frame_spring_hinge": 0,
  "frame_adjustable_nose_pad": 1,
  "frame_sales_status": 10,
  "frame_supply_status": 10
}
```

关键字段：

| 字段 | 说明 | 类型/控件 | 默认值 | 前端维护 |
| --- | --- | --- | --- | --- |
| `product_id` | 镜框产品ID | number | - | 只读 |
| `optical_product_type` | 眼镜产品类型 | select | `0` | 可编辑 |
| `packaging_cost` | 包材费用 | number/金额 | `0.00` | 可编辑 |
| `frame_lens_width` | 镜片宽度 | number | `null` | 可编辑 |
| `frame_lens_height` | 镜片高度 | number | `null` | 可编辑 |
| `frame_width` | 镜框宽度 | number | `null` | 可编辑 |
| `frame_bridge_width` | 中梁宽度 | number | `null` | 可编辑 |
| `frame_temple_length` | 镜腿长度 | number | `null` | 可编辑 |
| `frame_lens_diagonal` | 镜片斜对角线 | number | `null` | 可编辑 |
| `frame_size_display` | 展示格式，如 `54□18-135` | string | `null` | 可编辑 |
| `frame_size_unit` | 尺寸单位 | select | `mm` | 可编辑 |
| `frame_manufacturer` | 镜框厂家 | string | `null` | 可编辑 |
| `frame_factory_item_no` | 厂家货号 | string | `null` | 可编辑 |
| `frame_factory_color_no` | 厂家颜色编号 | string | `null` | 可编辑 |
| `frame_color` | 镜框颜色 | string | `null` | 可编辑 |
| `frame_original_price` | 原始定价，仅上架参考 | number/金额 | `0.00` | 可编辑 |
| `frame_spring_hinge` | 弹簧铰链 | switch/select | `0` | 可编辑 |
| `frame_adjustable_nose_pad` | 可调节鼻托 | switch/select | `0` | 可编辑 |
| `frame_sales_status` | 镜框销售状态 | select | `0` | 可编辑 |
| `frame_supply_status` | 供应状态 | select | `0` | 建议只读，供应维护页可编辑 |
| `frame_sku_grade` | SKU 等级 | select | `null` | 只读，系统计算 |
| `frame_supply_status_updated_at` | 最近供应状态更新时间 | datetime/string | `null` | 只读 |
| `frame_supply_status_updated_by` | 最近供应状态回传方 | string | `null` | 只读 |

枚举配置：

| 字段 | 配置 key | 选项 |
| --- | --- | --- |
| `optical_product_type` | `PRODUCT_OPTICAL_TYPE` | `0` 未设置，`10` 镜框，`20` 镜片，`30` 包材 |
| `frame_size_unit` | `PRODUCT_FRAME_SIZE_UNIT` | `mm` 毫米 |
| `frame_spring_hinge` | `PRODUCT_FRAME_BOOL` | `0` 否，`1` 是 |
| `frame_adjustable_nose_pad` | `PRODUCT_FRAME_BOOL` | `0` 否，`1` 是 |
| `frame_sales_status` | `PRODUCT_FRAME_SALES_STATUS` | `0` 未上架，`10` 正常销售，`20` 暂时隐藏，`30` 停止销售 |
| `frame_supply_status` | `PRODUCT_FRAME_SUPPLY_STATUS` | `0` 未知，`10` 供应正常，`20` 需要生产，`30` 停产 |
| `frame_sku_grade` | `PRODUCT_FRAME_SKU_GRADE` | `A`/`B`/`C`/`D` |

说明：

1. 这些字段直接来自 `product` 表新增字段。
2. 后端履约拆解时直接读取 `Product` 模型字段。
3. 如果 `frame_size_display` 为空，后端会根据 `frame_lens_width`、`frame_bridge_width`、`frame_temple_length` 自动拼接展示值。
4. 拆解后会写入 `custom_item_component.params_snapshot.frame`，订单履约后续使用快照，不直接依赖产品当前字段。
5. `frame_supply_status`、`frame_sku_grade`、`frame_supply_status_updated_at`、`frame_supply_status_updated_by` 偏系统/供应回传字段，普通产品编辑页建议只读展示。
6. `packaging_cost` 是通用产品成本补充字段，不带 `frame_` 前缀，但本次与镜框资料一起给前端维护。
7. `optical_product_type` 用于区分产品在眼镜履约中的用途。镜框产品填 `10`，镜片产品填 `20`，包材产品填 `30`；非眼镜履约产品保持 `0`。

## 五、异常调整

### 暂停/恢复

```http
POST /admin/custom-fulfillment-adjust/pause
POST /admin/custom-fulfillment-adjust/resume
```

请求：

```json
{
  "target_type": "custom_order_item",
  "target_id": 3001,
  "reason": "客户要求确认处方，暂停处理"
}
```

说明：MVP 阶段暂停不新增业务字段，使用 `custom_process_log` 记录；同一 `target_type + target_id` 最新记录为 `pause` 且未被 `resume` 覆盖时，自动生成履约单据、组装委外、组装入库会被阻断。

### 标记异常/解除异常

```http
POST /admin/custom-fulfillment-adjust/markAbnormal
POST /admin/custom-fulfillment-adjust/resolveAbnormal
```

请求：

```json
{
  "target_type": "component",
  "target_id": 5001,
  "reason": "供应商拒单"
}
```

说明：订单列表筛选字段只使用 `sale_order.custom_abnormal_status`，明细原因从 `custom_order_item`、`custom_item_component`、`custom_item_process` 和 `custom_process_log` 查看。

### 手动推进/工序回退

```http
POST /admin/custom-fulfillment-adjust/manualForward
POST /admin/custom-fulfillment-adjust/rollback
```

## 六、组装委外

### 生成组装委外

```http
POST /admin/sale-order/customAssembleEntrust
```

请求：

```json
{
  "id": 14,
  "supplier_id": 88
}
```

行为：

1. 检查订单下定制履约部件是否齐套。
2. 生成 `entrust.type = 2` 的组装委外单。
3. 默认审核通过，进入待接单。
4. 自动生成部件发料出库单并审核。
5. 将组装委外单关联到 `custom_item_process.process_type = assemble`。

### 组装委外发货与接收入库

继续复用委外单接口：

```http
POST /admin/entrust/ship
POST /admin/entrust/receiveStockIn
```

组装委外接收入库后，会自动完成组装工序，并尝试推进下一工序。

## 七、委外 SLA

### SLA 规则维护

复用通用 CRUD：

```http
GET  /admin/entrust-sla-rule/index
GET  /admin/entrust-sla-rule/detail?id=1
POST /admin/entrust-sla-rule/add
POST /admin/entrust-sla-rule/edit
```

### 扫描委外 SLA

```http
POST /admin/entrust/inspectSla
```

返回：

```json
{
  "total": 20,
  "warning": 3,
  "timeout": 1
}
```

## 八、流程测试 Command

```bash
php bin/hyperf.php custom:20260730-flow-test 14 --mutate=1 --prescription_id=12 --assemble_supplier_id=88
```

常用参数：

| 参数 | 说明 |
| --- | --- |
| `sale_order_id` | 销售订单ID |
| `--mutate=1` | 是否写入数据 |
| `--prescription_id=12` | 使用指定客户处方 |
| `--use_latest_prescription=1` | 使用客户最新处方 |
| `--assemble_supplier_id=88` | 生成组装委外的供应商 |
| `--json=1` | 输出 JSON |
