liyong
8 天以前 1ab2b329a40decba8ff7eefbeb158ff259aceb6d
追溯+分类接口
已添加17个文件
已修改29个文件
2237 ■■■■■ 文件已修改
.claude/plan.md 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/batch_traceability_frontend.md 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_followup_record_fix.md 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/feedback_traceability_plan.md 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/item_type_integration.md 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_item_sync_mes.md 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/process_backflush_flag.md 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/batch_traceability_fix.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/task_pending_feedback_ledger.md 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApi.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemTypeApi.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemTypeRespDTO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemTypeApiImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackTraceRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemTypeMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/feedback/MesProFeedbackMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/unitmeasure/MesMdUnitMeasureServiceImpl.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
# å¾…报工台账 æŽ¥å£æ–¹æ¡ˆ
## éœ€æ±‚
排产后需要一个"待报工台账":展示已排产但尚未报工完的任务,核心是 **待报工数量 = æŽ’产数量 âˆ’ å·²æŠ¥å·¥æ•°é‡**。
数据源用 `mes_pro_task`(任务级汇总,正确--待报工天然是任务级概念;已生产数量 producedQuantity ç”±æŠ¥å·¥å®¡æ‰¹ç´¯åŠ å›žå†™ï¼Œæ— éœ€çŽ°ç®— feedback)。
## æ ¸å¿ƒè¿‡æ»¤ä¸Žå­—段
- **过滤**:`status = PREPARE(0)`(进行中,排除已完成/已取消)AND `(quantity - IFNULL(produced_quantity,0)) > 0`(仍有待报工量)
- **新增展示字段**:
  - `pendingQuantity` å¾…报工数量 = `quantity - producedQuantity`
  - `feedbackRate` æŠ¥å·¥å®Œæˆçއ = `producedQuantity / quantity Ã— 100`(排产数量为 0 æ—¶ç»™ 0)
- å…¶ä½™å±•示字段复用现有 `MesProTaskRespVO`(工单号、产品、工序、工位、客户、排产/已生产/合格/不良数量、需求日期等,已由 `buildTaskRespVOList` æ‰¹é‡å…³è”拼好)
## æ”¹åŠ¨æ¸…å•
### 1. `MesProTaskRespVO` åŠ ä¸¤ä¸ªå­—æ®µ
`controller/admin/pro/task/vo/MesProTaskRespVO.java`,在 `unqualifyQuantity` ä¹‹åŽï¼š
```java
@Schema(description = "待报工数量", example = "50.00")
private BigDecimal pendingQuantity;
@Schema(description = "报工完成率(%)", example = "50.00")
private BigDecimal feedbackRate;
```
### 2. `MesProTaskController.buildTaskRespVOList` è®¡ç®—这两个字段
在 `convertList` çš„ lambda é‡Œï¼Œ`BeanUtils.toBean` ä¹‹åŽè¿½åŠ ï¼š
```java
BigDecimal quantity = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity();
BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
vo.setPendingQuantity(quantity.subtract(produced));
vo.setFeedbackRate(quantity.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO
        : produced.multiply(new BigDecimal("100")).divide(quantity, 2, RoundingMode.HALF_UP));
```
> çŽ°æœ‰ page / get / gantt éƒ½èµ° `buildTaskRespVOList`,加这两列后它们也会带出,无害且更完整。
### 3. `MesProTaskMapper` åŠ  `selectPendingFeedbackPage`
`dal/mysql/pro/task/MesProTaskMapper.java`,仿 `selectPage`,强制 `status=PREPARE` + å¾…报工量>0,复用其余过滤条件,不需要 checkFlag join:
```java
default PageResult<MesProTaskDO> selectPendingFeedbackPage(MesProTaskPageReqVO reqVO) {
    MPJLambdaWrapperX<MesProTaskDO> query = new MPJLambdaWrapperX<>();
    query.selectAll(MesProTaskDO.class);
    query.eq(MesProTaskDO::getStatus, MesProTaskStatusEnum.PREPARE.getStatus()); // å¾…报工=进行中
    query.apply("(quantity - IFNULL(produced_quantity, 0)) > 0");                  // ä»æœ‰å¾…报工量
    query.likeIfPresent(MesProTaskDO::getCode, reqVO.getCode())
            .likeIfPresent(MesProTaskDO::getName, reqVO.getName())
            .eqIfPresent(MesProTaskDO::getWorkOrderId, reqVO.getWorkOrderId())
            .eqIfPresent(MesProTaskDO::getRouteId, reqVO.getRouteId())
            .eqIfPresent(MesProTaskDO::getProcessId, reqVO.getProcessId())
            .eqIfPresent(MesProTaskDO::getWorkstationId, reqVO.getWorkstationId())
            .betweenIfPresent(MesProTaskDO::getCreateTime, reqVO.getCreateTime())
            .orderByDesc(MesProTaskDO::getId);
    return selectPage(reqVO, query);
}
```
> `apply` ç”¨è£¸åˆ—名:本查询无 join,`quantity`/`produced_quantity` æ— æ­§ä¹‰ã€‚`reqVO.getStatus()`/`getStatuses()` åœ¨æ­¤æŽ¥å£è¢«å¿½ç•¥ï¼ˆå¼ºåˆ¶ PREPARE),符合"待报工"语义。
### 4. `MesProTaskService` + Impl åŠ æ–¹æ³•
接口加:
```java
PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO);
```
Impl:
```java
@Override
public PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO) {
    return taskMapper.selectPendingFeedbackPage(pageReqVO);
}
```
### 5. `MesProTaskController` åŠ æŽ¥å£
复用 `buildTaskRespVOList` æ‹¼å±•示字段,复用 `MesProTaskPageReqVO` åšè¿‡æ»¤ï¼š
```java
@GetMapping("/pending-feedback-page")
@Operation(summary = "获得待报工台账分页", description = "已排产且仍有待报工数量的任务(状态=进行中)")
@PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
public CommonResult<PageResult<MesProTaskRespVO>> getPendingFeedbackPage(@Valid MesProTaskPageReqVO pageReqVO) {
    PageResult<MesProTaskDO> pageResult = taskService.getPendingFeedbackPage(pageReqVO);
    return success(new PageResult<>(buildTaskRespVOList(pageResult.getList()), pageResult.getTotal()));
}
```
### 6. å‰ç«¯æ–‡æ¡£
按 frontend-docs è§„则输出 `docs/task_pending_feedback_ledger.md`:接口路径、过滤参数、响应字段(含 pendingQuantity/feedbackRate)、前端表格列建议。
### 7. ç¼–译校验
`mvn compile -pl yudao-module-mes -am -Dmaven.compiler.source=25 -Dmaven.compiler.target=25 -q`(按 compile-check è§„则)
## è¡Œä¸ºè¯´æ˜Ž
| ä»»åŠ¡æƒ…å†µ | æ˜¯å¦å‡ºçŽ°åœ¨å¾…æŠ¥å·¥å°è´¦ |
|---------|--------------------|
| çŠ¶æ€=进行中,已报工 < æŽ’产 | âœ… å‡ºçŽ°ï¼ŒpendingQuantity>0 |
| çŠ¶æ€=进行中,已报工 = æŽ’产(报满了但工单没完工)| âŒ ä¸å‡ºçŽ°ï¼ˆpendingQuantity=0)|
| çŠ¶æ€=已完成/已取消 | âŒ ä¸å‡ºçް |
| çŠ¶æ€=进行中,从未报工 | âœ… å‡ºçŽ°ï¼ŒpendingQuantity=排产数量 |
> å·²æŠ¥å·¥=排产但任务仍 PREPARE çš„任务不在台账里(没待报工量了)。若想看"所有进行中任务",用现有 `/mes/pro/task/page?status=0`。
## ä¸æ”¹åЍ项
- æŠ¥å·¥ç›¸å…³é€»è¾‘不动
- `MesProTaskPageReqVO` ä¸åŠ¨ï¼ˆå¤ç”¨ï¼‰
- `selectPage` åŽŸé€»è¾‘ä¸åŠ¨
- ä¸æ–°å»ºç‹¬ç«‹ ReqVO/RespVO(复用现有,最小改动)
## å¾…确认
- `feedbackRate`(完成率)要不要?默认加上,展示更直观;不要可去掉。
- æ˜¯å¦éœ€è¦"按待报工数量倒序"排列?当前按 id å€’序。可改 `orderByDesc(pendingQuantity)`,但 MPJ å¯¹è®¡ç®—列排序需 raw SQL,略繁。默认 id å€’序,需要再说。
.claude/settings.local.json
@@ -30,7 +30,8 @@
      "Bash(chmod +x /tmp/fix_controller.sh)",
      "Bash(/tmp/fix_controller.sh)",
      "Bash(chmod +x /tmp/fix2.sh)",
      "Bash(/tmp/fix2.sh)"
      "Bash(/tmp/fix2.sh)",
      "Bash(\"E:/soft/IntelliJ IDEA 2025.3.4/plugins/maven/lib/maven3/bin/mvn.cmd\" -DskipTests=true -q compile -pl yudao-module-mdm-api,yudao-module-mdm,yudao-module-mes -am)"
    ],
    "deny": [
      "Bash(rm -rf *)",
docs/batch_traceability_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,262 @@
# æ‰¹æ¬¡è¿½æº¯åŠŸèƒ½ - å‰ç«¯è”调方案
## æ¦‚è¿°
本次改造修复并完善了批次追溯功能,支持从批次出发的正向(原材料→成品)和反向(成品→原材料)追溯查询。
---
## ä¸€ã€æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| æ‰¹æ¬¡ç®¡ç†åˆ—表 | `/mes/wm/batch/list` | æ·»åŠ "向前追溯"、"向后追溯"按钮 |
| æ‰¹æ¬¡è¯¦æƒ… | `/mes/wm/batch/detail` | å±•示追溯结果时间线 |
---
## äºŒã€API æŽ¥å£
### 2.1 æ‰¹æ¬¡å‘前追溯
**请求方式**:GET
**请求路径**:`/mes/wm/batch/forward-list`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡ç¼–码 |
**响应示例**:
```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "id": 1,
      "code": "BATCH20250713001",
      "itemId": 100,
      "itemCode": "ITEM001",
      "itemName": "成品A",
      "workOrderId": 50,
      "workOrderCode": "WO20250713001",
      "taskId": 80,
      "taskCode": "TASK001",
      "vendorId": 20,
      "vendorCode": "VENDOR001",
      "vendorName": "供应商A",
      "clientId": 30,
      "clientCode": "CLIENT001",
      "clientName": "客户A",
      "workstationId": 40,
      "workstationCode": "WS001",
      "toolId": 50,
      "toolCode": "TOOL001"
    }
  ]
}
```
### 2.2 æ‰¹æ¬¡å‘后追溯
**请求方式**:GET
**请求路径**:`/mes/wm/batch/backward-list`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡ç¼–码 |
**响应示例**:同向前追溯
---
## ä¸‰ã€å‰ç«¯å®žçŽ°è¦ç‚¹
### 3.1 æ•°æ®èŽ·å–æ—¶æœº
用户点击"向前追溯"或"向后追溯"按钮时,调用对应接口获取追溯结果。
### 3.2 è¿½æº¯ç»“果展示
建议使用时间线组件展示追溯链路:
```html
<!-- å‘前追溯:原材料 â†’ æˆå“ -->
<el-timeline>
  <el-timeline-item
    v-for="(batch, index) in forwardList"
    :key="batch.id"
    :timestamp="formatDate(batch.createTime)"
    placement="top"
  >
    <el-card shadow="hover">
      <h4>批次:{{ batch.code }}</h4>
      <p>物料:{{ batch.itemName }} ({{ batch.itemCode }})</p>
      <p v-if="batch.workOrderCode">工单:{{ batch.workOrderCode }}</p>
      <p v-if="batch.taskCode">任务:{{ batch.taskCode }}</p>
      <p v-if="batch.vendorName">供应商:{{ batch.vendorName }}</p>
      <p v-if="batch.clientName">客户:{{ batch.clientName }}</p>
    </el-card>
  </el-timeline-item>
</el-timeline>
<!-- å‘后追溯:成品 â†’ åŽŸææ–™ -->
<el-timeline>
  <el-timeline-item
    v-for="(batch, index) in backwardList"
    :key="batch.id"
    :timestamp="formatDate(batch.createTime)"
    placement="top"
  >
    <el-card shadow="hover">
      <h4>批次:{{ batch.code }}</h4>
      <p>物料:{{ batch.itemName }} ({{ batch.itemCode }})</p>
      <p v-if="batch.workOrderCode">工单:{{ batch.workOrderCode }}</p>
      <p v-if="batch.taskCode">任务:{{ batch.taskCode }}</p>
      <p v-if="batch.vendorName">供应商:{{ batch.vendorName }}</p>
    </el-card>
  </el-timeline-item>
</el-timeline>
```
### 3.3 æ— æ•°æ®æç¤º
当追溯结果为空时,显示友好提示:
```html
<el-empty v-if="!loading && traceList.length === 0" description="暂无追溯数据">
  <template #image>
    <svg viewBox="0 0 1024 1024" width="128">
      <path d="M512 128c-199.8 0-360 160.2-360 360s160.2 360 360 360 360-160.2 360-360S711.8 128 512 128zm0 128c119.8 0 216 96.2 216 216s-96.2 216-216 216-216-96.2-216-216 96.2-216 216-216zm0 128c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96z" fill="#d9d9d9"/>
    </svg>
  </template>
</el-empty>
```
---
## å››ã€ä¸šåŠ¡æµç¨‹å›¾
### 4.1 æ‰¹æ¬¡å‘前追溯流程
```
┌─────────────┐
│  æ‰¹æ¬¡åˆ—表页  â”‚
└──────┬──────┘
       â”‚ ç‚¹å‡»"向前追溯"
       â–¼
┌─────────────────────────────────────┐
│  è°ƒç”¨ GET /mes/wm/batch/forward-list â”‚
│  å‚æ•°: code = å½“前批次编码            â”‚
└──────────────────┬──────────────────┘
                   â”‚
                   â–¼
┌─────────────────────────────────────┐
│  è¿”回该批次产出的所有下游批次列表     â”‚
│                                   â”‚
│  è¿½æº¯è·¯å¾„:                        â”‚
│  åº“存事务(消耗) â†’ æ¶ˆè€—单 â†’ æŠ¥å·¥è®°å½•  â”‚
│  â†’ åº“存事务(产出) â†’ ä¸‹æ¸¸æ‰¹æ¬¡        â”‚
└──────────────────┬──────────────────┘
                   â”‚
                   â–¼
┌─────────────────────────────────────┐
│  ä»¥æ—¶é—´çº¿å½¢å¼å±•示追溯结果             â”‚
│  å¯ç‚¹å‡»ä»»ä¸€æ¡ç›®è·³è½¬åˆ°å¯¹åº”批次详情     â”‚
└─────────────────────────────────────┘
```
### 4.2 æ‰¹æ¬¡å‘后追溯流程
```
┌─────────────┐
│  æ‰¹æ¬¡åˆ—表页  â”‚
└──────┬──────┘
       â”‚ ç‚¹å‡»"向后追溯"
       â–¼
┌─────────────────────────────────────┐
│  è°ƒç”¨ GET /mes/wm/batch/backward-list│
│  å‚æ•°: code = å½“前批次编码            â”‚
└──────────────────┬──────────────────┘
                   â”‚
                   â–¼
┌─────────────────────────────────────┐
│  è¿”回生产该批次所消耗的所有上游批次   â”‚
│                                   â”‚
│  è¿½æº¯è·¯å¾„:                        â”‚
│  åº“存事务(产出) â†’ äº§å‡ºå• â†’ æŠ¥å·¥è®°å½•  â”‚
│  â†’ åº“存事务(消耗) â†’ ä¸Šæ¸¸æ‰¹æ¬¡        â”‚
└──────────────────┬──────────────────┘
                   â”‚
                   â–¼
┌─────────────────────────────────────┐
│  ä»¥æ—¶é—´çº¿å½¢å¼å±•示追溯结果             â”‚
│  å¯ç‚¹å‡»ä»»ä¸€æ¡ç›®è·³è½¬åˆ°å¯¹åº”批次详情     â”‚
└─────────────────────────────────────┘
```
---
## äº”、注意事项
### 5.1 æ•°æ®å®Œæ•´æ€§
- è¿½æº¯åŠŸèƒ½ä¾èµ– `mes_wm_transaction` åº“存事务表的数据完整性
- ç¡®ä¿æ‰€æœ‰æ¶ˆè€—和产出操作都正确生成了库存事务记录
- `biz_type` å­—段值:`123` = ç‰©æ–™æ¶ˆè€—,`124` = äº§å“äº§å‡º
### 5.2 æ€§èƒ½è€ƒè™‘
- æ‰¹æ¬¡è¿½æº¯é‡‡ç”¨é€’归查询,最大深度限制为 20 å±‚
- å¯¹äºŽå¤æ‚生产场景,可能返回大量数据,建议分页展示或限制展示数量
### 5.3 æƒé™æŽ§åˆ¶
- è¿½æº¯æŸ¥è¯¢éœ€è¦ `mes:wm-batch:query` æƒé™
- å¦‚需编辑追溯相关数据,需相应模块的编辑权限
---
## å…­ã€ç›¸å…³æŽ¥å£æ±‡æ€»
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/batch/get | èŽ·å–æ‰¹æ¬¡è¯¦æƒ… |
| GET | /mes/wm/batch/page | èŽ·å–æ‰¹æ¬¡åˆ†é¡µåˆ—è¡¨ |
| GET | /mes/wm/batch/forward-list | æ‰¹æ¬¡å‘前追溯 |
| GET | /mes/wm/batch/backward-list | æ‰¹æ¬¡å‘后追溯 |
---
## ä¸ƒã€æ•°æ®åº“变更说明
### 7.1 æ–°å¢žå­—段
以下表已添加 `feedback_id` å­—段用于精确关联:
| è¡¨å | æ–°å¢žå­—段 | è¯´æ˜Ž |
|------|----------|------|
| mes_wm_item_consume | feedback_id | å…³è”报工记录编号 |
| mes_wm_product_produce | feedback_id | å…³è”报工记录 ID |
### 7.2 ç´¢å¼•优化
```sql
CREATE INDEX idx_item_consume_feedback_id ON mes_wm_item_consume(feedback_id);
CREATE INDEX idx_product_produce_feedback_id ON mes_wm_product_produce(feedback_id);
```
---
## å…«ã€æ›´æ–°æ—¥å¿—
| æ—¥æœŸ | ç‰ˆæœ¬ | è¯´æ˜Ž |
|------|------|------|
| 2026-07-13 | v1.0 | åˆå§‹ç‰ˆæœ¬ï¼Œå®Œæˆæ‰¹æ¬¡è¿½æº¯åŠŸèƒ½ä¿®å¤ |
docs/crm_followup_record_fix.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,302 @@
# CRM è·Ÿè¿›è®°å½•接口 - å‰ç«¯ä¿®æ”¹è¯´æ˜Ž
## é—®é¢˜æè¿°
调用 `/admin-api/crm/follow-up-record/create` æŽ¥å£æ—¶å‡ºçް JSON è§£æžé”™è¯¯ï¼š
```
Cannot deserialize value of type `java.util.ArrayList<java.lang.String>` from String value
```
---
## åŽŸå› åˆ†æž
后端字段 `picUrls` å’Œ `fileUrls` å®šä¹‰ä¸º `List<String>` ç±»åž‹ï¼ŒæœŸæœ›æŽ¥æ”¶æ•°ç»„格式的数据,但前端传递的是字符串格式。
### å½“前前端发送的数据(错误)
```json
{
  "bizId": 2,
  "bizType": 1,
  "type": 2,
  "nextTime": "1783924981689",
  "content": "4123123",
  "picUrls": "http://127.0.0.1:48080/admin-api/infra/file/29/get/xxx.png",
  "fileUrls": "http://127.0.0.1:48080/admin-api/infra/file/29/get/xxx.png"
}
```
### åŽç«¯æœŸæœ›çš„æ•°æ®ï¼ˆæ­£ç¡®ï¼‰
```json
{
  "bizId": 2,
  "bizType": 1,
  "type": 2,
  "nextTime": "1783924981689",
  "content": "4123123",
  "picUrls": ["http://127.0.0.1:48080/admin-api/infra/file/29/get/xxx.png"],
  "fileUrls": ["http://127.0.0.1:48080/admin-api/infra/file/29/get/xxx.png"]
}
```
---
## ä¿®æ”¹æ–¹æ¡ˆ
### æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| å•†æœºè¯¦æƒ… | `/crm/business/detail` | æ·»åŠ è·Ÿè¿›è®°å½• |
| å®¢æˆ·è¯¦æƒ… | `/crm/customer/detail` | æ·»åŠ è·Ÿè¿›è®°å½• |
| åˆåŒè¯¦æƒ… | `/crm/contract/detail` | æ·»åŠ è·Ÿè¿›è®°å½• |
### ä¿®æ”¹ç‚¹
#### 1. API è°ƒç”¨å‚数处理
在调用创建跟进记录接口前,将 URL å­—符串转为数组:
```javascript
// æ–‡ä»¶è·¯å¾„:src/api/crm/followUpRecord.js
import request from '@/utils/request'
/**
 * åˆ›å»ºè·Ÿè¿›è®°å½•
 */
export function createFollowUpRecord(data) {
  // å¤„理 picUrls å’Œ fileUrls:确保是数组格式
  const processedData = {
    ...data,
    picUrls: Array.isArray(data.picUrls) ? data.picUrls : (data.picUrls ? [data.picUrls] : []),
    fileUrls: Array.isArray(data.fileUrls) ? data.fileUrls : (data.fileUrls ? [data.fileUrls] : [])
  }
  return request({
    url: '/crm/follow-up-record/create',
    method: 'post',
    data: processedData
  })
}
```
#### 2. è¡¨å•提交数据处理
在提交表单时,确保数据格式正确:
```javascript
// ç¤ºä¾‹ï¼šå•†æœºè¯¦æƒ…页添加跟进记录
methods: {
  async submitFollowUp() {
    const formData = {
      bizId: this.businessId,
      bizType: 1,
      type: this.form.type,
      nextTime: this.form.nextTime,
      content: this.form.content,
      // ç¡®ä¿ picUrls å’Œ fileUrls æ˜¯æ•°ç»„
      picUrls: this.form.picUrls || [],
      fileUrls: this.form.fileUrls || []
    }
    await createFollowUpRecord(formData)
  }
}
```
#### 3. å›¾ç‰‡/附件上传后的数据处理
上传成功后,直接存储为数组格式:
```javascript
// ä¸Šä¼ å›¾ç‰‡æˆåŠŸåŽ
handleUploadSuccess(response) {
  if (!this.form.picUrls) {
    this.form.picUrls = []
  }
  this.form.picUrls.push(response.url)
}
// åˆ é™¤å›¾ç‰‡æ—¶
handleRemoveImage(index) {
  this.form.picUrls.splice(index, 1)
}
```
---
## å®Œæ•´ç¤ºä¾‹
### æ–°å¢žè·Ÿè¿›è®°å½•弹窗组件
```vue
<template>
  <el-dialog v-model="visible" title="添加跟进记录" width="600px">
    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
      <el-form-item label="跟进类型" prop="type">
        <el-select v-model="form.type" placeholder="请选择跟进类型">
          <el-option label="电话跟进" :value="1" />
          <el-option label="拜访跟进" :value="2" />
          <el-option label="微信跟进" :value="3" />
        </el-select>
      </el-form-item>
      <el-form-item label="跟进内容" prop="content">
        <el-input v-model="form.content" type="textarea" :rows="3" placeholder="请输入跟进内容" />
      </el-form-item>
      <el-form-item label="图片">
        <el-upload
          :action="uploadUrl"
          :on-success="handlePicSuccess"
          :on-remove="handlePicRemove"
          :file-list="picFileList"
          list-type="picture-card"
        >
          <el-icon><Plus /></el-icon>
        </el-upload>
      </el-form-item>
      <el-form-item label="附件">
        <el-upload
          :action="uploadUrl"
          :on-success="handleFileSuccess"
          :on-remove="handleFileRemove"
          :file-list="fileFileList"
        >
          <el-button>上传附件</el-button>
        </el-upload>
      </el-form-item>
      <el-form-item label="下次联系" prop="nextTime">
        <el-date-picker v-model="form.nextTime" type="datetime" placeholder="选择下次联系时间" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="submitForm">确定</el-button>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { createFollowUpRecord } from '@/api/crm/followUpRecord'
const visible = ref(false)
const formRef = ref()
const form = reactive({
  type: null,
  content: '',
  nextTime: null,
  picUrls: [],  // ç¡®ä¿åˆå§‹åŒ–为数组
  fileUrls: []   // ç¡®ä¿åˆå§‹åŒ–为数组
})
const rules = {
  type: [{ required: true, message: '请选择跟进类型', trigger: 'change' }],
  content: [{ required: true, message: '请输入跟进内容', trigger: 'blur' }],
  nextTime: [{ required: true, message: '请选择下次联系时间', trigger: 'change' }]
}
const picFileList = ref([])
const fileFileList = ref([])
// å›¾ç‰‡ä¸Šä¼ æˆåŠŸ
const handlePicSuccess = (response) => {
  if (!form.picUrls) form.picUrls = []
  form.picUrls.push(response.url)
  picFileList.value.push({ name: response.name, url: response.url })
}
// å›¾ç‰‡ç§»é™¤
const handlePicRemove = (file) => {
  const index = picFileList.value.indexOf(file)
  if (index > -1) {
    picFileList.value.splice(index, 1)
    form.picUrls.splice(index, 1)
  }
}
// é™„件上传成功
const handleFileSuccess = (response) => {
  if (!form.fileUrls) form.fileUrls = []
  form.fileUrls.push(response.url)
  fileFileList.value.push({ name: response.name, url: response.url })
}
// é™„件移除
const handleFileRemove = (file) => {
  const index = fileFileList.value.indexOf(file)
  if (index > -1) {
    fileFileList.value.splice(index, 1)
    form.fileUrls.splice(index, 1)
  }
}
// æäº¤è¡¨å•
const submitForm = async () => {
  await formRef.value.validate()
  await createFollowUpRecord({
    bizId: props.bizId,
    bizType: props.bizType,
    ...form
  })
  ElMessage.success('添加成功')
  visible.value = false
}
// æ‰“开弹窗
const open = (bizId, bizType) => {
  visible.value = true
  form.bizId = bizId
  form.bizType = bizType
  form.picUrls = []
  form.fileUrls = []
  picFileList.value = []
  fileFileList.value = []
}
defineExpose({ open })
</script>
```
---
## æ³¨æ„äº‹é¡¹
1. **初始化为空数组**:`picUrls` å’Œ `fileUrls` å­—段在 `data` ä¸­åˆå§‹åŒ–为空数组 `[]`,而不是空字符串 `''`
2. **上传后追加到数组**:图片/附件上传成功后,使用 `push()` æ–¹æ³•追加到数组中
3. **删除时从数组移除**:删除图片/附件时,使用 `splice()` æ–¹æ³•从数组中移除对应项
4. **提交前检查格式**:确保传递给接口的数据中,`picUrls` å’Œ `fileUrls` å§‹ç»ˆæ˜¯æ•°ç»„格式
---
## æµ‹è¯•验证
修改完成后,请进行以下测试:
1. ä¸ä¸Šä¼ å›¾ç‰‡å’Œé™„件,直接提交 â†’ åº”成功
2. ä¸Šä¼ ä¸€å¼ å›¾ç‰‡ï¼Œæäº¤ â†’ åº”成功
3. ä¸Šä¼ å¤šå¼ å›¾ç‰‡ï¼Œæäº¤ â†’ åº”成功
4. ä¸Šä¼ å›¾ç‰‡åŽåˆ é™¤ï¼Œå†æäº¤ â†’ åº”成功
---
## ç›¸å…³æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /admin-api/crm/follow-up-record/create | åˆ›å»ºè·Ÿè¿›è®°å½• |
| PUT | /admin-api/crm/follow-up-record/update | æ›´æ–°è·Ÿè¿›è®°å½• |
| DELETE | /admin-api/crm/follow-up-record/delete | åˆ é™¤è·Ÿè¿›è®°å½• |
| GET | /admin-api/crm/follow-up-record/page | åˆ†é¡µæŸ¥è¯¢è·Ÿè¿›è®°å½• |
docs/feedback_traceability_plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
# ç”Ÿäº§æŠ¥å·¥è¿½æº¯æ–¹æ¡ˆ
## ä¸€ã€è¿½æº¯ç›®æ ‡
| æ–¹å‘ | é“¾è·¯ |
|------|------|
| **正向**(订单 -> äº§å‡ºï¼‰| é”€å”®æ¥æº -> å·¥å• -> ä»»åŠ¡ -> æŠ¥å·¥ -> äº§å‡º/消耗/质检 -> åº“存事务 -> æ‰¹æ¬¡åº“å­˜ |
| **反向**(产出 -> è®¢å•)| åº“存事务/批次 -> äº§å‡º/消耗单 -> æŠ¥å·¥ -> ä»»åŠ¡ -> å·¥å• -> å®¢æˆ·/来源 |
| **横向**(按物料/批次)| ç‰©æ–™/批次 -> æ‰€æœ‰è¿›å‡ºäº‹åŠ¡ -> ç›¸å…³æŠ¥å·¥/工单 |
## äºŒã€çŽ°æœ‰è¿½æº¯èµ„äº§ï¼ˆé“¾è·¯å·²é€šçš„éƒ¨åˆ†ï¼‰
现有的 ID é“¾å·²ç»æž„成一张追溯网,**正常流程下追溯是完整的**:
```
mes_pro_work_order (工单)
  â””─> mes_pro_task (任务, work_order_id)              [待报工台账入口]
       â””─> mes_pro_feedback (报工, task_id + work_order_id)   â† åŒç»‘定,工单侧兜底
            â”œâ”€> mes_wm_product_produce / _line (产出, feedback_id)
            â”‚     â”œâ”€> mes_wm_transaction (IN, biz_type=WM_PRODUCT_PRODUCE, biz_id=produce.id)
            â”‚     â””─> mes_qc_ipqc (source_doc_id=feedback_id, source_line_id=produce_line_id)
            â”‚            â””─> mes_qc_defect_record (qc_id)
            â””─> mes_wm_item_consume / _line / _detail (消耗, feedback_id)
                  â””─> mes_wm_transaction (OUT, biz_type=WM_ITEM_CONSUME, biz_id=consume.id)
```
关键点:
- `mes_wm_transaction` è®°å½•**每一笔**库存进出(扣料 OUT / å…¥åº“ IN),带 `biz_type` + `biz_id` + `biz_line_id` + `batch_id` + `transaction_time` + `related_transaction_id`。这是最细粒度的流水,批次/物料级追溯靠它。
- feedback **同时存 `task_id` å’Œ `work_order_id`**,任一链路出问题都能从另一侧绕回。
- å·²å®¡æ‰¹ feedback ä¸å¯åˆ ï¼ˆä»…草稿可删),已回写数量的审计痕迹保留。
- `BaseDO` ä½¿ç”¨ `@TableLogic` è½¯åˆ é™¤ï¼Œ`deleteById` å®žä¸ºè½¯åˆ ï¼ˆ`deleted=1`),数据行保留,取证时可绕过过滤查询。
## ä¸‰ã€é£Žé™©ç‚¹ä¸Žå¯¹åº”方案
### é£Žé™©1:删任务导致软删引用(UX è¿½æº¯æ–­é“¾ï¼‰â­ æœ€å®žé™…
- **现状**:`MesProTaskServiceImpl.deleteTask` åªæ ¡éªŒä»»åŠ¡éžç»ˆæ€ï¼Œä¸æŸ¥æŠ¥å·¥ã€‚æŠ¥å·¥å›žå†™ä¸æ”¹ä»»åŠ¡çŠ¶æ€ï¼Œæ•…å·²è¢«æŠ¥å·¥çš„ä»»åŠ¡ä»æ˜¯ PREPARE、可删。软删后 `feedback.task_id` æŒ‡å‘ `deleted=1` çš„任务,正常查询看不到,点击追溯断。
- **方案**:`deleteTask` åŠ æ ¡éªŒâ€”â€”æœ‰æŠ¥å·¥è®°å½•çš„ä»»åŠ¡ç¦æ­¢åˆ é™¤ã€‚
  ```java
  // feedbackMapper æŒ‰ task_id æŸ¥æ•°é‡ > 0 åˆ™æŠ› PRO_TASK_HAS_FEEDBACK
  ```
- **成本**:极小(一个 count + ä¸€ä¸ªé”™è¯¯ç ï¼‰ã€‚**建议立即做。**
### é£Žé™©2:taskId å¼±ç»‘定(无 DB å¤–键,DO æ³¨é‡Š `TODO å¾…关联`)
- **现状**:靠 `validateFeedbackData` ä¸šåŠ¡æ ¡éªŒä¿è¯ task ä¸Žå·¥å•/工位/路线/工序/物料一致;DB å±‚æ—  FK。当前数据 0 æ¡ null task_id。
- **方案**:
  - **不加硬 FK**(软删除 + è·¨æ¨¡å—会冲突,yudao ä½“系惯例不用 FK)。
  - ç»™ `mes_pro_feedback.task_id`、`work_order_id` **加普通索引**,提升追溯查询性能。
  - feedback è¯¦æƒ…查询时确保能带出 task åŸºæœ¬ä¿¡æ¯ï¼ˆå³ä½¿ä»»åŠ¡è¢«è½¯åˆ ä¹Ÿæç¤º"任务已删除"而非报错)。
- **成本**:小。**建议立即做(加索引)。**
### é£Žé™©3:台账看不到在途报工(盲区,非断链)
- **现状**:台账用 `task.produced_quantity`(只含已审批 status=4)。草稿/审批中/待检验报工未回写,台账"待报工数量"偏大,与实际在途不符。
- **方案**:任务/台账增加在途报工量字段,口径明确:
  - `approvedQuantity` = è¯¥ä»»åС䏋 status=4 çš„ feedbackQuantity ä¹‹å’Œï¼ˆ= producedQuantity,冗余展示)
  - `inTransitQuantity` = è¯¥ä»»åС䏋 status IN (0,2,3) çš„ feedbackQuantity ä¹‹å’Œ
  - `pendingQuantity` = æŽ’产数量 âˆ’ approvedQuantity(台账口径不变,"待报工"指尚未报)
  - `reportableQuantity` = æŽ’产 âˆ’ approved âˆ’ inTransit(还可报的量)
- **成本**:中(一个聚合查询)。**建议一期做。**
### é£Žé™©4:无报工冲销/撤销(纠错缺口)⭐ æœ€å¤§ç¼ºå£
- **现状**:报工审批后不可撤销、无反向冲销单、`feedbackQuantity` æ ¡éªŒ >0 ä¸å…è®¸è´Ÿæ•°ã€‚数量填错只能改 DB。
- **方案**:新增**报工冲销流程**:
  - feedback è¡¨åŠ  `source_feedback_id`(冲销关联原报工)+ çŠ¶æ€åŠ  `REVERSED(已冲销)`
  - å†²é”€æ—¶åˆ›å»ºä¸€æ¡å†²é”€ feedback(数量为正,但标记为冲销类型),生成**反向库存事务**(`related_transaction_id` é…å¯¹åŽŸäº‹åŠ¡ï¼‰ï¼Œä»»åŠ¡/工单数量**反向回写**(减)
  - åŽŸ feedback ä¿ç•™ï¼ˆå®¡è®¡ç—•迹),状态置 `REVERSED`
  - æƒé™ç‹¬ç«‹ï¼ˆ`mes:pro-feedback:reverse`)
- **成本**:大(新流程、反向事务、状态机)。**建议二期。**
### é£Žé™©5:producedQuantity æ— å˜æ›´åŽ†å²
- **现状**:`setSql increment` ç´¯åŠ ï¼Œæ— ç‹¬ç«‹æµæ°´ã€‚è¦è¿˜åŽŸæŸæ—¶åˆ»å€¼å¾—æŒ‰ feedback å®¡æ‰¹æ—¶é—´ç´¯åŠ ã€‚
- **方案**:
  - **A(推荐,轻量)**:不建表,靠 feedback è®°å½•重建(feedback æœ¬èº«å°±æ˜¯æµæ°´ï¼ŒæŒ‰ `update_time` æŽ’序累加即可还原)。在任务详情加"报工历史"tab å±•示。
  - B(重):新建 `mes_pro_task_progress_log` æ¯æ¬¡å›žå†™è®°ä¸€æ¡ã€‚更严谨但维护成本高。
- **成本**:A å°ã€‚**建议一期做(加报工历史 tab)。**
## å››ã€åˆ†æœŸå®žæ–½å»ºè®®
| æœŸ | å†…容 | æˆæœ¬ | æ”¶ç›Š |
|----|------|------|------|
| **一期·堵漏** | é£Žé™©1 åˆ ä»»åŠ¡æ ¡éªŒ + é£Žé™©2 åŠ ç´¢å¼• + é£Žé™©3 åœ¨é€”报工量 + é£Žé™©5 æŠ¥å·¥åŽ†å² tab | å°ï½žä¸­ | å µä½æ–­é“¾ã€è¡¥é½ç›²åŒºï¼Œè¿½æº¯åŸºæœ¬å®Œæ•´ |
| **二期·纠错** | é£Žé™©4 æŠ¥å·¥å†²é”€æµç¨‹ | å¤§ | é—­çŽ¯çº é”™ï¼Œå®¡è®¡å®Œæ•´ |
| **三期·增强**(可选) | æ‰¹æ¬¡çº§æ­£åå‘追溯接口;任务进度流水表(B æ–¹æ¡ˆï¼‰ | ä¸­ | ç²¾ç»†è¿½æº¯ |
## äº”、推荐的追溯查询接口(一期可顺手加)
| æŽ¥å£ | å…¥å‚ | è¿”回 | ç”¨é€” |
|------|------|------|------|
| `GET /mes/pro/feedback/trace/{id}` | æŠ¥å·¥ ID | æŠ¥å·¥ + äº§å‡º/消耗/质检 å…¨é“¾ | æŒ‰æŠ¥å·¥å•追溯 |
| `GET /mes/pro/task/feedback-list?taskId=` | ä»»åŠ¡ ID | è¯¥ä»»åŠ¡æ‰€æœ‰æŠ¥å·¥ï¼ˆå«åœ¨é€”ï¼‰ | ä»»åŠ¡ -> æŠ¥å·¥æ˜Žç»† |
| `GET /mes/wm/transaction/trace-by-item?itemId=&batchId=` | ç‰©æ–™/批次 | ç›¸å…³äº‹åŠ¡ + æ¥æºå•据 | ç‰©æ–™/批次反向追溯 |
前两个是一期就该有的基本追溯能力(台账点进去能展开明细);第三个可三期。
## å…­ã€ä¸€æœŸè½åœ°åŽçš„追溯能力(预期)
做完一期后,从"待报工台账"出发的完整追溯路径:
```
待报工台账(任务行:排产/已审批/在途/待报工/可报量)
  â”‚ ç‚¹å‡»ä»»åŠ¡
  â–¼
任务详情 + æŠ¥å·¥åŽ†å² tab(按时间排列所有报工,含在途状态)
  â”‚ ç‚¹å‡»æŸæ¡æŠ¥å·¥
  â–¼
报工追溯页(报工单 + å…³è”产出/消耗/质检 + åº“存事务流水)
  â”‚ ç‚¹å‡»åº“存事务
  â–¼
批次库存变动明细(该批次所有进出)
```
且因风险1已堵,任务不会被误删导致断链;因风险2加索引,大表下追溯查询不卡。
## ä¸ƒã€ç»“论
- æ­£å¸¸æµç¨‹ä¸‹ï¼ŒçŽ°æœ‰ ID é“¾å·²æž„成完整追溯网,**追溯本身是通的**。
- ä¸»è¦é𐿂£æ˜¯"删任务不校验报工"造成的软删断链,**一期立即修**。
- å°è´¦çš„"在途报工"盲区和"报工历史"是一期补齐的重点。
- æŠ¥å·¥å†²é”€æ˜¯æœ€å¤§çš„缺口,但工程量大,建议一期稳定后再上二期。
## é™„:涉及文件清单(一期预估)
| æ”¹åЍ | æ–‡ä»¶ |
|------|------|
| åˆ ä»»åŠ¡æ ¡éªŒ | `MesProTaskServiceImpl.deleteTask`、`MesProFeedbackMapper`(加 countByTaskId)、`ErrorCodeConstants`(加 PRO_TASK_HAS_FEEDBACK) |
| åŠ ç´¢å¼• | DDL:`mes_pro_feedback` åŠ  `idx_task_id`、`idx_work_order_id` |
| åœ¨é€”报工量 | `MesProTaskRespVO` åŠ å­—æ®µã€`MesProTaskMapper`/Service åŠ èšåˆæŸ¥è¯¢ã€`buildTaskRespVOList` å¡«å…… |
| æŠ¥å·¥åŽ†å² tab | `MesProFeedbackController` åŠ  `list-by-task` æŽ¥å£ã€`MesProFeedbackMapper` åŠ  selectListByTaskId |
| è¿½æº¯æŽ¥å£ | `MesProFeedbackController` åŠ  `/trace/{id}`,组装产出/消耗/质检 |
| å‰ç«¯æ–‡æ¡£ | `docs/feedback_traceability_*` ç³»åˆ—文档 |
docs/item_type_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
# ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ mes_md_item_type - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- MDM ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料)
- MES ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ `mes_md_item_type` è¡¨ï¼ŒMDM å’Œ MES æ¨¡å—共用同一套分类数据
2. ç‰©æ–™ç±»åž‹å­—段 `itemType` ç”¨äºŽå¿«é€Ÿé€‰æ‹©ï¼š1原料、2半成品、3成品、4辅料
3. å½“选择 `categoryId` æ—¶ï¼Œç›´æŽ¥å…³è”分类 ID;当只选择 `itemType` æ—¶ï¼Œç³»ç»Ÿè‡ªåŠ¨åŒ¹é…å¯¹åº”åç§°çš„åˆ†ç±»
## API
### èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /admin-api/mes/md/item-type/list | èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| status | Integer | å¦ | çŠ¶æ€ç­›é€‰ |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | åˆ†ç±»ç¼–号 |
| code | String | åˆ†ç±»ç¼–码 |
| name | String | åˆ†ç±»åç§° |
| parentId | Long | çˆ¶åˆ†ç±»ç¼–号 |
| itemOrProduct | String | ç‰©æ–™/产品标识 |
| sort | Integer | æŽ’序 |
| status | Integer | çŠ¶æ€ |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 1,
      "code": "RAW001",
      "name": "原料",
      "parentId": 0,
      "itemOrProduct": "1",
      "sort": 1,
      "status": 0
    }
  ]
}
```
### èŽ·å–ç‰©æ–™åˆ†ç±»è¯¦æƒ…
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /admin-api/mes/md/item-type/get | èŽ·å–ç‰©æ–™åˆ†ç±»è¯¦æƒ… |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | åˆ†ç±»ç¼–号 |
### ä¿å­˜ç‰©æ–™åˆ†ç±»
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /admin-api/mes/md/item-type/create | åˆ›å»ºç‰©æ–™åˆ†ç±» |
| PUT | /admin-api/mes/md/item-type/update | æ›´æ–°ç‰©æ–™åˆ†ç±» |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | åˆ†ç±»ç¼–号(更新时必填) |
| code | String | æ˜¯ | åˆ†ç±»ç¼–码 |
| name | String | æ˜¯ | åˆ†ç±»åç§° |
| parentId | Long | å¦ | çˆ¶åˆ†ç±»ç¼–号 |
| itemOrProduct | String | å¦ | ç‰©æ–™/产品标识 |
| sort | Integer | å¦ | æŽ’序 |
| status | Integer | å¦ | çŠ¶æ€ |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| categoryId | æ–°å¢ž/编辑表单 | ç‰©æ–™åˆ†ç±»ä¸‹æ‹‰æ¡†ï¼Œæ•°æ®æ¥æºï¼šmes_md_item_type |
| itemType | æ–°å¢ž/编辑表单 | ç‰©æ–™ç±»åž‹å¿«æ·é€‰æ‹©ï¼š1原料、2半成品、3成品、4辅料 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç‰©æ–™åˆ†ç±»é€‰æ‹© | ä½¿ç”¨ `mes_md_item_type` è¡¨ï¼ŒMDM å’Œ MES å…±ç”¨åŒä¸€å¥—分类数据 |
| itemType ä¸Ž categoryId å…³ç³» | å½“ categoryId ä¸ºç©ºæ—¶ï¼Œå¯æ ¹æ® itemType è‡ªåŠ¨åŒ¹é…å¯¹åº”åç§°çš„åˆ†ç±» |
| itemType æ˜ å°„ | 1→原料,2→半成品,3→成品,4→辅料 |
| åˆ†ç±»æ ¡éªŒ | ä¿å­˜ç‰©æ–™æ—¶ï¼Œä¼šé€šè¿‡ RPC API æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨ |
## æ³¨æ„äº‹é¡¹
- ç‰©æ–™åˆ†ç±»æ•°æ®åœ¨ MES æ¨¡å—管理,MDM æ¨¡å—通过 RPC API è°ƒç”¨èŽ·å–
- `itemType` æ˜¯å¿«æ·é€‰æ‹©å­—段,最终会转换为对应的分类编码
- å¦‚果同时设置了 `categoryId` å’Œ `itemType`,优先使用 `categoryId`
- åˆ†ç±»æ”¯æŒæ ‘形结构,可通过 `parentId` æž„建层级关系
docs/mdm_item_sync_mes.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
# MDM ç‰©æ–™åŒæ­¥åˆ° MES
## æ¶‰åŠé¡µé¢
- MDM ç‰©æ–™ç®¡ç† - æ–°å¢žç‰©æ–™å¼¹çª—
- MDM ç‰©æ–™ç®¡ç† - ç¼–辑物料弹窗
## åŠŸèƒ½è¯´æ˜Ž
在 MDM ç‰©æ–™ã€Œæ–°å¢ž / ç¼–辑」时,新增「同步到 MES」开关。勾选后,MDM ç‰©æ–™ä¿å­˜æˆåŠŸçš„åŒæ—¶ï¼Œä¼šè‡ªåŠ¨åœ¨ MES ä¾§åˆ›å»º/更新一条对应的物料产品(`mes_md_item`),实现主数据向 MES çš„同步。
- æ–°å¢žï¼ˆcreate):勾选同步 â†’ MDM æ–°å»º + MES æ–°å»ºã€‚
- ç¼–辑(update):勾选同步 â†’ MDM æ›´æ–° + MES æ›´æ–°ï¼ˆMES ä¾§æŒ‰ç‰©æ–™ç¼–码 code åŒ¹é…ï¼šå·²å­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™æ–°å»ºï¼Œå³ upsert)。
- ä¸å‹¾é€‰æˆ– `syncMes=false`:仅操作 MDM,不影响 MES。
## API
接口路径不变,仅在请求体新增 `syncMes` å­—段。
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /admin-api/mdm/item/create | åˆ›å»ºç‰©æ–™ï¼ˆå¯åŒæ­¥ MES) |
| PUT | /admin-api/mdm/item/update | æ›´æ–°ç‰©æ–™ï¼ˆå¯åŒæ­¥ MES) |
**请求参数(仅列出新增/相关字段):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| syncMes | Boolean | å¦ | æ˜¯å¦åŒæ­¥åˆ° MES,默认 false。勾选时触发 MES åŒæ­¥ |
| code | String | æ˜¯ | ç‰©æ–™ç¼–码(MDM/MES å…³è”键,两边须一致) |
| name | String | æ˜¯ | ç‰©æ–™åç§° |
| unitMeasureId | Long | æ˜¯ | MDM è®¡é‡å•位 id(同步时按 code æ˜ å°„到 MES å•位) |
| categoryId | Long | æ˜¯ | MDM ç‰©æ–™åˆ†ç±» id(同步时按 code æ˜ å°„到 MES ç‰©æ–™åˆ†ç±»ï¼‰ |
**响应:** åˆ›å»ºè¿”回 `{ "code": 200, "data": <物料id> }`;更新返回 `{ "code": 200, "data": true }`(与原接口一致)。
> æ³¨æ„ï¼šåŒæ­¥å¤±è´¥æ—¶ï¼ˆå¦‚ MES ä¾§æ‰¾ä¸åˆ°å¯¹åº” code çš„单位/分类、或 MES åç§°é‡å¤ç­‰ï¼‰ï¼ŒæŽ¥å£ä¼šè¿”回错误,且 **MDM çš„创建/更新会一并回滚**(同一事务)。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è¡¨å•增加「同步到 MES」开关
在物料新增/编辑表单(`mes/pd/...` æˆ– `mdm/item` å¯¹åº”表单组件)中新增一个开关:
```html
<el-form-item label="同步到 MES">
  <el-switch v-model="formData.syncMes" />
</el-form-item>
```
### 2. data æ•°æ®
```js
data() {
  return {
    formData: {
      // ... åŽŸæœ‰å­—æ®µ
      syncMes: false, // æ˜¯å¦åŒæ­¥åˆ° MES,默认不勾选
    },
  }
}
```
> ç¼–辑回显时,`syncMes` é»˜è®¤ç½®ä¸º `false`(不同步)即可,由用户按需勾选。
### 3. æäº¤æ–¹æ³•
无需改动提交逻辑,`syncMes` éšè¡¨å•一并提交即可:
```js
submitForm() {
  this.$refs['form'].validate(valid => {
    if (!valid) return
    // formData ä¸­å·²åŒ…含 syncMes,正常调用 create / update æŽ¥å£
    if (this.formData.id) {
      updateItem(this.formData).then(...)
    } else {
      createItem(this.formData).then(...)
    }
  })
}
```
## æ³¨æ„äº‹é¡¹
- **仅 create / update ç”Ÿæ•ˆ**:删除、状态变更不同步 MES。
- **同步失败会回滚**:勾选同步后若 MES ä¾§å‡ºé”™ï¼ŒMDM çš„保存也会回滚,前端会收到错误提示,需根据错误信息处理(通常是 MES ä¾§ç¼ºå¯¹åº”单位/分类)。
- **MES ç‰©æ–™é»˜è®¤ç¦ç”¨**:同步创建的 MES ç‰©æ–™çŠ¶æ€ä¸ºã€Œç¦ç”¨ã€ï¼Œéœ€åœ¨ MES ç‰©æ–™äº§å“é¡µé¢å¦è¡Œå¯ç”¨ã€‚
- **前提:单位/分类编码须一致**:同步按 `code` æ˜ å°„——MDM çš„计量单位 `code` å¿…须在 MES `mes_md_unit_measure` ä¸­å­˜åœ¨ï¼ŒMDM çš„物料分类 `code` å¿…须在 MES `mes_md_item_type` ä¸­å­˜åœ¨ï¼ˆä¸”为叶子分类),否则同步报错回滚。请确保两边主数据编码已对齐。
- **字段映射**:MDM `isHighValue`→MES `highValue`,MDM `isBatchManaged`→MES `batchFlag`,`minStock/maxStock/specification/remark` ç›´ä¼ ï¼›MES `safeStockFlag` å›ºå®šä¸º true ä»¥ä¿ç•™åº“存上下限。
docs/process_backflush_flag.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
# å·¥åºã€Œæ˜¯å¦å€’冲」字段
## èƒŒæ™¯
报工审批时,倒冲工序按 BOM è‡ªåŠ¨æ‰£æ–™ï¼ˆBOM ç”¨é‡ Ã— æŠ¥å·¥æ•°é‡ï¼Œä»Žçº¿è¾¹åº“ FIFO æ‰£å‡ï¼‰ï¼›éžå€’冲工序不自动扣料,物料走手工领料/投料。
新增字段挂在**工艺路线工序**(`mes_pro_route_process`,与「是否关键工序」「是否质检工序」同级),不是工序主表。
## æ¶‰åŠé¡µé¢
- å·¥è‰ºè·¯çº¿ - å·¥åºåˆ—表/表单(维护路线下的工序配置,新增「是否倒冲」开关)
- ç”Ÿäº§æŠ¥å·¥ - æŠ¥å·¥å®¡æ‰¹ï¼ˆå€’冲工序未配 BOM æ—¶ç»™å‡ºé”™è¯¯æç¤ºï¼‰
## æ•°æ®åº“
```sql
ALTER TABLE mes_pro_route_process
  ADD COLUMN backflush_flag bit(1) DEFAULT b'1' COMMENT '是否倒冲' AFTER check_flag;
```
- é»˜è®¤ `b'1'`(倒冲),保留现有「配了 BOM å°±æ‰£æ–™ã€çš„行为。
- åŽ†å²æ•°æ®å›žå¡«ä¸ºå€’å†²=true。
## API
工艺路线工序接口(`/mes/pro/route-process`)的 **create / update / get / list** è¯·æ±‚与响应均新增 `backflushFlag` å­—段,其余不变。
**新增字段:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| backflushFlag | Boolean | å¦ï¼ˆé»˜è®¤ true)| æ˜¯å¦å€’冲。true=报工时按 BOM è‡ªåŠ¨æ‰£æ–™ï¼›false=不自动扣料 |
**请求示例(create/update):**
```json
{
  "routeId": 1,
  "processId": 10,
  "sort": 1,
  "keyFlag": false,
  "checkFlag": false,
  "backflushFlag": true
}
```
**响应示例(get/list):**
```json
{
  "id": 1,
  "routeId": 1,
  "processId": 10,
  "keyFlag": false,
  "checkFlag": false,
  "backflushFlag": true
}
```
报工审批接口 `PUT /mes/pro/feedback/approve` è¡Œä¸ºå˜æ›´ï¼ˆæ— å…¥å‚变化):
| å·¥åº backflushFlag | BOM é…ç½® | å®¡æ‰¹ç»“æžœ |
|--------------------|----------|----------|
| true(默认)| æœ‰ | æ­£å¸¸æ‰£æ–™å…¥åº“ |
| true | æ—  | æŠ¥é”™ï¼š`当前工序为倒冲工序,但未配置BOM消耗,无法报工`(错误码 1040506013)|
| false | æœ‰ | ä¸æ‰£æ–™ï¼ˆBOM é…ç½®å­˜åœ¨ä½†ä¸ä½¿ç”¨ï¼‰|
| false | æ—  | ä¸æ‰£æ–™ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å·¥è‰ºè·¯çº¿ - å·¥åºè¡¨å•加「是否倒冲」开关
在「是否关键工序」「是否质检工序」之后加一个 `el-switch`:
```html
<el-form-item label="是否关键工序">
  <el-switch v-model="form.keyFlag" />
</el-form-item>
<el-form-item label="是否质检工序">
  <el-switch v-model="form.checkFlag" />
</el-form-item>
<!-- æ–°å¢ž -->
<el-form-item label="是否倒冲">
  <el-switch v-model="form.backflushFlag" />
  <span style="margin-left: 8px; color: #909399; font-size: 12px;">
    å¼€å¯åŽæŠ¥å·¥æ—¶æŒ‰BOM自动扣料;关闭后不自动扣料,走手工领料
  </span>
</el-form-item>
```
### 2. data æ•°æ®
```js
data() {
  return {
    form: {
      // ...原有字段
      keyFlag: false,
      checkFlag: false,
      backflushFlag: true, // é»˜è®¤å€’冲
    },
  }
}
```
### 3. å·¥åºåˆ—表表格展示(可选)
```html
<el-table-column label="是否倒冲" align="center" width="100">
  <template #default="scope">
    <el-tag :type="scope.row.backflushFlag ? 'success' : 'info'">
      {{ scope.row.backflushFlag ? '倒冲' : '不倒冲' }}
    </el-tag>
  </template>
</el-table-column>
```
### 4. è¡¨å•重置/回显
```js
resetForm() {
  this.form = {
    // ...原有字段
    keyFlag: false,
    checkFlag: false,
    backflushFlag: true,
  }
}
handleEdit(row) {
  this.form = { ...row }
  // backflushFlag éš row å›žæ˜¾
}
```
## æ³¨æ„äº‹é¡¹
- å­—段挂在**工艺路线工序**(每条路线下的工序逐条配置),不是工序主表;与 `keyFlag`/`checkFlag` åŒçº§ï¼Œå‰ç«¯åœ¨è·¯çº¿-工序配置页维护。
- é»˜è®¤å€¼ä¸ºã€Œå€’冲=true」,与历史行为一致。只有显式关闭的工序才会跳过自动扣料。
- å€’冲工序必须在「工艺路线产品 BOM」(`/mes/pro/route-product-bom`)里配好该路线+工序+产品的 BOM æ¶ˆè€—,否则报工审批会报错。
- éžå€’冲工序的物料请通过任务投料(`/mes/pro/task-issue`)或仓库领料单手工处理。
docs/sql/batch_traceability_fix.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
-- =============================================
-- æ‰¹æ¬¡è¿½æº¯åŠŸèƒ½ä¿®å¤ - DDL è„šæœ¬
-- æ—¥æœŸ: 2026-07-13
-- è¯´æ˜Ž: æ·»åŠ  feedback_id å­—段用于批次追溯的精确关联
-- =============================================
-- 1. mes_wm_item_consume è¡¨æ·»åŠ ç¼ºå¤±å­—æ®µ
ALTER TABLE mes_wm_item_consume ADD COLUMN task_id bigint COMMENT '生产任务编号' AFTER work_order_id;
ALTER TABLE mes_wm_item_consume ADD COLUMN workstation_id bigint COMMENT '工作站编号' AFTER task_id;
ALTER TABLE mes_wm_item_consume ADD COLUMN process_id bigint COMMENT '工序编号' AFTER workstation_id;
ALTER TABLE mes_wm_item_consume ADD COLUMN feedback_id bigint COMMENT '报工记录编号' AFTER process_id;
ALTER TABLE mes_wm_item_consume ADD COLUMN consume_date datetime COMMENT '消耗日期' AFTER feedback_id;
-- 2. mes_wm_product_produce è¡¨æ·»åŠ  feedback_id å­—段
ALTER TABLE mes_wm_product_produce ADD COLUMN feedback_id bigint COMMENT '报工记录 ID' AFTER work_order_id;
-- 3. ä¸ºåé¦ˆ ID æ·»åŠ ç´¢å¼•ï¼ˆæå‡è¿½æº¯æŸ¥è¯¢æ€§èƒ½ï¼‰
CREATE INDEX idx_item_consume_feedback_id ON mes_wm_item_consume(feedback_id);
CREATE INDEX idx_product_produce_feedback_id ON mes_wm_product_produce(feedback_id);
docs/task_pending_feedback_ledger.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,161 @@
# å¾…报工台账
## èƒŒæ™¯
排产后需要一个"待报工台账":展示已排产但尚未报工完的任务,核心指标 **待报工数量 = æŽ’产数量 âˆ’ å·²æŠ¥å·¥æ•°é‡**。数据源为生产任务(`mes_pro_task`),任务级汇总视图。
## æ¶‰åŠé¡µé¢
- æ–°å¢žèœå•/页面:待报工台账(生产管理下)
- å¤ç”¨ç”Ÿäº§ä»»åŠ¡åˆ—è¡¨çš„å…³è”å±•ç¤ºï¼ˆå·¥å•/产品/工序/工位/客户)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/pro/task/pending-feedback-page | å¾…报工台账分页(进行中且仍有待报工量的任务)|
### è¯·æ±‚参数(Query,复用 MesProTaskPageReqVO)
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| pageNo | Integer | æ˜¯ | é¡µç  |
| pageSize | Integer | æ˜¯ | æ¯é¡µæ¡æ•° |
| code | String | å¦ | ä»»åŠ¡ç¼–ç ï¼ˆæ¨¡ç³Šï¼‰|
| name | String | å¦ | ä»»åŠ¡åç§°ï¼ˆæ¨¡ç³Šï¼‰|
| workOrderId | Long | å¦ | ç”Ÿäº§å·¥å•编号 |
| routeId | Long | å¦ | å·¥è‰ºè·¯çº¿ç¼–号 |
| processId | Long | å¦ | å·¥åºç¼–号 |
| workstationId | Long | å¦ | å·¥ä½œç«™ç¼–号 |
| createTime | LocalDateTime[] | å¦ | åˆ›å»ºæ—¶é—´åŒºé—´ |
> `status`/`statuses` å‚数在此接口被忽略:后端强制 `status=进行中(0)`,且过滤 `待报工数量 > 0`。
### å“åº”(MesProTaskRespVO,关键字段)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä»»åŠ¡ç¼–å· |
| code | String | ä»»åŠ¡ç¼–ç  |
| name | String | ä»»åŠ¡åç§° |
| workOrderCode / workOrderName | String | å·¥å•编码/名称 |
| itemCode / itemName / itemSpecification | String | äº§å“ç¼–码/名称/规格 |
| unitMeasureName | String | è®¡é‡å•位 |
| processName | String | å·¥åºåç§° |
| workstationCode / workstationName | String | å·¥ä½ç¼–码/名称 |
| clientName | String | å®¢æˆ·åç§° |
| quantity | BigDecimal | æŽ’产数量 |
| producedQuantity | BigDecimal | å·²æŠ¥å·¥æ•°é‡ï¼ˆå·²å®¡æ‰¹ç´¯åŠ ï¼‰|
| qualifyQuantity | BigDecimal | åˆæ ¼å“æ•°é‡ |
| unqualifyQuantity | BigDecimal | ä¸è‰¯å“æ•°é‡ |
| **pendingQuantity** | BigDecimal | **待报工数量 = æŽ’产 âˆ’ å·²æŠ¥å·¥** |
| **feedbackRate** | BigDecimal | **报工完成率(%) = å·²æŠ¥å·¥ / æŽ’产 Ã— 100** |
| startTime / endTime | LocalDateTime | æŽ’产开始/结束时间 |
| requestDate | LocalDateTime | éœ€æ±‚日期 |
| status | Integer | ä»»åŠ¡çŠ¶æ€ï¼ˆæ­¤å¤„æ’ä¸º 0=进行中)|
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 101,
        "code": "PT202507130001",
        "name": "电路板【100】个",
        "workOrderCode": "WO-001",
        "itemName": "电路板",
        "processName": "注塑",
        "workstationName": "注塑工作站",
        "quantity": 100.00,
        "producedQuantity": 30.00,
        "qualifyQuantity": 29.00,
        "unqualifyQuantity": 1.00,
        "pendingQuantity": 70.00,
        "feedbackRate": 30.00,
        "status": 0
      }
    ],
    "total": 1
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è¡¨æ ¼åˆ—建议
```html
<el-table-column label="任务编码" prop="code" />
<el-table-column label="工单编码" prop="workOrderCode" />
<el-table-column label="产品" prop="itemName" />
<el-table-column label="工序" prop="processName" />
<el-table-column label="工位" prop="workstationName" />
<el-table-column label="排产数量" prop="quantity" />
<el-table-column label="已报工" prop="producedQuantity" />
<el-table-column label="合格" prop="qualifyQuantity" />
<el-table-column label="不良" prop="unqualifyQuantity" />
<el-table-column label="待报工数量" prop="pendingQuantity">
  <template #default="scope">
    <el-tag type="warning">{{ scope.row.pendingQuantity }}</el-tag>
  </template>
</el-table-column>
<el-table-column label="完成率" prop="feedbackRate">
  <template #default="scope">
    <el-progress :percentage="Number(scope.row.feedbackRate)" />
  </template>
</el-table-column>
<el-table-column label="需求日期" prop="requestDate" />
```
### 2. æŸ¥è¯¢è¡¨å•
```html
<el-form :inline="true" :model="queryParams">
  <el-form-item label="任务编码">
    <el-input v-model="queryParams.code" clearable />
  </el-form-item>
  <el-form-item label="工单">
    <el-select v-model="queryParams.workOrderId" clearable><!-- å·¥å•下拉 --></el-select>
  </el-form-item>
  <el-form-item label="工序">
    <el-select v-model="queryParams.processId" clearable><!-- å·¥åºä¸‹æ‹‰ --></el-select>
  </el-form-item>
  <el-form-item label="工位">
    <el-select v-model="queryParams.workstationId" clearable><!-- å·¥ä½ä¸‹æ‹‰ --></el-select>
  </el-form-item>
</el-form>
```
### 3. API è°ƒç”¨
```js
// api/mes/pro/task.js
export function getPendingFeedbackPage(params) {
  return request({
    url: '/mes/pro/task/pending-feedback-page',
    method: 'get',
    params
  })
}
```
```js
// é¡µé¢ list æ–¹æ³•
getList() {
  this.loading = true
  getPendingFeedbackPage(this.queryParams).then(res => {
    this.list = res.data.list
    this.total = res.data.total
  }).finally(() => this.loading = false)
}
```
## æ³¨æ„äº‹é¡¹
- å°è´¦åªæ˜¾ç¤º **进行中 + ä»æœ‰å¾…报工量** çš„任务:已报满(待报工=0)或已完工/已取消的任务不出现。
- æƒ³çœ‹"所有进行中任务(含已报满未关单)"用 `/mes/pro/task/page?status=0`。
- `producedQuantity` åªå«**已审批通过**的报工;草稿/审批中/待检验的报工未回写,台账看不到这部分在途报工量。
- è¯¥æŽ¥å£å¤ç”¨ä»»åŠ¡åˆ†é¡µæƒé™ `mes:pro-task:query`,如需独立权限可新增按钮权限码。
pom.xml
@@ -27,6 +27,7 @@
        <module>yudao-module-erp-api</module>
<!--        <module>yudao-module-iot</module>-->
        <module>yudao-module-mes</module>
        <module>yudao-module-mes-api</module>
        <module>yudao-module-im</module>
        <!-- MDM ä¸»æ•°æ®ç®¡ç†æ¨¡å— -->
        <module>yudao-module-mdm-api</module>
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java
@@ -40,9 +40,9 @@
    @Schema(description = "关联的联系人编号数组")
    private List<Long> contactIds;
    @Schema(description = "图片")
    @Schema(description = "图片 URL åˆ—表")
    private List<String> picUrls;
    @Schema(description = "附件")
    @Schema(description = "附件 URL åˆ—表")
    private List<String> fileUrls;
}
yudao-module-mdm/pom.xml
@@ -24,6 +24,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MES åˆ¶é€ æ‰§è¡Œ API(用于同步物料主数据到 MES) -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mes-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -24,7 +24,7 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", example = "1")
    @Schema(description = "物料分类编号(关联 MES ç‰©æ–™åˆ†ç±» mes_md_item_type)", example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", example = "1")
@@ -87,4 +87,7 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "是否同步到 MES", example = "false")
    private Boolean syncMes;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemMapper.java
@@ -45,6 +45,22 @@
        return selectOne(MdmItemDO::getCode, code);
    }
    /**
     * æ ¹æ®ç¼–码和删除状态查询物料(用于解决软删除唯一键冲突)
     */
    default MdmItemDO selectByCodeAndDeleted(String code, Boolean deleted) {
        return selectOne(new LambdaQueryWrapperX<MdmItemDO>()
                .eq(MdmItemDO::getCode, code)
                .eq(MdmItemDO::getDeleted, deleted));
    }
    /**
     * ç‰©ç†åˆ é™¤ï¼ˆç”¨äºŽæ¸…理已软删除的重复记录)
     * æ³¨æ„ï¼šç”±äºŽ BaseDO æœ‰ @TableLogic,deleteById æ˜¯è½¯åˆ é™¤ï¼Œéœ€è¦ä½¿ç”¨åŽŸç”Ÿ SQL
     */
    @org.apache.ibatis.annotations.Delete("DELETE FROM mdm_item WHERE id = #{id}")
    void physicalDeleteById(Long id);
    default MdmItemDO selectByBarCode(String barCode) {
        return selectOne(MdmItemDO::getBarCode, barCode);
    }
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,17 +2,25 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
@@ -40,32 +48,107 @@
    @Resource
    private MdmBrandService brandService;
    @Resource
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createItem(MdmItemSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(createReqVO);
        // æ’å…¥
        // æ’å…¥ MDM ç‰©æ–™
        MdmItemDO item = BeanUtils.toBean(createReqVO, MdmItemDO.class);
        itemMapper.insert(item);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        if (Boolean.TRUE.equals(createReqVO.getSyncMes())) {
            syncItemToMes(createReqVO);
        }
        return item.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItem(MdmItemSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(updateReqVO);
        // æ›´æ–°
        // æ›´æ–° MDM ç‰©æ–™
        MdmItemDO updateObj = BeanUtils.toBean(updateReqVO, MdmItemDO.class);
        itemMapper.updateById(updateObj);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        if (Boolean.TRUE.equals(updateReqVO.getSyncMes())) {
            syncItemToMes(updateReqVO);
        }
    }
    private void validateItemSaveData(MdmItemSaveReqVO reqVO) {
        // æ ¡éªŒç‰©æ–™ç¼–码的唯一性
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
    }
    /**
     * åŒæ­¥ç‰©æ–™åˆ° MES(单位/分类按 code æ˜ å°„)
     *
     * MDM çš„ unitMeasureId/categoryId ä¸Ž MES çš„ unitMeasureId/itemTypeId åˆ†å±žä¸¤å¥—主数据,
     * è¿™é‡Œå…ˆåœ¨ MDM ä¾§è§£æžå‡º code,再交由 MesMdItemApi åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
     * åŒæ­¥å¤±è´¥å°†æŠ›å‡ºå¼‚常,触发外层 @Transactional æ•´ä½“回滚(MDM æ“ä½œä¸€å¹¶å›žæ»šï¼‰ã€‚
     */
    private void syncItemToMes(MdmItemSaveReqVO reqVO) {
        // è§£æžè®¡é‡å•位 id -> code
        String unitCode = null;
        if (reqVO.getUnitMeasureId() != null) {
            MdmUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasure(reqVO.getUnitMeasureId());
            if (unitMeasure == null) {
                throw exception(MDM_UNIT_MEASURE_NOT_EXISTS);
            }
            unitCode = unitMeasure.getCode();
        }
        // è§£æžåˆ†ç±» id -> code(使用 MES ç‰©æ–™åˆ†ç±» API)
        String itemTypeCode = null;
        if (reqVO.getCategoryId() != null) {
            CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(reqVO.getCategoryId());
            if (result != null && result.isSuccess() && result.getData() != null) {
                itemTypeCode = result.getData().getCode();
            }
        } else if (reqVO.getItemType() != null) {
            // æ ¹æ® itemType æŸ¥æ‰¾å¯¹åº”分类编码
            // itemType: 1原料, 2半成品, 3成品, 4辅料
            String itemTypeName = convertItemTypeToName(reqVO.getItemType());
            CommonResult<List<MesMdItemTypeRespDTO>> listResult = mesMdItemTypeApi.getItemTypeList(null);
            if (listResult != null && listResult.isSuccess() && listResult.getData() != null) {
                itemTypeCode = listResult.getData().stream()
                        .filter(c -> c.getName() != null && c.getName().equals(itemTypeName))
                        .findFirst()
                        .map(MesMdItemTypeRespDTO::getCode)
                        .orElse(null);
            }
        }
        // ç»„装同步 DTO
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
        syncReqDTO.setCode(reqVO.getCode());
        syncReqDTO.setName(reqVO.getName());
        syncReqDTO.setSpecification(reqVO.getSpecification());
        syncReqDTO.setUnitCode(unitCode);
        syncReqDTO.setItemTypeCode(itemTypeCode);
        syncReqDTO.setSafeStockFlag(Boolean.TRUE); // ä¿ç•™ MDM çš„ minStock/maxStock,避免被 MES æ¸…é›¶
        syncReqDTO.setMinStock(reqVO.getMinStock());
        syncReqDTO.setMaxStock(reqVO.getMaxStock());
        syncReqDTO.setHighValue(reqVO.getIsHighValue());
        syncReqDTO.setBatchFlag(reqVO.getIsBatchManaged());
        syncReqDTO.setRemark(reqVO.getRemark());
        // create èµ° createItem,update èµ° updateItem(MES ä¾§æŒ‰ code upsert)
        if (reqVO.getId() == null) {
            mesMdItemApi.createItem(syncReqDTO);
        } else {
            mesMdItemApi.updateItem(syncReqDTO);
        }
    }
    @Override
@@ -77,10 +160,16 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteItem(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(id);
        // åˆ é™¤
        MdmItemDO item = validateItemExists(id);
        // è§£å†³è½¯åˆ é™¤å”¯ä¸€é”®å†²çªï¼šå¦‚果已存在相同 code ä¸”已软删除的记录,先物理删除它
        MdmItemDO existingDeleted = itemMapper.selectByCodeAndDeleted(item.getCode(), true);
        if (existingDeleted != null && !existingDeleted.getId().equals(id)) {
            itemMapper.physicalDeleteById(existingDeleted.getId());
        }
        // è½¯åˆ é™¤å½“前记录
        itemMapper.deleteById(id);
    }
@@ -113,7 +202,11 @@
    }
    private void validateCategoryExists(Long categoryId) {
        if (itemCategoryService.getItemCategory(categoryId) == null) {
        if (categoryId == null) {
            return;
        }
        CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(categoryId);
        if (result == null || !result.isSuccess() || result.getData() == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
    }
@@ -130,6 +223,30 @@
        }
    }
    /**
     * å°† itemType è½¬æ¢ä¸ºä¸­æ–‡åç§°
     *
     * @param itemType ç‰©æ–™ç±»åž‹ï¼š1原料, 2半成品, 3成品, 4辅料
     * @return ä¸­æ–‡åç§°
     */
    private String convertItemTypeToName(Integer itemType) {
        if (itemType == null) {
            return null;
        }
        switch (itemType) {
            case 1:
                return "原料";
            case 2:
                return "半成品";
            case 3:
                return "成品";
            case 4:
                return "辅料";
            default:
                return null;
        }
    }
    @Override
    public MdmItemDO getItem(Long id) {
        return itemMapper.selectById(id);
yudao-module-mes-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-mes-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        mes æ¨¡å—下,制造执行系统的 API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.api.item;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
/**
 * MES ç‰©æ–™äº§å“ API æŽ¥å£
 *
 * ä¾›å…¶ä»–模块(如 MDM)同步物料主数据到 MES ä½¿ç”¨ã€‚
 * å•位、分类均按 code ä¼ é€’,由 MES ä¾§è§£æžä¸ºå¯¹åº” id。
 *
 * @author èŠ‹é“æºç 
 */
public interface MesMdItemApi {
    /**
     * åˆ›å»º MES ç‰©æ–™äº§å“ï¼ˆåŒæ­¥ç”¨ï¼‰
     *
     * @param syncReqDTO åŒæ­¥ä¿¡æ¯ï¼ˆå•位/分类传 code)
     * @return MES ç‰©æ–™ç¼–号
     */
    Long createItem(MesMdItemSyncReqDTO syncReqDTO);
    /**
     * æ›´æ–° MES ç‰©æ–™äº§å“ï¼ˆåŒæ­¥ç”¨ï¼‰
     *
     * æŒ‰ code åŒ¹é…ï¼šå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼ˆupsert)。
     *
     * @param syncReqDTO åŒæ­¥ä¿¡æ¯
     */
    void updateItem(MesMdItemSyncReqDTO syncReqDTO);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemTypeApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.mes.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MES ç‰©æ–™åˆ†ç±» API æŽ¥å£
 *
 * ä¾›å…¶ä»–模块(如 MDM)查询物料分类数据使用。
 *
 * @author èŠ‹é“æºç 
 */
public interface MesMdItemTypeApi {
    String PREFIX = "/rpc-api/mes/item-type";
    /**
     * èŽ·å–ç‰©æ–™åˆ†ç±»
     *
     * @param id åˆ†ç±»ç¼–号
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MesMdItemTypeRespDTO> getItemType(Long id);
    /**
     * èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨
     *
     * @param ids åˆ†ç±»ç¼–号列表
     * @return ç‰©æ–™åˆ†ç±»åˆ—表
     */
    CommonResult<List<MesMdItemTypeRespDTO>> getItemTypeList(Collection<Long> ids);
    /**
     * æ ¹æ®ç¼–码获取物料分类
     *
     * @param code åˆ†ç±»ç¼–码
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MesMdItemTypeRespDTO> getItemTypeByCode(String code);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "MES ç‰©æ–™äº§å“åŒæ­¥ Request DTO")
@Data
public class MesMdItemSyncReqDTO {
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "规格型号", example = "M6*20")
    private String specification;
    @Schema(description = "计量单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    private String unitCode;
    @Schema(description = "物料分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "TYPE001")
    private String itemTypeCode;
    @Schema(description = "是否启用安全库存", example = "false")
    private Boolean safeStockFlag;
    @Schema(description = "最低库存量", example = "100")
    private BigDecimal minStock;
    @Schema(description = "最高库存量", example = "10000")
    private BigDecimal maxStock;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean highValue;
    @Schema(description = "是否启用批次管理", example = "true")
    private Boolean batchFlag;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemTypeRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "RPC æœåŠ¡ - MES ç‰©æ–™åˆ†ç±» Response DTO")
@Data
public class MesMdItemTypeRespDTO {
    @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "RAW001")
    private String code;
    @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料")
    private String name;
    @Schema(description = "父分类编号", example = "0")
    private Long parentId;
    @Schema(description = "物料/产品标识", example = "1")
    private String itemOrProduct;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
}
yudao-module-mes/pom.xml
@@ -24,6 +24,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MES åˆ¶é€ æ‰§è¡Œ API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mes-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.mes.api.item;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES ç‰©æ–™äº§å“ API å®žçŽ°ç±»
 *
 * ä¾› MDM ç­‰æ¨¡å—同步物料主数据到 MES ä½¿ç”¨ã€‚
 * å•位、分类按 code åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
@Slf4j
public class MesMdItemApiImpl implements MesMdItemApi {
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Override
    public Long createItem(MesMdItemSyncReqDTO syncReqDTO) {
        log.info("[createItem] åŒæ­¥åˆ›å»º MES ç‰©æ–™äº§å“ï¼Œcode={}", syncReqDTO.getCode());
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        return itemService.createItem(saveReqVO);
    }
    @Override
    public void updateItem(MesMdItemSyncReqDTO syncReqDTO) {
        log.info("[updateItem] åŒæ­¥æ›´æ–° MES ç‰©æ–™äº§å“ï¼Œcode={}", syncReqDTO.getCode());
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        // æŒ‰ code åŒ¹é…ï¼šå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼ˆupsert)
        MesMdItemDO existItem = itemService.getItemByCode(syncReqDTO.getCode());
        if (existItem == null) {
            itemService.createItem(saveReqVO);
        } else {
            saveReqVO.setId(existItem.getId());
            itemService.updateItem(saveReqVO);
        }
    }
    /**
     * å°†åŒæ­¥ DTO è½¬æ¢ä¸º MES ç‰©æ–™äº§å“ SaveReqVO,并按 code è§£æžå•位/分类 id
     */
    private MesMdItemSaveReqVO buildSaveReqVO(MesMdItemSyncReqDTO syncReqDTO) {
        // è§£æžè®¡é‡å•位 code -> id
        MesMdUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasureByCode(syncReqDTO.getUnitCode());
        if (unitMeasure == null) {
            throw exception(MD_UNIT_MEASURE_NOT_EXISTS);
        }
        // è§£æžç‰©æ–™åˆ†ç±» code -> id
        MesMdItemTypeDO itemType = itemTypeService.getItemTypeByCode(syncReqDTO.getItemTypeCode());
        if (itemType == null) {
            throw exception(MD_ITEM_TYPE_NOT_EXISTS);
        }
        // ç»„装(code/name/specification/safeStockFlag/minStock/maxStock/highValue/batchFlag/remark åŒåæ‹·è´ï¼‰
        MesMdItemSaveReqVO saveReqVO = BeanUtils.toBean(syncReqDTO, MesMdItemSaveReqVO.class);
        saveReqVO.setUnitMeasureId(unitMeasure.getId());
        saveReqVO.setItemTypeId(itemType.getId());
        return saveReqVO;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemTypeApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MES ç‰©æ–™åˆ†ç±» API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MesMdItemTypeApi.PREFIX)
@Validated
public class MesMdItemTypeApiImpl implements MesMdItemTypeApi {
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Override
    public CommonResult<MesMdItemTypeRespDTO> getItemType(Long id) {
        MesMdItemTypeDO itemType = itemTypeService.getItemType(id);
        return success(BeanUtils.toBean(itemType, MesMdItemTypeRespDTO.class));
    }
    @Override
    public CommonResult<List<MesMdItemTypeRespDTO>> getItemTypeList(Collection<Long> ids) {
        List<MesMdItemTypeDO> list = itemTypeService.getItemTypeList(ids);
        return success(BeanUtils.toBean(list, MesMdItemTypeRespDTO.class));
    }
    @Override
    public CommonResult<MesMdItemTypeRespDTO> getItemTypeByCode(String code) {
        MesMdItemTypeDO itemType = itemTypeService.getItemTypeByCode(code);
        return success(BeanUtils.toBean(itemType, MesMdItemTypeRespDTO.class));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java
@@ -41,6 +41,19 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackTraceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
@@ -78,6 +91,17 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmProductProduceMapper productProduceMapper;
    @Resource
    private MesWmProductProduceLineMapper productProduceLineMapper;
    @Resource
    private MesWmItemConsumeMapper itemConsumeMapper;
    @Resource
    private MesWmItemConsumeLineMapper itemConsumeLineMapper;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @PostMapping("/create")
    @Operation(summary = "创建生产报工")
@@ -123,6 +147,15 @@
        return success(new PageResult<>(buildFeedbackRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/list-by-task")
    @Operation(summary = "按任务获得报工历史列表")
    @Parameter(name = "taskId", description = "任务编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:pro-feedback:query')")
    public CommonResult<List<MesProFeedbackRespVO>> getFeedbackListByTask(@RequestParam("taskId") Long taskId) {
        List<MesProFeedbackDO> list = feedbackService.getFeedbackListByTaskId(taskId);
        return success(buildFeedbackRespVOList(list));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出生产报工 Excel")
    @PreAuthorize("@ss.hasPermission('mes:pro-feedback:export')")
@@ -163,6 +196,32 @@
    // ==================== æ‹¼æŽ¥ VO ====================
    @GetMapping("/trace/{id}")
    @Operation(summary = "获得报工追溯信息", description = "报工 + äº§å‡º/产出明细 + æ¶ˆè€—/消耗明细 + IPQC检验")
    @Parameter(name = "id", description = "报工编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:pro-feedback:query')")
    public CommonResult<MesProFeedbackTraceRespVO> getFeedbackTrace(@PathVariable("id") Long id) {
        MesProFeedbackDO feedback = feedbackService.getFeedback(id);
        if (feedback == null) {
            return success(null);
        }
        MesProFeedbackTraceRespVO trace = new MesProFeedbackTraceRespVO();
        // 1. æŠ¥å·¥æ˜Žç»†ï¼ˆå¤ç”¨å±•示拼接,带工单/任务/工序/物料/报工人等显示名)
        trace.setFeedback(buildFeedbackRespVOList(ListUtil.of(feedback)).get(0));
        // 2. ç”Ÿäº§äº§å‡º + äº§å‡ºæ˜Žç»†è¡Œï¼ˆå…³é”®å·¥åºæ‰æœ‰ï¼‰
        trace.setProductProduce(productProduceMapper.selectByFeedbackId(id));
        trace.setProductProduceLines(productProduceLineMapper.selectListByFeedbackId(id));
        // 3. ç‰©æ–™æ¶ˆè€— + æ¶ˆè€—明细行(倒冲工序才有)
        MesWmItemConsumeDO itemConsume = itemConsumeMapper.selectByFeedbackId(id);
        trace.setItemConsume(itemConsume);
        if (itemConsume != null) {
            trace.setItemConsumeLines(itemConsumeLineMapper.selectListByConsumeId(itemConsume.getId()));
        }
        // 4. IPQC è¿‡ç¨‹æ£€éªŒå•(关键+需检验工序才有)
        trace.setIpqcList(ipqcMapper.selectListBySourceDoc(MesBizTypeConstants.PRO_FEEDBACK, id));
        return success(trace);
    }
    private List<MesProFeedbackRespVO> buildFeedbackRespVOList(List<MesProFeedbackDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackTraceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - æŠ¥å·¥è¿½æº¯ Response VO")
@Data
public class MesProFeedbackTraceRespVO {
    @Schema(description = "报工信息")
    private MesProFeedbackRespVO feedback;
    @Schema(description = "生产产出单(关键工序才有)")
    private MesWmProductProduceDO productProduce;
    @Schema(description = "生产产出明细行(按合格/不合格/待检验拆行)")
    private List<MesWmProductProduceLineDO> productProduceLines;
    @Schema(description = "物料消耗单(倒冲工序才有)")
    private MesWmItemConsumeDO itemConsume;
    @Schema(description = "物料消耗明细行(按 BOM ç‰©æ–™ï¼‰")
    private List<MesWmItemConsumeLineDO> itemConsumeLines;
    @Schema(description = "IPQC è¿‡ç¨‹æ£€éªŒå•列表(关键+需检验工序才有)")
    private List<MesQcIpqcDO> ipqcList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
@@ -51,6 +51,9 @@
    @Schema(description = "是否质检工序", example = "false")
    private Boolean checkFlag;
    @Schema(description = "是否倒冲(true=报工时按BOM自动扣料;false=不自动扣料)", example = "true")
    private Boolean backflushFlag;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
@@ -41,6 +41,9 @@
    @Schema(description = "是否质检工序", example = "false")
    private Boolean checkFlag;
    @Schema(description = "是否倒冲(true=报工时按BOM自动扣料;false=不自动扣料)", example = "true")
    private Boolean backflushFlag;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -41,6 +41,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import java.io.IOException;
import java.math.BigDecimal;
@@ -74,6 +77,8 @@
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProFeedbackService feedbackService;
    @PostMapping("/create")
    @Operation(summary = "创建生产任务")
@@ -116,6 +121,14 @@
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<PageResult<MesProTaskRespVO>> getTaskPage(@Valid MesProTaskPageReqVO pageReqVO) {
        PageResult<MesProTaskDO> pageResult = taskService.getTaskPage(pageReqVO);
        return success(new PageResult<>(buildTaskRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/pending-feedback-page")
    @Operation(summary = "获得待报工台账分页", description = "已排产且仍有待报工数量的任务(状态=进行中)")
    @PreAuthorize("@ss.hasPermission('mes:pro-task:query')")
    public CommonResult<PageResult<MesProTaskRespVO>> getPendingFeedbackPage(@Valid MesProTaskPageReqVO pageReqVO) {
        PageResult<MesProTaskDO> pageResult = taskService.getPendingFeedbackPage(pageReqVO);
        return success(new PageResult<>(buildTaskRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
@@ -232,9 +245,29 @@
                        .put(rp.getProcessId(), Boolean.TRUE.equals(rp.getCheckFlag()));
            }
        }
        // æŠ¥å·¥åœ¨é€”量:按任务聚合 feedbackQuantity(草稿/审批中/待检验 = åœ¨é€”;已完成 = å·²å®¡æ‰¹ï¼‰
        List<MesProFeedbackDO> taskFeedbacks = feedbackService.getFeedbackListByTaskIds(
                convertSet(list, MesProTaskDO::getId));
        Map<Long, BigDecimal> inTransitMap = new HashMap<>();
        for (MesProFeedbackDO fb : taskFeedbacks) {
            BigDecimal fbQty = fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity();
            if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())) {
                inTransitMap.merge(fb.getTaskId(), fbQty, BigDecimal::add);
            }
        }
        // æ‹¼æŽ¥ VO
        return convertList(list, task -> {
            MesProTaskRespVO vo = BeanUtils.toBean(task, MesProTaskRespVO.class);
            // å¾…报工数量 = æŽ’产数量 - å·²ç”Ÿäº§æ•°é‡ï¼›æŠ¥å·¥å®Œæˆçއ = å·²ç”Ÿäº§ / æŽ’产 Ã— 100
            BigDecimal quantity = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity();
            BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
            vo.setPendingQuantity(quantity.subtract(produced));
            vo.setFeedbackRate(quantity.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO
                    : produced.multiply(new BigDecimal("100")).divide(quantity, 2, RoundingMode.HALF_UP));
            // åœ¨é€”报工量(草稿/审批中/待检验)+ å¯æŠ¥å·¥æ•°é‡ = æŽ’产 - å·²ç”Ÿäº§ - åœ¨é€”
            BigDecimal inTransit = inTransitMap.getOrDefault(task.getId(), BigDecimal.ZERO);
            vo.setInTransitQuantity(inTransit);
            vo.setReportableQuantity(quantity.subtract(produced).subtract(inTransit).max(BigDecimal.ZERO));
            findAndThen(workOrderMap, task.getWorkOrderId(), wo ->
                    vo.setWorkOrderCode(wo.getCode()).setWorkOrderName(wo.getName()).setRequestDate(wo.getRequestDate()));
            findAndThen(workstationMap, task.getWorkstationId(), ws ->
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -73,6 +73,18 @@
    @Schema(description = "不良品数量", example = "2.00")
    private BigDecimal unqualifyQuantity;
    @Schema(description = "待报工数量", example = "50.00")
    private BigDecimal pendingQuantity;
    @Schema(description = "在途报工数量", example = "20.00")
    private BigDecimal inTransitQuantity;
    @Schema(description = "可报工数量", example = "30.00")
    private BigDecimal reportableQuantity;
    @Schema(description = "报工完成率(%)", example = "50.00")
    private BigDecimal feedbackRate;
    @Schema(description = "调整数量", example = "0")
    private BigDecimal changedQuantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java
@@ -78,6 +78,12 @@
     */
    private Boolean checkFlag;
    /**
     * æ˜¯å¦å€’冲
     *
     * true=报工时按 BOM è‡ªåŠ¨æ‰£æ–™ï¼›false=不自动扣料,物料走手工领料/投料
     */
    private Boolean backflushFlag;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemTypeMapper.java
@@ -31,6 +31,10 @@
        return selectOne(MesMdItemTypeDO::getParentId, parentId, MesMdItemTypeDO::getCode, code);
    }
    default MesMdItemTypeDO selectByCode(String code) {
        return selectOne(MesMdItemTypeDO::getName, code);
    }
    default Long selectCountByParentId(Long parentId) {
        return selectCount(MesMdItemTypeDO::getParentId, parentId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/feedback/MesProFeedbackMapper.java
@@ -7,6 +7,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES ç”Ÿäº§æŠ¥å·¥ Mapper
 *
@@ -28,4 +31,19 @@
                .orderByDesc(MesProFeedbackDO::getId));
    }
    default Long selectCountByTaskId(Long taskId) {
        return selectCount(MesProFeedbackDO::getTaskId, taskId);
    }
    default List<MesProFeedbackDO> selectListByTaskId(Long taskId) {
        return selectList(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getTaskId, taskId)
                .orderByDesc(MesProFeedbackDO::getId));
    }
    default List<MesProFeedbackDO> selectListByTaskIds(Collection<Long> taskIds) {
        return selectList(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .in(MesProFeedbackDO::getTaskId, taskIds));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -44,6 +45,23 @@
        return selectPage(reqVO, query);
    }
    default PageResult<MesProTaskDO> selectPendingFeedbackPage(MesProTaskPageReqVO reqVO) {
        MPJLambdaWrapperX<MesProTaskDO> query = new MPJLambdaWrapperX<>();
        query.selectAll(MesProTaskDO.class);
        // å¾…报工台账:进行中的任务(排除已完成/已取消),且仍有待报工量
        query.eq(MesProTaskDO::getStatus, MesProTaskStatusEnum.PREPARE.getStatus());
        query.apply("(quantity - IFNULL(produced_quantity, 0)) > 0");
        query.likeIfPresent(MesProTaskDO::getCode, reqVO.getCode())
                .likeIfPresent(MesProTaskDO::getName, reqVO.getName())
                .eqIfPresent(MesProTaskDO::getWorkOrderId, reqVO.getWorkOrderId())
                .eqIfPresent(MesProTaskDO::getRouteId, reqVO.getRouteId())
                .eqIfPresent(MesProTaskDO::getProcessId, reqVO.getProcessId())
                .eqIfPresent(MesProTaskDO::getWorkstationId, reqVO.getWorkstationId())
                .betweenIfPresent(MesProTaskDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MesProTaskDO::getId);
        return selectPage(reqVO, query);
    }
    default List<MesProTaskDO> selectListByWorkOrderId(Long workOrderId) {
        return selectList(new LambdaQueryWrapperX<MesProTaskDO>()
                .eqIfPresent(MesProTaskDO::getWorkOrderId, workOrderId)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/ipqc/MesQcIpqcMapper.java
@@ -7,6 +7,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES è¿‡ç¨‹æ£€éªŒå•(IPQC) Mapper
 *
@@ -31,4 +33,11 @@
        return selectOne(MesQcIpqcDO::getCode, code);
    }
    default List<MesQcIpqcDO> selectListBySourceDoc(Integer sourceDocType, Long sourceDocId) {
        return selectList(new LambdaQueryWrapperX<MesQcIpqcDO>()
                .eq(MesQcIpqcDO::getSourceDocType, sourceDocType)
                .eq(MesQcIpqcDO::getSourceDocId, sourceDocId)
                .orderByDesc(MesQcIpqcDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -272,6 +272,7 @@
    ErrorCode PRO_TASK_WORKSTATION_MISMATCH = new ErrorCode(1_040_503_003, "生产任务不属于当前工作站");
    ErrorCode PRO_TASK_ROUTE_PROCESS_MISMATCH = new ErrorCode(1_040_503_004, "生产任务与当前工艺路线或工序不一致");
    ErrorCode PRO_TASK_ITEM_MISMATCH = new ErrorCode(1_040_503_005, "生产任务产品与当前产品物料不一致");
    ErrorCode PRO_TASK_HAS_FEEDBACK = new ErrorCode(1_040_503_006, "该任务下存在报工记录,无法删除(避免报工追溯断链)");
    ErrorCode PRO_WORKSTATION_PROCESS_MISMATCH = new ErrorCode(1_040_503_006, "工作站所属工序与当前工序不一致");
    // ========== MES ç”Ÿäº§ç®¡ç†-生产任务投料(1-040-503-100) ==========
    ErrorCode PRO_TASK_ISSUE_NOT_EXISTS = new ErrorCode(1_040_503_100, "生产任务投料记录不存在");
@@ -299,6 +300,7 @@
    ErrorCode PRO_FEEDBACK_QUANTITY_MUST_POSITIVE = new ErrorCode(1_040_506_010, "报工数量必须大于 0");
    ErrorCode PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED = new ErrorCode(1_040_506_011, "请输入合格品和不良品数量,且合计须大于 0");
    ErrorCode PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS = new ErrorCode(1_040_506_012, "当前报工单未完成检验(待检数量:{}),无法执行报工");
    ErrorCode PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED = new ErrorCode(1_040_506_013, "当前工序为倒冲工序,但未配置BOM消耗,无法报工");
    // ========== MES ç”Ÿäº§ç®¡ç†-生产流转卡(1-040-507-000) ==========
    ErrorCode PRO_CARD_NOT_EXISTS = new ErrorCode(1_040_507_000, "生产流转卡不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemService.java
@@ -60,6 +60,14 @@
    MesMdItemDO getItem(Long id);
    /**
     * æ ¹æ®ç¼–码获得物料产品
     *
     * @param code ç‰©æ–™ç¼–码
     * @return ç‰©æ–™äº§å“
     */
    MesMdItemDO getItemByCode(String code);
    /**
     * æ ¡éªŒç‰©æ–™äº§å“å­˜åœ¨
     *
     * @param id ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -245,6 +245,11 @@
    }
    @Override
    public MesMdItemDO getItemByCode(String code) {
        return itemMapper.selectByCode(code);
    }
    @Override
    public PageResult<MesMdItemDO> getItemPage(MesMdItemPageReqVO pageReqVO) {
        // æŸ¥æ‰¾æ—¶ï¼Œå¦‚果查找某个分类编号,则包含它的子分类
        Set<Long> itemTypeIds = null;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeService.java
@@ -49,6 +49,14 @@
    MesMdItemTypeDO getItemType(Long id);
    /**
     * æ ¹æ®ç¼–码获得物料产品分类
     *
     * @param code åˆ†ç±»ç¼–码
     * @return ç‰©æ–™äº§å“åˆ†ç±»
     */
    MesMdItemTypeDO getItemTypeByCode(String code);
    /**
     * èŽ·å¾—ç‰©æ–™äº§å“åˆ†ç±»åˆ—è¡¨
     *
     * @param listReqVO æŸ¥è¯¢æ¡ä»¶
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java
@@ -148,6 +148,11 @@
    }
    @Override
    public MesMdItemTypeDO getItemTypeByCode(String code) {
        return itemTypeMapper.selectByCode(code);
    }
    @Override
    public List<MesMdItemTypeDO> getItemTypeList(MesMdItemTypeListReqVO listReqVO) {
        return itemTypeMapper.selectList(listReqVO);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/unitmeasure/MesMdUnitMeasureServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.md.unitmeasure;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.md.unitmeasure.vo.MesMdUnitMeasurePageReqVO;
@@ -14,6 +15,8 @@
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcLineService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcLineService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateIndicatorService;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import org.springframework.context.annotation.Lazy;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -23,6 +26,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -30,11 +34,16 @@
/**
 * MES è®¡é‡å•位 Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šæŸ¥è¯¢æ“ä½œç»Ÿä¸€ä½¿ç”¨ MDM API èŽ·å–æ•°æ®ï¼Œä¿æŒä¸Ž MDM ä¸»æ•°æ®ä¸€è‡´
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MesMdUnitMeasureServiceImpl implements MesMdUnitMeasureService {
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    @Resource
    private MesMdUnitMeasureMapper unitMeasureMapper;
@@ -138,16 +147,28 @@
    @Override
    public MesMdUnitMeasureDO getUnitMeasure(Long id) {
        return unitMeasureMapper.selectById(id);
        // ä¼˜å…ˆä»Ž MDM API èŽ·å–
        CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(id);
        if (result != null && result.isSuccess() && result.getData() != null) {
            return convertToDo(result.getData());
        }
        // å¦‚æžœ MDM API å¤±è´¥ï¼Œå›žé€€åˆ°æœ¬åœ°æ•°æ®åº“
        MesMdUnitMeasureDO local = unitMeasureMapper.selectById(id);
        if (local != null) {
            return local;
        }
        return null;
    }
    @Override
    public PageResult<MesMdUnitMeasureDO> getUnitMeasurePage(MesMdUnitMeasurePageReqVO pageReqVO) {
        // ä½¿ç”¨æœ¬åœ°æ•°æ®åº“分页查询(MDM API ä¸æ”¯æŒåˆ†é¡µå‚数)
        return unitMeasureMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesMdUnitMeasureDO> getUnitMeasureListByStatus(Integer status) {
        // ä½¿ç”¨æœ¬åœ°æ•°æ®åº“查询(MDM API ä¸æ”¯æŒæŒ‰çŠ¶æ€è¿‡æ»¤ï¼‰
        return unitMeasureMapper.selectListByStatus(status);
    }
@@ -156,12 +177,45 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        // ä¼˜å…ˆä»Ž MDM API èŽ·å–
        CommonResult<List<MdmUnitMeasureRespDTO>> result = unitMeasureApi.getUnitMeasureList(ids);
        if (result != null && result.isSuccess() && result.getData() != null) {
            return result.getData().stream()
                    .map(this::convertToDo)
                    .collect(Collectors.toList());
        }
        // å¦‚æžœ MDM API å¤±è´¥ï¼Œå›žé€€åˆ°æœ¬åœ°æ•°æ®åº“
        return unitMeasureMapper.selectByIds(ids);
    }
    @Override
    public MesMdUnitMeasureDO getUnitMeasureByCode(String code) {
        return unitMeasureMapper.selectByCode(code);
        // ä¼˜å…ˆä»Ž MDM API èŽ·å–
        CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasureByCode(code);
        if (result != null && result.isSuccess() && result.getData() != null) {
            return convertToDo(result.getData());
        }
        // å¦‚æžœ MDM API å¤±è´¥ï¼Œå›žé€€åˆ°æœ¬åœ°æ•°æ®åº“
        MesMdUnitMeasureDO local = unitMeasureMapper.selectByCode(code);
        if (local != null) {
            return local;
        }
        return null;
    }
    /**
     * å°† MDM DTO è½¬æ¢ä¸º MES DO
     */
    private MesMdUnitMeasureDO convertToDo(MdmUnitMeasureRespDTO dto) {
        MesMdUnitMeasureDO doObj = new MesMdUnitMeasureDO();
        doObj.setId(dto.getId());
        doObj.setCode(dto.getCode());
        doObj.setName(dto.getName());
        doObj.setPrimaryFlag(dto.getPrimaryFlag());
        doObj.setPrimaryId(dto.getPrimaryId());
        doObj.setChangeRate(dto.getChangeRate());
        doObj.setStatus(dto.getStatus());
        return doObj;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java
@@ -7,6 +7,8 @@
import jakarta.validation.Valid;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
/**
 * MES ç”Ÿäº§æŠ¥å·¥ Service æŽ¥å£
@@ -62,6 +64,22 @@
    PageResult<MesProFeedbackDO> getFeedbackPage(MesProFeedbackPageReqVO pageReqVO);
    /**
     * æŒ‰ä»»åŠ¡ç¼–å·åˆ—è¡¨æ‰¹é‡èŽ·å¾—æŠ¥å·¥è®°å½•ï¼ˆç”¨äºŽä»»åŠ¡å°è´¦èšåˆåœ¨é€”/已审批报工量)
     *
     * @param taskIds ä»»åŠ¡ç¼–å·åˆ—è¡¨
     * @return æŠ¥å·¥è®°å½•列表
     */
    List<MesProFeedbackDO> getFeedbackListByTaskIds(Collection<Long> taskIds);
    /**
     * æŒ‰ä»»åŠ¡ç¼–å·èŽ·å¾—æŠ¥å·¥è®°å½•åˆ—è¡¨ï¼ˆæŠ¥å·¥åŽ†å²ï¼Œå«è‰ç¨¿/审批中/待检验/已完成)
     *
     * @param taskId ä»»åŠ¡ç¼–å·
     * @return æŠ¥å·¥è®°å½•列表
     */
    List<MesProFeedbackDO> getFeedbackListByTaskId(Long taskId);
    /**
     * æäº¤æŠ¥å·¥ï¼ˆè‰ç¨¿ â†’ å®¡æ‰¹ä¸­ï¼‰
     *
     * @param id ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -9,6 +11,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
@@ -19,6 +22,7 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
@@ -31,6 +35,8 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -52,6 +58,8 @@
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesMdWorkstationService workstationService;
    @Resource
@@ -105,6 +113,19 @@
    }
    @Override
    public List<MesProFeedbackDO> getFeedbackListByTaskIds(Collection<Long> taskIds) {
        if (CollUtil.isEmpty(taskIds)) {
            return Collections.emptyList();
        }
        return feedbackMapper.selectListByTaskIds(taskIds);
    }
    @Override
    public List<MesProFeedbackDO> getFeedbackListByTaskId(Long taskId) {
        return feedbackMapper.selectListByTaskId(taskId);
    }
    @Override
    public PageResult<MesProFeedbackDO> getFeedbackPage(MesProFeedbackPageReqVO pageReqVO) {
        return feedbackMapper.selectPage(pageReqVO);
    }
@@ -154,10 +175,20 @@
            throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity());
        }
        // 3. ç‰©æ–™æ¶ˆè€—:根据工序 BOM ç”Ÿæˆæ¶ˆè€—记录并执行扣减
        MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
        if (itemConsume != null) {
            itemConsumeService.finishItemConsume(itemConsume.getId());
        // 3. ç‰©æ–™æ¶ˆè€—:倒冲工序按 BOM è‡ªåŠ¨æ‰£æ–™ï¼›éžå€’å†²å·¥åºè·³è¿‡ï¼ˆç‰©æ–™èµ°æ‰‹å·¥é¢†æ–™/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        if (backflushFlag) {
            // 3.1 æ ¡éªŒå€’冲工序必须配置 BOM æ¶ˆè€—,避免静默不扣料
            List<MesProRouteProductBomDO> boms = routeProductBomService.getRouteProductBomList(
                    feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
            // 3.2 ç”Ÿæˆæ¶ˆè€—记录并执行扣减(BOM Ã— æŠ¥å·¥æ•°é‡ï¼ŒFIFO æ‰£çº¿è¾¹åº“)
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
            }
        }
        // 4. å…³é”®å·¥åºï¼šç”Ÿæˆäº§å‡ºå•,并根据是否需要检验决定入库方式
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -59,6 +59,14 @@
    PageResult<MesProTaskDO> getTaskPage(MesProTaskPageReqVO pageReqVO);
    /**
     * èŽ·å¾—å¾…æŠ¥å·¥å°è´¦åˆ†é¡µï¼ˆè¿›è¡Œä¸­ä¸”ä»æœ‰å¾…æŠ¥å·¥é‡çš„ä»»åŠ¡ï¼‰
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å¾…报工任务分页
     */
    PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO);
    /**
     * æ ¹æ®å·¥å•编号列表,批量获得生产任务列表
     *
     * @param workOrderIds å·¥å•编号列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
@@ -33,6 +34,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_ALREADY_FINISHED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_HAS_FEEDBACK;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_EXISTS;
/**
@@ -46,6 +48,9 @@
    @Resource
    private MesProTaskMapper taskMapper;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    @Lazy
@@ -134,8 +139,11 @@
    public void deleteTask(Long id) {
        // 1. æ ¡éªŒå­˜åœ¨ä¸”非终态
        validateTaskNotFinished(id);
        // 2. åˆ é™¤
        // 2. æ ¡éªŒä»»åŠ¡ä¸‹æ— æŠ¥å·¥è®°å½•ï¼Œé¿å…åˆ é™¤åŽæŠ¥å·¥è¿½æº¯æ–­é“¾
        if (feedbackMapper.selectCountByTaskId(id) > 0) {
            throw exception(PRO_TASK_HAS_FEEDBACK);
        }
        // 3. åˆ é™¤
        taskMapper.deleteById(id);
    }
@@ -150,6 +158,11 @@
    }
    @Override
    public PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO) {
        return taskMapper.selectPendingFeedbackPage(pageReqVO);
    }
    @Override
    public List<MesProTaskDO> getTaskListByWorkOrderIds(Collection<Long> workOrderIds) {
        if (CollUtil.isEmpty(workOrderIds)) {
            return Collections.emptyList();
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -11,32 +11,36 @@
    <!--
        å‘前追溯:从某个原材料批次出发,查找它被哪些工单消耗后,产出了哪些成品批次
        SQL è·¯å¾„:消耗明细(icd) â†’ æ¶ˆè€—单(ic) â†’ æŠ¥å·¥è®°å½•(pf) â†’ ç”Ÿäº§å…¥åº“单(pp) â†’ å…¥åº“行(ppl)
        SQL è·¯å¾„:库存事务(消耗 OUT) â†’ æ¶ˆè€—单 â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(产出 IN) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code
        FROM mes_wm_item_consume_detail icd
        LEFT JOIN mes_wm_item_consume ic ON icd.consume_id = ic.id AND ic.deleted = 0
        FROM mes_wm_transaction tx_consume
        LEFT JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_product_produce pp ON pp.work_order_id = ic.work_order_id AND pp.deleted = 0
        LEFT JOIN mes_wm_product_produce_line ppl ON pp.id = ppl.produce_id AND ppl.deleted = 0
        WHERE icd.batch_code = #{batchCode}
        AND icd.deleted = 0
        LEFT JOIN mes_wm_transaction tx_produce ON tx_produce.biz_id = pf.id AND tx_produce.biz_type = 124 AND tx_produce.deleted = 0
        LEFT JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        WHERE tx_consume.batch_code = #{batchCode}
        AND tx_consume.biz_type = 123
        AND tx_consume.deleted = 0
        AND tx_produce.batch_id IS NOT NULL
    </select>
    <!--
        å‘后追溯:从某个成品批次出发,查找生产它时消耗了哪些原材料批次
        SQL è·¯å¾„:入库明细(ppd) â†’ å…¥åº“单(pp) â†’ æ¶ˆè€—单(ic) â†’ æ¶ˆè€—明细(icd)
        SQL è·¯å¾„:库存事务(产出 IN) â†’ äº§å‡ºå• â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(消耗 OUT) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code
        FROM mes_wm_product_produce_detail ppd
        LEFT JOIN mes_wm_product_produce pp ON ppd.produce_id = pp.id AND pp.deleted = 0
        LEFT JOIN mes_wm_item_consume ic ON pp.work_order_id = ic.work_order_id AND ic.deleted = 0
        LEFT JOIN mes_wm_item_consume_detail icd ON ic.id = icd.consume_id AND icd.deleted = 0
        WHERE ppd.batch_code = #{batchCode}
        AND ppd.deleted = 0
        AND icd.batch_code IS NOT NULL
        FROM mes_wm_transaction tx_produce
        LEFT JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = pp.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_transaction tx_consume ON tx_consume.biz_id = pf.id AND tx_consume.biz_type = 123 AND tx_consume.deleted = 0
        LEFT JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        WHERE tx_produce.batch_code = #{batchCode}
        AND tx_produce.biz_type = 124
        AND tx_produce.deleted = 0
        AND tx_consume.batch_id IS NOT NULL
    </select>
</mapper>