# 生产管理：委外入库、工单、组装、质检、生产入库实施方案

## 需求范围

来源：`doc/jykj/软件开发需求功能点.md` 第 22-26 行。

| 功能 | 需求说明 |
| --- | --- |
| 委外入库 | 当工厂交付镜片后，生成入库单、管理入库流程、通知关联的订单生产组装工单。 |
| 工单管理 | 当定制的镜片入库后，自动根据销售订单生成工单、分派工单给工人。 |
| 组装工序 | 工人根据工单和领取到的原料进行眼镜的组装。 |
| 质检工序 | 组装完成自动流转到质检工序，质检通过的重新入库，质检不过的返回到上一步，或者进入异常处理程序。 |
| 生产入库 | 对于组装完成并且质检通过的订单进行生产入库，仓库保管成品，并发起后续的发货流程。 |

这部分需求属于定制眼镜订单的生产履约后半段，前置条件是订单审核后已经完成：

1. 镜框部件库存锁定或采购入库。
2. 左右镜片委外下单。
3. 镜片工厂发货并接收入库。

## 现有基础

当前代码已经具备可复用的执行单据和履约跟踪能力。

| 能力 | 现有模型/接口 | 说明 |
| --- | --- | --- |
| 委外加工单 | `entrust` | 已支持委外单发货、接收入库、质检单生成。 |
| 委外接收入库 | `Entrust::receiveStockIn()` | 已生成 `repository_receipt` / `repository_receipt_detail` 并自动审核入库。 |
| 定制履约单元 | `custom_order_item` | 一个销售订单明细下的定制成品履约对象。 |
| 定制部件 | `custom_item_component` | 镜框、左镜片、右镜片等部件，记录部件来源、库存/采购/委外关联和状态。 |
| 定制工序 | `custom_item_process` | 用于表示组装、质检、成品入库等履约节点。 |
| 履约日志 | `custom_process_log` | 记录状态变化、异常、回退、人工操作。 |
| 组装单 | `repository_assemble` / `repository_assemble_detail` | 已支持消耗多个配件库存并生成一个成品库存。 |
| 质检单 | `qc_receipt` / `qc_receipt_detail` | 已有质检单据基础，可关联委外单。 |
| 成品入库 | `CustomItemProcess::finishFinishedProductStockIn()` | 已能在成品入库工序完成时生成 `repository_assemble` 并审核，消耗部件库存、入成品库存。 |
| Shopify 状态同步 | `CustomOrderStageSyncService` | 定制履约状态变化后可同步 Shopify 自定义订单状态。 |

结论：

1. 不需要新建一套独立生产工单系统。
2. 生产后半段以 `custom_item_process` 作为“订单级工单/工序视图”。
3. 真正改变库存的动作继续交给 `repository_receipt` 和 `repository_assemble`。
4. 委外镜片入库、组装、质检、成品入库都要回写 `custom_order_item` 和 Shopify 自定义订单状态。

## 总体流程

```text
镜片委外单发货
  -> 委外单接收入库
  -> 左右镜片 custom_item_component.status = 50 已完成
  -> lens_stock_in 工序完成
  -> 检查镜框 + 左镜片 + 右镜片是否齐套
  -> 自动生成或启动组装工单
  -> 工人完成组装
  -> 自动进入质检工序
  -> 质检通过
  -> 成品入库工序完成
  -> 生成 repository_assemble
  -> 消耗镜框/镜片库存，入库一个眼镜成品
  -> 发起或解锁后续发货流程
```

异常分支：

```text
质检不通过
  -> 可选择返工
      -> 组装工序重新打开
      -> 记录返工原因和质检结果
  -> 可选择异常处理
      -> custom_order_item.status = 80
      -> 工序/部件标记异常
      -> 订单面板红色预警
      -> 通知运营人员
```

## 一、委外入库方案

### 1.1 触发入口

继续复用现有接口：

```text
POST /admin/entrust/receiveStockIn
```

入参建议：

| 参数 | 说明 |
| --- | --- |
| `id` | 委外单 ID |
| `num` | 接收入库数量，可为空，默认剩余待入库数量 |
| `repository_id` | 镜片入库仓位，不传时使用镜片产品默认仓位 |
| `admin_remark` | 入库备注 |

### 1.2 入库动作

执行 `Entrust::receiveStockIn()`：

1. 校验委外单已审核。
2. 校验委外单已发货。
3. 创建 `repository_receipt`：
   - `type = TYPE_ENTRUST_IN`
   - `pm = PM_IN`
   - `entrust_id = entrust.id`
4. 创建 `repository_receipt_detail`：
   - `product_id = 镜片产品`
   - `supplier_id = 镜片供应商`
   - `repository_id = 镜片仓位`
   - `num = 入库数量`
5. 自动审核入库单，增加镜片库存。
6. 回写 `custom_item_component`：
   - `status = 50`
   - `actual_finish_time = 入库时间`
   - `repository_receipt_detail_id` 建议补充回写。

### 1.3 需要补强的点

当前已有委外入库和部件状态回写，但建议补强：

1. `custom_item_component.repository_receipt_detail_id`
   - 委外入库生成明细后，应把镜片部件关联到具体入库明细。
   - 成品组装入库时可以直接知道镜片来自哪个仓位。

2. 部分入库处理
   - 如果左右镜片分开委外或分批入库，只允许已入库的镜片部件变为完成。
   - 同一委外单包含多片时，需要按 `custom_item_component` 粒度匹配入库数量。

3. 入库后触发齐套检查
   - 镜片入库后调用“齐套检查服务”。
   - 只有镜框、左镜片、右镜片都完成，才自动启动组装工序。

## 二、工单管理方案

### 2.1 工单模型选择

不建议新增 `production_work_order` 表作为第一阶段实现。

本期以 `custom_item_process` 作为定制眼镜生产工单：

```text
custom_order_item = 一副眼镜的履约单元
custom_item_process = 这副眼镜的生产工序/工单节点
```

标准工序建议：

| 工序 | `process_type` | 说明 |
| --- | --- | --- |
| 镜片入库 | `lens_stock_in` | 委外镜片接收入库后自动完成 |
| 组装 | `assemble` | 工人领取镜框和镜片后组装 |
| 质检 | `qc` | 组装完成后自动进入 |
| 成品入库 | `stock_in` | 质检通过后入库成品 |

### 2.2 工单生成时机

推荐采用“订单审核后生成全流程工序，镜片入库后启动组装”的方式：

```text
订单审核通过
  -> 生成 custom_order_item
  -> 生成 custom_item_component
  -> 生成 custom_item_process: lens_stock_in / assemble / qc / stock_in
  -> 初始 status = 0 待开始
```

当镜片入库后：

```text
Entrust::receiveStockIn()
  -> lens_stock_in.status = 20
  -> 检查部件齐套
  -> assemble.status = 10
  -> 分派给工人
```

### 2.3 分派工人

优先复用 `custom_item_process` 字段：

| 字段 | 用途 |
| --- | --- |
| `main_admin_id` 或 `owner_admin_id` | 工序负责人/工人 |
| `plan_start_time` | 计划开始时间 |
| `plan_finish_time` | 计划完成时间 |
| `actual_start_time` | 实际开始时间 |
| `actual_finish_time` | 实际完成时间 |
| `status` | 工序状态 |

如果现有 `custom_item_process` 尚无负责人字段，建议补：

```sql
ALTER TABLE `hg_custom_item_process`
  ADD `main_admin_id` int(11) unsigned DEFAULT NULL COMMENT '工序负责人' AFTER `supplier_id`,
  ADD `work_group_id` int(11) unsigned DEFAULT NULL COMMENT '生产小组ID' AFTER `main_admin_id`;
```

第一阶段可先只补 `main_admin_id`，生产小组用于后续数据大屏。

### 2.4 工单列表

建议新增或扩展接口：

```text
GET  /admin/custom-item-process/workOrderList
POST /admin/custom-item-process/assign
POST /admin/custom-item-process/start
POST /admin/custom-item-process/finish
POST /admin/custom-item-process/rollback
POST /admin/custom-item-process/abnormal
```

筛选条件：

- `process_type`
- `status`
- `main_admin_id`
- `work_group_id`
- `sale_order_id`
- `custom_order_item_id`
- `plan_finish_time`
- 是否超期

## 三、组装工序方案

### 3.1 启动条件

组装工序启动前必须满足：

1. 镜框部件完成：
   - 库存锁定完成，或采购入库完成。
2. 左镜片完成：
   - 委外入库完成。
3. 右镜片完成：
   - 委外入库完成。
4. 当前 `assemble` 工序未完成且前序工序已完成。

建议封装：

```text
CustomFulfillmentProcessService::tryStartAssemble(custom_order_item_id)
```

职责：

1. 检查 `custom_item_component.status`。
2. 检查前序 `custom_item_process`。
3. 将 `assemble.status` 从 `0` 改为 `10`。
4. 写入 `custom_process_log`。
5. 同步 `custom_order_item.status = 20`。
6. 触发 Shopify 状态同步。

### 3.2 工人操作

组装页面展示：

- 销售订单内部编号，可按权限控制展示。
- 成品产品信息。
- 镜框产品信息。
- 左右镜片产品信息。
- 左右镜片定制参数。
- 部件仓位/库存来源。
- 当前工序负责人。
- 操作按钮：开始、完成、异常、返工备注。

组装完成：

```text
POST /admin/custom-item-process/finish
process_type = assemble
```

处理：

1. `assemble.status = 20`
2. `actual_finish_time = now`
3. 自动启动下一道 `qc` 工序：
   - `qc.status = 10`
4. `custom_order_item.status = 30`
5. 写日志。

## 四、质检工序方案

### 4.1 质检单据选择

现有 `qc_receipt` / `qc_receipt_detail` 偏单据式质检，可继续复用。

定制眼镜组装质检建议：

```text
custom_item_process.process_type = qc
custom_item_process.ref_module = qc_receipt
custom_item_process.ref_id = qc_receipt.id
```

第一阶段也可以先用 `custom_item_process` 的状态和异常字段完成轻量质检，不强制生成 `qc_receipt`。

推荐分两步：

1. 后端先实现轻量质检：通过/不通过/返工/异常。
2. 如果需要正式质检报告、图片、质检项目，再生成 `qc_receipt` 和 `qc_receipt_detail`。

### 4.2 质检结果

建议状态：

| 结果 | 处理 |
| --- | --- |
| 通过 | `qc.status = 20`，自动启动 `stock_in` |
| 不通过返工 | `qc.status = 80`，`assemble.status` 回退到 `10` 或新增返工组装工序 |
| 不通过异常 | `qc.status = 80`，`custom_order_item.status = 80`，订单面板预警 |
| 报废重做 | 生成重做记录，重新生成需要返工的部件/工序 |

建议接口：

```text
POST /admin/custom-item-process/qcPass
POST /admin/custom-item-process/qcReject
```

`qcReject` 入参：

| 参数 | 说明 |
| --- | --- |
| `id` | 质检工序 ID |
| `reject_type` | `rework` / `abnormal` / `remake` |
| `reason` | 不通过原因 |
| `images` | 质检图片 |
| `rollback_process_type` | 回退到哪个工序，默认 `assemble` |

### 4.3 回退和日志

质检不通过不能只改状态，必须记录：

1. 原状态。
2. 新状态。
3. 回退目标。
4. 责任工序。
5. 原因。
6. 操作人。
7. 附件图片。

继续使用：

```text
custom_process_log
```

事件建议：

| `event` | 说明 |
| --- | --- |
| `qc_pass` | 质检通过 |
| `qc_reject` | 质检不通过 |
| `process_rollback` | 工序回退 |
| `rework_start` | 返工开始 |
| `abnormal_created` | 异常创建 |

## 五、生产入库方案

### 5.1 入库对象

生产入库不是把三个部件分别入库，而是：

```text
消耗镜框 + 左镜片 + 右镜片库存
  -> 入库 1 个成品眼镜
```

成品来源：

```text
custom_order_item.product_id
```

部件来源：

```text
custom_item_component.product_id
custom_item_component.repository_freeze_log_id
custom_item_component.repository_receipt_detail_id
```

### 5.2 执行动作

继续复用现有：

```text
CustomItemProcess::finishFinishedProductStockIn()
```

该方法已经做了核心动作：

1. 检查定制履约单元和成品产品。
2. 找到成品默认入库仓位。
3. 遍历所有部件。
4. 找部件仓位：
   - 优先 `repository_receipt_detail_id`
   - 再用 `repository_freeze_log_id`
   - 再查可用库存
5. 生成 `repository_assemble`。
6. 审核 `repository_assemble`。
7. 自动生成配件出库单和成品入库单。
8. 回写 `stock_in` 工序的 `ref_module/ref_id`。

### 5.3 需要补强的点

建议补强以下细节：

1. 部件入库明细回写
   - 委外镜片入库后必须写 `custom_item_component.repository_receipt_detail_id`。
   - 采购镜框入库后也应回写对应部件的入库明细。

2. 成品入库后回写
   - `custom_item_component.status = 50`
   - `custom_order_item.status = 40`
   - `custom_item_process.stock_in.status = 20`
   - `custom_item_process.stock_in.ref_module = repository_assemble`
   - `custom_item_process.stock_in.ref_id = repository_assemble.id`

3. 发货流程触发
   - 成品入库后可自动生成或解锁发货计划。
   - 如果已有销售订单发货计划，更新发货计划明细为可出库。
   - 如果没有发货计划，可由客服/仓库手动创建。

## 六、状态设计

### 6.1 定制履约单元状态

继续使用 `custom_order_item.status`：

| 状态 | 说明 |
| --- | --- |
| `0` | 待处理 |
| `10` | 准备中 |
| `20` | 组装中 |
| `30` | 质检中 |
| `40` | 已生产入库 |
| `50` | 已交付 |
| `80` | 异常 |
| `90` | 取消 |

### 6.2 部件状态

继续使用 `custom_item_component.status`：

| 状态 | 说明 |
| --- | --- |
| `0` | 待处理 |
| `10` | 已备齐 |
| `20` | 采购中 |
| `30` | 委外中 |
| `40` | 生产中 |
| `50` | 已完成 |
| `80` | 异常 |
| `90` | 取消 |

### 6.3 工序状态

继续使用 `custom_item_process.status`：

| 状态 | 说明 |
| --- | --- |
| `0` | 待开始 |
| `10` | 进行中 |
| `20` | 已完成 |
| `80` | 异常 |
| `90` | 取消 |

### 6.4 Shopify 自定义订单状态

生产管理各节点和 Shopify 自定义状态映射：

| 内部节点 | Shopify 阶段 |
| --- | --- |
| 镜片/镜框备料中 | `CUSTOM_ORDER_STAGE = 10` 准备中 |
| 组装开始 | `CUSTOM_ORDER_STAGE = 20` 组装中 |
| 质检开始 | `CUSTOM_ORDER_STAGE = 30` 质检中 |
| 成品入库完成 | `CUSTOM_ORDER_STAGE = 40` 已生产入库 |
| 发货完成 | `CUSTOM_ORDER_STAGE = 50` 已发货 |
| 任意异常 | `CUSTOM_ORDER_STAGE = 80` 异常 |

## 七、接口设计

### 7.1 委外入库

已存在：

```text
POST /admin/entrust/receiveStockIn
```

建议补充：

```text
POST /admin/entrust/receiveStockInAndTriggerAssemble
```

用途：委外接收入库后立即执行齐套检查，满足条件时自动启动组装。

### 7.2 工单管理

建议新增：

```text
GET  /admin/custom-item-process/workOrderList
POST /admin/custom-item-process/assign
POST /admin/custom-item-process/start
POST /admin/custom-item-process/finish
POST /admin/custom-item-process/rollback
POST /admin/custom-item-process/abnormal
```

### 7.3 组装

建议新增：

```text
GET  /admin/custom-item-process/assembleDetail?id={process_id}
POST /admin/custom-item-process/assembleFinish
```

### 7.4 质检

建议新增：

```text
GET  /admin/custom-item-process/qcDetail?id={process_id}
POST /admin/custom-item-process/qcPass
POST /admin/custom-item-process/qcReject
```

### 7.5 成品入库

建议新增：

```text
POST /admin/custom-item-process/stockInFinish
```

内部调用：

```text
CustomItemProcess::finishFinishedProductStockIn()
```

## 八、数据结构调整建议

### 8.1 `custom_item_process`

建议补字段：

```sql
ALTER TABLE `hg_custom_item_process`
  ADD `main_admin_id` int(11) unsigned DEFAULT NULL COMMENT '工序负责人' AFTER `supplier_id`,
  ADD `work_group_id` int(11) unsigned DEFAULT NULL COMMENT '生产小组ID' AFTER `main_admin_id`,
  ADD `reject_type` varchar(32) DEFAULT NULL COMMENT '质检不通过类型' AFTER `abnormal_reason`,
  ADD `reject_reason` varchar(500) DEFAULT NULL COMMENT '质检不通过原因' AFTER `reject_type`,
  ADD `reject_images` text DEFAULT NULL COMMENT '质检不通过图片' AFTER `reject_reason`;
```

如果当前阶段不做生产小组大屏，`work_group_id` 可延后。

### 8.2 `custom_item_component`

已有字段应继续使用：

```text
repository_freeze_log_id
repository_receipt_detail_id
purchase_order_detail_id
entrust_id
status
actual_finish_time
```

建议保证以下回写完整：

| 场景 | 回写字段 |
| --- | --- |
| 镜框库存锁定 | `repository_freeze_log_id` |
| 镜框采购入库 | `repository_receipt_detail_id` |
| 镜片委外入库 | `repository_receipt_detail_id` |
| 部件完成 | `status = 50` |

### 8.3 `custom_process_log`

建议不新增表，继续复用。

可规范 `event`：

```text
stock_in_received
work_order_assigned
assemble_started
assemble_finished
qc_started
qc_pass
qc_reject
process_rollback
finished_product_stock_in
```

## 九、服务拆分建议

建议新增服务：

```text
App\Service\Common\CustomProductionFlowService
```

职责：

1. `tryCompleteLensStockInByEntrust(Entrust $entrust)`
2. `tryStartAssemble(int $customOrderItemId)`
3. `assignProcess(CustomItemProcess $process, int $adminId)`
4. `finishAssemble(CustomItemProcess $process, int $adminId)`
5. `passQc(CustomItemProcess $process, int $adminId)`
6. `rejectQc(CustomItemProcess $process, array $data, int $adminId)`
7. `finishStockIn(CustomItemProcess $process, int $adminId)`
8. `syncOrderAndShopify(int $customOrderItemId)`

不要把这些流程继续堆到 Controller 中。Controller 只做参数校验、权限校验和调用服务。

## 十、权限与页面

### 10.1 角色

建议角色：

| 角色 | 权限 |
| --- | --- |
| 生产主管 | 查看全部工单、分派工人、处理异常 |
| 组装工人 | 查看分派给自己的组装工单，开始/完成组装 |
| 质检员 | 查看质检工单，质检通过/不通过 |
| 仓库人员 | 查看成品入库、发货计划 |
| 运营/客服 | 查看订单生产进度和异常 |

### 10.2 页面

建议页面：

```text
生产工单列表
组装工单详情
质检工单详情
生产异常列表
成品入库记录
```

订单面板需要展示：

- 镜框状态。
- 左镜片状态。
- 右镜片状态。
- 组装状态。
- 质检状态。
- 成品入库状态。
- 异常原因。
- 预计完成时间。
- 当前负责人。

## 十一、异常与预警

### 11.1 超期预警

预警对象：

1. 镜片委外超过预计交付时间未入库。
2. 镜片入库后组装未及时开始。
3. 组装超时未完成。
4. 质检超时未完成。
5. 质检不通过未处理。

建议通过计划时间判断：

```text
custom_item_process.plan_finish_time < now
and custom_item_process.status in (0, 10)
```

处理：

- `custom_item_process.is_abnormal = 1`
- `custom_item_process.abnormal_reason = 超期原因`
- `custom_order_item.abnormal_status = 1`
- 订单面板红色展示
- 发送系统消息给负责人/运营

### 11.2 返工

返工不建议直接覆盖原工序记录。

推荐两种方式：

1. 简化方式：
   - `assemble.status = 10`
   - `qc.status = 80`
   - 记录 `custom_process_log`

2. 严谨方式：
   - 原组装/质检保留完成和失败记录。
   - 新增一组返工工序：
     - `assemble_rework`
     - `qc_recheck`
   - 通过 `remake_from_id` 或 `parent_process_id` 关联。

第一阶段建议采用简化方式，后续再扩展返工工序链。

## 十二、实施顺序

### 第一阶段：打通后半段主流程

1. 委外入库后回写 `repository_receipt_detail_id`。
2. 镜片入库后执行齐套检查。
3. 齐套后自动启动 `assemble` 工序。
4. 组装完成后自动启动 `qc` 工序。
5. 质检通过后自动启动 `stock_in` 工序。
6. 成品入库工序完成后调用 `repository_assemble`，消耗三个部件库存并入库一个成品。

### 第二阶段：工单分派和页面

1. 增加工序负责人字段。
2. 增加工单列表接口。
3. 增加分派、开始、完成、异常接口。
4. 增加组装和质检页面所需详情接口。

### 第三阶段：质检与返工

1. 实现 `qcPass`。
2. 实现 `qcReject`。
3. 支持返工回退到组装。
4. 支持异常标记和运营通知。
5. 如需正式质检报告，接入 `qc_receipt`。

### 第四阶段：预警和看板

1. 定时扫描超期工序。
2. 写入异常状态。
3. 订单面板展示异常。
4. 生产进度看板统计：
   - 待组装
   - 组装中
   - 待质检
   - 质检中
   - 待入库
   - 已入库
   - 异常

## 十三、关键注意点

1. 成品入库必须入一个成品，不是把镜框、左镜片、右镜片分别作为最终库存。
2. 成品入库必须消耗三个部件库存，避免部件库存和成品库存同时虚增。
3. 委外入库和采购入库必须能追踪到具体 `repository_receipt_detail_id`。
4. 工序状态变更必须写 `custom_process_log`，否则返工和异常无法审计。
5. 质检不通过不能直接删除或覆盖原工序，应保留失败记录。
6. 供应商只能处理委外生产，不应看到组装、质检、成品入库内部工序。
7. Shopify 订单状态同步应由 `CustomOrderStageSyncService` 统一处理，业务代码只更新内部履约状态。

