# 定制履约异常记录与状态动态调整设计方案

## 一、背景

根据 `2027-07-30会议纪要-整理版.md` 中“履约流程状态与异常处理”的讨论，定制眼镜订单在履约过程中需要支持各阶段、各部件的人工调整能力，包括：

1. 订单履约可以暂停。
2. 部件、工序、委外、采购、配送等环节可以标记异常。
3. 异常处理后可以回退、继续、重新派单、重新采购、手动推进。
4. 人工调整必须有记录留底，能够追溯谁在什么时候因为什么原因调整了什么状态。
5. 已经产生库存、出库、入库、发货等实物影响的环节，不能只靠改状态回退，必须通过补偿单据处理。

现有系统已经有部分基础能力：

1. `custom_order_item`：定制履约主记录，已有 `status`、`abnormal_status`、`abnormal_reason` 等字段。
2. `custom_item_component`：定制部件记录，已有 `status`、`is_abnormal`、`abnormal_reason` 等字段。
3. `custom_item_process`：定制工序记录，已有 `status`、`is_abnormal`、`abnormal_reason` 等字段。
4. `custom_process_log`：已有履约过程日志，可记录 `target_type`、`target_id`、`event`、`from_status`、`to_status`、`remark`、`operator_admin_id`。
5. `CustomProductionFlowService`：已有质检驳回、工序回退、异常标记等能力。

本方案在现有模型基础上补充统一的异常记录、状态调整、审批和联动规则。

## 二、设计目标

1. 所有人工调整动作必须留痕，支持按订单、部件、工序、委外单、采购单、发货计划查询。
2. 状态调整必须有前置校验，不能越过库存、发货、财务等关键业务边界。
3. 暂停、回退、手动推进、异常解除等动作需要统一入口，避免不同控制器各自直接改状态。
4. 自动任务必须识别暂停和异常状态，避免异常订单继续自动生成后续单据。
5. 调整后需要同步刷新定制履约主状态、销售订单状态、Shopify 自定义订单状态。
6. 客服、生产、仓库、采购、供应商角色只能操作自己权限范围内的调整动作。

## 三、核心原则

### 3.1 状态变更必须留痕

任何人工改变 `custom_order_item`、`custom_item_component`、`custom_item_process`、`entrust`、`purchase`、`delivery_plan`、`express_task` 状态的动作，都必须记录调整日志。

日志需要保存：

1. 调整对象。
2. 调整前状态。
3. 调整后状态。
4. 调整原因。
5. 操作人。
6. 操作时间。
7. 调整前关键字段快照。
8. 调整后关键字段快照。
9. 被联动影响的其他对象。

### 3.2 区分业务状态和异常状态

业务状态表示流程正常推进到了哪一步，例如待采购、待委外、委外中、待组装、质检中、已入库、已发货。

异常状态表示当前对象是否存在需要人工处理的问题，例如供应商拒单、委外超时、库存不足、质检失败、配送失败。

不要把所有异常都做成新的业务状态，否则状态机会越来越复杂。建议保持：

1. `status` 表示业务阶段。
2. `abnormal_status` 或 `is_abnormal` 表示是否异常。
3. `abnormal_reason` 表示异常原因。
4. 异常处理记录放在调整日志中。

### 3.3 订单层需要承接汇总异常

当前代码中，`sale_order` 只有订单通用状态字段，例如 `status`、`audit_status`、`pay_status`、`delivery_status`、`recheck_status`、`refund_status`，没有专门用于承接定制履约异常的字段。

现有异常承接点主要在定制履约相关表：

| 表 | 字段 | 定位 |
| --- | --- | --- |
| `custom_order_item` | `status`、`abnormal_status`、`abnormal_reason` | 一副定制眼镜的履约主异常 |
| `custom_item_component` | `status`、`is_abnormal`、`abnormal_reason` | 镜框、镜片、包材等部件异常 |
| `custom_item_process` | `status`、`is_abnormal`、`abnormal_reason` | 采购、委外、组装、质检、入库、发货等工序异常 |
| `custom_process_log` | `event`、`remark`、`from_status`、`to_status` | 履约时间线记录 |

因此，定制履约异常的事实来源应继续放在 `custom_order_item`、`custom_item_component`、`custom_item_process` 上，`sale_order` 不应承接每一个明细异常，否则一个订单多副眼镜、多部件、多工序时会丢失层级信息。

但是订单列表、客服面板、销售订单详情需要快速判断“这个订单是否有定制异常”，所以建议在 `sale_order` 上只增加一个总异常标记字段，作为冗余筛选字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `custom_abnormal_status` | tinyint | 定制异常总标记：0无异常 1有未处理异常 |

订单层字段只负责筛选有异常的订单，不承接异常原因、异常数量、暂停状态、履约阶段等细节。实际处理仍然要回到具体的 `custom_order_item`、`custom_item_component`、`custom_item_process` 或关联单据。

状态同步规则：

1. 部件或工序异常时，同步更新所属 `custom_order_item.abnormal_status = 1`。
2. 任一 `custom_order_item.abnormal_status = 1` 时，同步更新 `sale_order.custom_abnormal_status = 1`。
3. 所有定制履约异常解除后，同步更新 `sale_order.custom_abnormal_status = 0`。
4. 订单暂停不写入 `sale_order.custom_abnormal_status`，暂停状态仍在定制履约对象和调整日志中体现；只有暂停原因本身被标记为异常时，才同步为订单异常。
5. Shopify 自定义订单状态同步时，可以使用 `sale_order.custom_abnormal_status` 快速判断是否需要进入异常状态，再回查定制履约明细确认具体阶段。

建议新增一个汇总服务：

```text
App\Service\Common\CustomOrderFulfillmentSummaryService
```

核心方法：

| 方法 | 说明 |
| --- | --- |
| `syncByCustomOrderItemId(int $customOrderItemId)` | 根据定制履约主记录同步销售订单异常总标记 |
| `syncBySaleOrderId(int $saleOrderId)` | 重算整个销售订单是否存在未处理定制异常 |
| `hasOpenAbnormal(int $saleOrderId)` | 判断订单下是否存在未处理异常 |

触发点：

1. 创建、修改 `custom_order_item` 后。
2. 修改 `custom_item_component.status/is_abnormal` 后。
3. 修改 `custom_item_process.status/is_abnormal` 后。
4. 委外单拒单、超时、发货、接收入库后。
5. 采购异常、采购入库后。
6. 质检驳回、返工、成品入库后。
7. 发货计划、快递任务异常后。
8. 人工暂停、恢复、标记异常、解除异常后。

### 3.4 实物单据不能简单回退

以下场景不允许仅通过修改状态回退：

1. 已审核入库。
2. 已审核出库。
3. 已产生库存占用。
4. 已发货。
5. 已完成采购入库。
6. 已完成委外接收入库。
7. 已完成成品入库。

这些场景必须通过补偿业务处理：

1. 已入库后发现问题：生成退料、报损、返工、重新加工或库存调整单。
2. 已出库后取消：生成退回入库或出库冲销单。
3. 已发货后失败：进入配送异常、退回、重新派送或售后流程。
4. 已完成组装入库后返工：成品出库拆解或返工单处理。

### 3.5 自动流程必须尊重暂停和异常

只要定制履约主记录、部件或当前工序处于暂停、异常未处理状态，自动流程不能继续生成后续单据。

需要拦截的自动动作包括：

1. 自动生成采购计划。
2. 自动生成镜片委外单。
3. 自动生成组装委外单。
4. 自动委外领料出库。
5. 自动组装入库。
6. 自动生成发货计划。
7. 自动生成快递任务。
8. 自动同步 Shopify 自定义订单状态。

## 四、调整对象范围

| 对象 | target_type | 说明 |
| --- | --- | --- |
| 定制履约单元 | `custom_order_item` | 一副定制眼镜的履约主记录 |
| 定制部件 | `custom_item_component` | 镜框、左镜片、右镜片、包材等部件 |
| 定制工序 | `custom_item_process` | 采购、镜片委外、组装、质检、入库、发货等工序 |
| 委外单 | `entrust` | 镜片委外、组装委外等供应商加工单据 |
| 采购单据 | `purchase` | 采购计划、采购单、采购明细 |
| 库存单据 | `stock_document` | 入库、出库、领料、退料、组装入库等库存相关单据 |
| 发货计划 | `delivery_plan` | 订单发货计划 |
| 快递任务 | `express_task` | 快递下单、面单、揽收、发货相关任务 |
| 销售订单 | `sale_order` | 销售订单主状态或发货状态联动 |

## 五、调整动作设计

| 动作 | action_type | 适用对象 | 说明 |
| --- | --- | --- | --- |
| 暂停 | `pause` | 订单、部件、工序、委外单 | 暂停自动推进，等待人工处理 |
| 恢复 | `resume` | 订单、部件、工序、委外单 | 解除暂停，允许继续自动流转 |
| 标记异常 | `mark_abnormal` | 订单、部件、工序、单据 | 记录异常并阻断后续自动动作 |
| 解除异常 | `resolve_abnormal` | 订单、部件、工序、单据 | 异常处理完成后解除阻断 |
| 回退 | `rollback` | 工序、部件、履约单元 | 回到上一个可执行节点 |
| 手动推进 | `manual_forward` | 工序、部件、履约单元 | 线下已完成，系统补录推进 |
| 手动完成 | `manual_complete` | 工序、部件、履约单元 | 强制完成某个非库存关键节点 |
| 重派供应商 | `reassign_supplier` | 部件、委外单 | 供应商拒单、超时、无法生产时重派 |
| 重新采购 | `repurchase` | 部件、采购单 | 当前采购无法满足时重新生成采购需求 |
| 返工 | `rework` | 组装、质检、成品 | 质检失败或客户要求返工 |
| 取消 | `cancel` | 部件、委外、采购、快递任务 | 取消尚未产生实物影响的执行单据 |

## 六、异常原因分类

建议统一维护异常原因枚举，供前端下拉选择，便于后续统计。

| reason_type | 说明 |
| --- | --- |
| `stock_insufficient` | 库存不足 |
| `purchase_timeout` | 采购超时 |
| `purchase_unavailable` | 供应商停产或无法采购 |
| `supplier_reject` | 供应商拒单 |
| `supplier_unreachable` | 无法联系供应商 |
| `entrust_timeout` | 委外加工超时 |
| `entrust_quality_issue` | 委外加工质量问题 |
| `lens_param_error` | 镜片定制参数错误 |
| `frame_param_error` | 镜框参数错误 |
| `material_missing` | 部件缺失 |
| `qc_failed` | 质检失败 |
| `delivery_failed` | 配送失败 |
| `express_exception` | 快递异常 |
| `customer_change_request` | 客户修改需求 |
| `manual_correction` | 人工修正 |
| `system_error` | 系统自动流程异常 |

## 七、日志模型设计

### 7.1 现有 `custom_process_log` 的定位

`custom_process_log` 继续作为履约时间线日志使用，也是 MVP 阶段异常调整的主要留痕表，用于订单详情页展示“发生了什么”。

例如：

1. 标记异常。
2. 解除异常。
3. 工序回退。
4. 手动推进。
5. 委外接收入库。
6. 质检驳回。

现有字段已经可以满足基础留痕：

| 字段 | 用途 |
| --- | --- |
| `custom_order_item_id` | 定位到定制履约主记录 |
| `target_type` | 标识调整对象，例如 `item`、`component`、`process`、`entrust`、`purchase`、`delivery` |
| `target_id` | 调整对象ID |
| `event` | 调整动作，例如 `abnormal_created`、`abnormal_resolved`、`process_rollback`、`manual_adjust`、`pause`、`resume` |
| `from_status` | 调整前状态 |
| `to_status` | 调整后状态 |
| `remark` | 调整原因、处理说明、关联单据信息 |
| `operator_admin_id` | 操作人 |
| `create_time` | 操作时间 |

### 7.2 MVP 阶段不新增调整审计表

为了收敛实现复杂度，MVP 阶段不新增 `hg_custom_status_adjustment_log`，直接复用 `custom_process_log`。

需要约定日志写法：

1. 所有人工调整必须写入 `custom_process_log`。
2. `target_type` 要指向真实被调整对象。
3. `event` 使用固定枚举，避免前端无法筛选。
4. `remark` 中写清楚原因、处理方式、关联单据号。
5. 如果一次操作联动多个对象，主对象写一条主日志，被联动对象各写一条简短日志。
6. 不在日志里保存大段 JSON 快照，避免日志表膨胀；需要看详情时实时查询关联对象。

建议补充的 `event`：

| event | 说明 |
| --- | --- |
| `pause` | 暂停 |
| `resume` | 恢复 |
| `abnormal_created` | 标记异常 |
| `abnormal_resolved` | 解除异常 |
| `process_rollback` | 工序回退 |
| `manual_adjust` | 人工调整 |
| `manual_forward` | 手动推进 |
| `supplier_reassign` | 重派供应商 |
| `stock_compensate` | 库存补偿处理 |
| `document_cancel` | 取消关联单据 |

### 7.3 后续扩展条件

只有出现以下需求时，再考虑新增独立调整审计表或附件表：

1. 需要审批流。
2. 需要上传异常凭证附件。
3. 需要保存调整前后完整字段快照。
4. 需要对人工调整做独立统计报表。
5. 需要满足更严格的审计合规要求。

在这些需求出现前，优先保持实现简单，避免为状态调整引入过重的数据结构。

## 八、暂停机制设计

### 8.1 暂停字段

不建议直接用 `abnormal_status` 表示暂停。暂停不一定是异常，可能是客户主动修改需求、内部等待确认、财务或客服暂缓。

建议在 `custom_order_item`、`custom_item_component`、`custom_item_process` 上补充：

| 字段 | 说明 |
| --- | --- |
| `hold_status` | 暂停状态：0未暂停 1已暂停 |
| `hold_reason` | 暂停原因 |
| `hold_time` | 暂停时间 |
| `hold_operator_admin_id` | 暂停操作人 |
| `resume_time` | 最近恢复时间 |

MVP 阶段如果暂不加字段，也可以先使用调整日志记录暂停动作，并将 `abnormal_status` 或 `is_abnormal` 作为阻断标记。但长期看，暂停和异常应分开。

### 8.2 暂停影响范围

订单级暂停：

1. 阻断该 `custom_order_item` 下所有未开始的自动动作。
2. 已经进行中的委外、采购、发货计划不自动取消。
3. 前端必须提示操作人选择外部单据处理方式。

部件级暂停：

1. 只暂停当前部件。
2. 依赖该部件的后续工序不能开始。
3. 其他无依赖部件可以继续。

工序级暂停：

1. 只暂停当前工序。
2. 下游工序不能开始。

### 8.3 外派委外单暂停处理

订单暂停时，如果存在已经生成的委外单，需要人工选择处理方式：

| 委外状态 | 可选处理 |
| --- | --- |
| 待派单 | 直接暂停或取消 |
| 待接单 | 通知供应商暂缓接单、取消重派 |
| 已接单未生产 | 联系供应商暂停生产、继续生产、取消重派 |
| 生产中 | 通常只能继续生产或协商中止，必须记录供应商反馈 |
| 已生产未发货 | 可等待发货、暂缓发货、内部确认后继续 |
| 已发货 | 不建议暂停委外单，只能在收货、退货、返工环节处理 |
| 已入库 | 不允许回退委外单状态，只能走库存补偿或返工 |

## 九、回退机制设计

### 9.1 可直接回退的场景

以下场景可以通过状态回退处理：

1. 质检未通过，回退到组装或镜片委外返工。
2. 供应商待接单阶段拒单，回退到待派单。
3. 采购计划未转正式采购单，回退到待采购。
4. 发货计划未分配配送员，回退到待生成发货计划。
5. 快递任务未真实下单，回退到待确认快递。

### 9.2 需要补偿单据的场景

以下场景不能只回退状态：

1. 镜片已经接收入库后，需要返工：生成返工委外或重新委外，不冲掉原入库记录。
2. 组装委外已经自动领料出库后取消：生成退料入库或出库冲销单。
3. 成品已经入库后发现问题：生成成品返工出库或库存调整。
4. 已经发货后客户拒收：进入配送异常或售后退回流程。
5. 已经扣减部件库存后取消订单：生成库存释放、退料或库存调整。

### 9.3 回退联动规则

回退到某个工序时，需要：

1. 当前目标工序设置为待处理或进行中。
2. 下游未产生实物影响的工序重置为待处理。
3. 下游已产生单据的对象标记为异常或取消，不能物理删除。
4. 定制履约主状态重新计算。
5. 部件状态重新计算。
6. 记录调整审计日志。
7. 写入 `custom_process_log` 时间线。
8. 同步 Shopify 自定义订单状态。

## 十、手动推进机制设计

手动推进只适用于线下已经真实完成，但系统因为历史数据、接口异常、供应商反馈延迟等原因没有同步成功的场景。

### 10.1 可手动推进的场景

1. 供应商线下已接单，系统补录接单。
2. 供应商线下已发货，系统补录快递单号。
3. 仓库已完成非库存关键节点，系统补录完成。
4. 质检结果已经线下确认，系统补录质检通过或驳回。
5. 发货计划线下已处理，系统补录状态。

### 10.2 不允许仅手动推进的场景

以下场景必须生成业务单据：

1. 入库。
2. 出库。
3. 组装成品入库。
4. 委外领料出库。
5. 扣减库存。
6. 增加库存。
7. 生成成本。

例如“镜片已到货入库”不能只把委外单改成已完成，必须生成入库单并审核通过，库存增加后再联动部件状态。

## 十一、异常处理闭环

异常不应只停留在“标记异常”，需要形成闭环。

### 11.1 异常生命周期

```text
正常流转
  -> 标记异常
  -> 分配处理人
  -> 选择处理方案
  -> 执行处理动作
  -> 解除异常
  -> 继续流转 / 回退 / 取消 / 进入售后
```

### 11.2 处理方案

| resolution_type | 说明 |
| --- | --- |
| `continue` | 确认无影响，继续流转 |
| `retry` | 重试当前动作 |
| `rollback` | 回退到指定节点 |
| `reassign_supplier` | 重派供应商 |
| `repurchase` | 重新采购 |
| `rework` | 返工 |
| `replace_component` | 更换部件 |
| `manual_forward` | 手动补录推进 |
| `cancel` | 取消当前执行单据或履约 |
| `after_sale` | 转入售后处理 |

### 11.3 异常解除条件

解除异常时需要校验：

1. 已填写处理说明。
2. 必要时已上传凭证。
3. 相关单据状态已经处理到允许继续的状态。
4. 库存相关异常已经通过库存单据处理。
5. 如果涉及供应商拒单或超时，已经完成重派或确认继续。
6. 如果涉及客户需求变更，已经同步更新定制参数和部件规则。

## 十二、统一服务设计

新增服务：

```text
App\Service\Common\CustomFulfillmentAdjustmentService
```

核心方法：

| 方法 | 说明 |
| --- | --- |
| `pauseTarget(array $params)` | 暂停订单、部件、工序或单据 |
| `resumeTarget(array $params)` | 恢复暂停对象 |
| `markAbnormal(array $params)` | 标记异常 |
| `resolveAbnormal(array $params)` | 解除异常并执行处理方案 |
| `rollbackTarget(array $params)` | 回退到指定节点 |
| `manualForward(array $params)` | 手动推进 |
| `reassignSupplier(array $params)` | 重派供应商 |
| `assertCanAdjust(array $params)` | 状态调整前置校验 |
| `writeAdjustmentLog(array $params)` | 写入调整审计日志 |
| `writeProcessLog(array $params)` | 写入履约时间线 |
| `syncFulfillmentStatus(int $customOrderItemId)` | 重算履约状态 |
| `syncShopifyOrderStage(int $saleOrderId)` | 同步 Shopify 自定义订单状态 |

### 12.1 事务边界

以下动作必须放在数据库事务中：

1. 修改目标对象状态。
2. 修改关联部件或工序状态。
3. 创建或取消执行单据。
4. 写入调整审计日志。
5. 写入履约时间线。

外部接口调用不放在事务内。事务提交后再触发异步任务，例如 Shopify 状态同步、供应商通知、短信或企业微信通知。

## 十三、接口设计

建议新增控制器：

```text
App\Controller\Admin\CustomFulfillmentAdjustmentController
```

### 13.1 暂停

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

请求参数：

```json
{
  "target_type": "custom_order_item",
  "target_id": 123,
  "reason_type": "customer_change_request",
  "reason": "客户要求修改镜片参数，暂停生产",
  "effect_policy": {
    "entrust_policy": "continue",
    "purchase_policy": "hold",
    "delivery_policy": "hold"
  }
}
```

### 13.2 恢复

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

请求参数：

```json
{
  "target_type": "custom_order_item",
  "target_id": 123,
  "reason": "客户已确认参数，恢复生产"
}
```

### 13.3 标记异常

```http
POST /admin/custom-fulfillment-adjust/mark-abnormal
```

请求参数：

```json
{
  "target_type": "custom_item_component",
  "target_id": 456,
  "reason_type": "supplier_reject",
  "reason": "供应商反馈该镜片配置暂时无法加工",
  "owner_admin_id": 1001
}
```

### 13.4 解除异常

```http
POST /admin/custom-fulfillment-adjust/resolve-abnormal
```

请求参数：

```json
{
  "target_type": "custom_item_component",
  "target_id": 456,
  "resolution_type": "reassign_supplier",
  "reason": "已重新分配供应商",
  "related_module": "entrust",
  "related_id": 789
}
```

### 13.5 回退

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

请求参数：

```json
{
  "custom_order_item_id": 123,
  "target_process_id": 10,
  "reason_type": "qc_failed",
  "reason": "质检失败，回退到组装工序返工"
}
```

### 13.6 手动推进

```http
POST /admin/custom-fulfillment-adjust/manual-forward
```

请求参数：

```json
{
  "target_type": "custom_item_process",
  "target_id": 10,
  "to_status": 100,
  "reason_type": "manual_correction",
  "reason": "线下已完成，补录系统状态",
  "related_module": "entrust",
  "related_id": 789
}
```

### 13.7 调整日志列表

```http
GET /admin/custom-fulfillment-adjust/logs
```

查询参数：

| 参数 | 说明 |
| --- | --- |
| `sale_order_id` | 销售订单ID |
| `custom_order_item_id` | 定制履约ID |
| `target_type` | 对象类型 |
| `target_id` | 对象ID |
| `action_type` | 动作类型 |
| `reason_type` | 原因类型 |
| `operator_admin_id` | 操作人 |
| `create_time|>=` | 开始时间 |
| `create_time|<=` | 结束时间 |

## 十四、权限与审批

### 14.1 权限建议

| 角色 | 可操作范围 |
| --- | --- |
| 客服 | 订单暂停、恢复申请、查看异常、备注处理结果 |
| 生产 | 工序异常、工序回退、手动推进非库存节点 |
| 采购 | 采购异常、重新采购、供应商不可供处理 |
| 仓库 | 入库、出库、退料、库存补偿相关处理 |
| 供应商 | 供应商端拒单、反馈异常、填写发货信息 |
| 管理员 | 高风险调整、审批、强制解除异常 |

### 14.2 需要审批的动作

以下动作建议进入审批：

1. 已产生库存单据后的回退。
2. 已发货订单的状态回退。
3. 取消已接单委外单。
4. 取消已审核采购单。
5. 强制解除异常。
6. 手动完成库存相关节点。
7. 修改供应商成本、委外价格、采购价格。

审批通过后才执行实际状态调整。审批未通过只记录申请，不改变业务状态。

## 十五、与现有履约流程的联动

### 15.1 镜片委外

1. 委外待派单：可取消、重派、暂停。
2. 委外待接单：供应商拒单后，部件回到待派单，记录拒单原因。
3. 委外生产中：允许标记异常，但不直接取消，需要内部确认处理方案。
4. 委外已发货：只能补录物流异常或等待签收入库。
5. 委外已入库：部件状态由入库结果决定，异常处理走返工或重新委外。

### 15.2 组装委外

镜片入库、镜框齐套后，如果组装走委外：

1. 生成组装委外单前检查订单、部件、工序是否暂停或异常。
2. 自动生成委外领料和出库时必须记录关联关系。
3. 组装供应商拒单时，回退到待分配组装供应商。
4. 组装委外已领料出库后取消，必须生成退料入库或出库冲销。
5. 组装完成入库后，成品库存增加，部件库存消耗，不允许简单回退。

### 15.3 采购

1. 镜框有库存时直接锁定库存，不生成采购。
2. 镜框无库存时生成采购计划。
3. 供应商不可供或采购超时时，部件标记异常。
4. 重新采购时保留原采购记录，新增采购需求或采购单。
5. 采购入库后，部件状态根据库存占用和齐套情况推进。

### 15.4 发货与配送

1. 成品入库后自动生成发货计划和快递任务前，需要检查订单是否暂停或异常。
2. 快递任务未真实下单前，可取消或回退。
3. 已下运单后，异常走快递异常处理。
4. 配送失败时，发货计划回到可再次分配状态，并记录失败原因。

## 十六、Shopify 自定义订单状态联动

状态调整完成后，需要触发 Shopify 自定义订单状态同步。

建议规则：

| ERP 状态 | Shopify 自定义订单状态 |
| --- | --- |
| 正常生产中 | 生产中 |
| 暂停中 | 暂停中 |
| 异常待处理 | 异常待处理 |
| 委外中 | 委外加工中 |
| 待组装 | 待组装 |
| 质检中 | 质检中 |
| 已入库待发货 | 待发货 |
| 已发货 | 已发货 |
| 已取消 | 已取消 |

如果 Shopify 当前只配置了有限枚举，可以先将暂停和异常统一同步为“异常待处理”，ERP 内部保留更细状态。

## 十七、前端交互设计

### 17.1 订单详情页

订单详情页增加“异常与调整”区域：

1. 当前是否暂停。
2. 当前是否异常。
3. 异常原因。
4. 处理负责人。
5. 最近一次调整记录。
6. 预计恢复时间。
7. 操作按钮：暂停、恢复、标记异常、解除异常、回退、手动推进。

### 17.2 调整弹窗

调整弹窗需要展示：

1. 调整对象。
2. 当前状态。
3. 可调整到的状态。
4. 调整原因类型。
5. 详细说明。
6. 影响范围预览。
7. 是否需要审批。
8. 附件上传。

对于会影响库存、委外、采购、快递的动作，前端必须展示风险提示和关联单据。

### 17.3 异常时间线

按时间展示：

1. 异常创建。
2. 负责人变更。
3. 供应商反馈。
4. 人工调整。
5. 审批记录。
6. 异常解除。
7. 后续状态推进。

## 十八、自动任务拦截点

以下服务在执行前需要统一调用 `assertCanAutoProceed($customOrderItemId, $targetType, $targetId)`：

1. 生成采购计划。
2. 生成镜片委外单。
3. 生成组装委外单。
4. 自动委外领料出库。
5. 委外接收入库后自动推进。
6. 组装入库。
7. 质检通过后入库。
8. 成品入库后生成发货计划。
9. 发货计划生成快递任务。
10. Shopify 订单状态同步。

拦截返回结构：

```json
{
  "allowed": false,
  "reason": "custom_order_item_paused",
  "message": "定制履约已暂停，不能自动生成组装委外单",
  "target_type": "custom_order_item",
  "target_id": 123
}
```

## 十九、实施阶段

### 19.1 MVP 阶段

1. 复用 `custom_process_log` 记录暂停、恢复、异常、回退、手动推进等调整日志。
2. 新增 `CustomFulfillmentAdjustmentService`。
3. 新增暂停、恢复、标记异常、解除异常、回退、手动推进接口。
4. 将现有工序回退、质检驳回、异常标记统一接入调整服务。
5. 在自动生成委外、组装、入库、发货计划、快递任务前增加暂停和异常校验。
6. 订单详情页支持查看调整日志。

### 19.2 第二阶段

1. 增加调整审批。
2. 增加附件凭证。
3. 增加供应商拒单、超时、反馈异常的闭环处理。
4. 增加采购异常和重新采购处理。
5. 增加组装委外取消后的自动退料入库或冲销单据。

### 19.3 第三阶段

1. 增加异常统计看板。
2. 增加异常原因分布分析。
3. 增加供应商异常率、超时率、返工率分析。
4. 增加客服视角的预计恢复时间和客户沟通记录。
5. 增加 Shopify 异常状态细分同步。

## 二十、关键校验规则

1. 没有原因说明，不允许人工调整。
2. 没有权限，不允许调整。
3. 高风险动作未审批，不允许执行。
4. 已入库、已出库、已发货的对象，不允许简单状态回退。
5. 库存相关动作必须通过库存单据完成。
6. 委外已发货后不允许取消，只能进入签收、退回、返工或异常处理。
7. 订单暂停后，自动流程不能继续生成新的执行单据。
8. 异常未解除前，不能自动进入下游工序。
9. 手动推进必须记录关联线下凭证或说明。
10. 所有调整必须同时写入审计日志和履约时间线。

## 二十一、结论

定制履约的异常处理不能只做成“改状态”功能，而应设计为一套可审计、可审批、可联动业务单据的状态调整体系。

建议以 `CustomFulfillmentAdjustmentService` 为统一入口，MVP 阶段复用 `custom_process_log` 保存异常和人工调整记录。对库存、委外、采购、发货等已经产生实物影响的节点，必须通过补偿单据处理，确保系统状态、库存数量、成本和供应商协同记录一致。
