8 小时以前 757c7e03bee3560bb4d7766fddd03673e2c2e611
feat(mes): 完善MES模块功能并优化日志配置

- 添加error日志专用Appender,按天滚动存储并保留15天
- 实现生产反馈中的BOM物料消耗逻辑重构,统一使用工单BOM替代工艺路线BOM
- 新增工序投料BOM相关VO类和接口,支持按产品获取工序及投料明细
- 在工单工序保存时自动同步更新工单BOM投料数据
- 优化物料消耗生成逻辑,基于工单BOM进行倒冲扣料
- 添加详细的工序投料BOM集成文档说明
- 创建.mcp.json配置文件用于IDEA服务器连接
已添加3个文件
已修改11个文件
380 ■■■■■ 文件已修改
.mcp.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/route_process_bom_integration.md 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProProcessBomItemVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductBomService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductBomServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/logback-spring.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.mcp.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
{
  "mcpServers": {
    "idea": {
      "url": "http://127.0.0.1:64342/stream",
      "type": "http"
    }
  }
}
docs/route_process_bom_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
# å·¥è‰ºè·¯çº¿å·¥åºæŽ¥å£æ–°å¢žæŠ•æ–™BOM - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§å·¥å•新增/编辑页(`src/views/mes/pro/workorder/detail.vue`)
  - å­ç»„ä»¶ `modules/process-list.vue`(工序列表)
  - å­ç»„ä»¶ `modules/process-form.vue`(工序新增/编辑弹窗)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”¨æˆ·æ–°å»º/编辑工单 â†’ é€‰æ‹©äº§å“ â†’ `process-list.vue` è°ƒç”¨æŽ¥å£èŽ·å–å·¥åºåˆ—è¡¨
2. å·¥åºåˆ—表返回数据中新增 `bomItems` å­—段(工序投料明细)
3. å·¥åºå±•开行自动展示投料明细(无需额外请求)
4. ä¿å­˜å·¥å•æ—¶ `bomItems` éšå·¥åºæ•°æ®ä¸€èµ·æäº¤
## API
### æŽ¥å£è¯´æ˜Ž
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/pro/route-process/list-by-product?productId={productId}` | æŒ‰äº§å“èŽ·å–å·¥è‰ºè·¯çº¿å·¥åºï¼ˆå«æŠ•æ–™BOM) |
### å“åº”字段新增
`MesProRouteProcessRespVO` æ–°å¢žå­—段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| bomItems | ProcessBomItem[] | å·¥åºæŠ•æ–™BOM列表,无投料时为空数组或 null |
### ProcessBomItem ç»“æž„
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| itemId | Long | BOM物料编号 |
| itemCode | String | BOM物料编码 |
| itemName | String | BOM物料名称 |
| quantity | BigDecimal | ç”¨æ–™æ¯”例 |
| remark | String | å¤‡æ³¨ |
### å“åº”示例
```json
{
  "code": 0,
  "data": [
    {
      "id": 1,
      "routeId": 10,
      "processId": 100,
      "processCode": "P001",
      "processName": "焊接",
      "sort": 1,
      "nextProcessId": 101,
      "nextProcessName": "组装",
      "linkType": 3,
      "keyFlag": true,
      "checkFlag": false,
      "backflushFlag": true,
      "outputItemId": 200,
      "outputItemCode": "M200",
      "outputItemName": "半成品A",
      "bomItems": [
        {
          "itemId": 1001,
          "itemCode": "M001",
          "itemName": "钢板",
          "quantity": 2.0,
          "remark": ""
        },
        {
          "itemId": 1002,
          "itemCode": "M002",
          "itemName": "焊条",
          "quantity": 0.5,
          "remark": ""
        }
      ]
    },
    {
      "id": 2,
      "routeId": 10,
      "processId": 101,
      "processCode": "P002",
      "processName": "组装",
      "sort": 2,
      "nextProcessId": null,
      "bomItems": []
    }
  ]
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| bomItems | å·¥åºè¡¨æ ¼å±•开行 | ç‚¹å‡»å±•开按钮后以 Tag åˆ—表形式展示每个投料物料 |
| itemName / itemCode | Tag æ–‡æœ¬ | ä¼˜å…ˆæ˜¾ç¤ºç‰©æ–™åç§°ï¼Œæ— åç§°æ—¶æ˜¾ç¤ºç¼–码 |
| quantity | Tag åŽç¼€ | æ˜¾ç¤ºä¸º `×{quantity}` æ ¼å¼ |
## å‰ç«¯å¯¹æŽ¥ç¡®è®¤æ¸…单
### å·²ä¿®æ”¹æ–‡ä»¶ï¼ˆå½“前分支已有)
| æ–‡ä»¶ | ä¿®æ”¹å†…容 |
|------|----------|
| `src/api/mes/pro/route/process/index.ts` | `RouteProcess` æŽ¥å£æ–°å¢ž `bomItems?: ProcessBomItem[]` |
| `src/api/mes/pro/workorder/process/index.ts` | `WorkOrderProcess` æŽ¥å£æ–°å¢ž `bomItems?: ProcessBomItem[]` |
| `src/views/mes/pro/workorder/data.ts` | `useProcessGridColumns` æ–°å¢žå±•开列 `{ type: 'expand', width: 50 }` |
| `src/views/mes/pro/workorder/modules/process-list.vue` | `getList()` æ˜ å°„ `bomItems`,模板新增 `#expand` slot |
### éœ€ç¡®è®¤
1. `ProcessBomItem` æŽ¥å£ç±»åž‹åœ¨å‰ç«¯ API æ–‡ä»¶ä¸­å·²è¡¥å……,确认与后端返回结构一致
2. å±•开行内的投料明细 Tag åˆ—表展示按当前设计:蓝色 Tag,显示 `物料名称 Ã—用量`
3. æ— æŠ•料工序的展开行为空(无任何内容),不显示"投料明细"标题
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| äº§å“æœªé…ç½®å·¥è‰ºè·¯çº¿ | è¿”回空列表,不报错 |
| å·¥è‰ºè·¯çº¿æœ‰å·¥åºä½†æœªé…ç½®æŠ•æ–™ | bomItems ä¸ºç©ºï¼Œå±•开行无内容 |
| æŠ•料物料已被删除 | ä¸è¿”回该条投料记录(数据库逻辑删除) |
| å·¥åºè¢«åˆ é™¤åŽé‡å»º | å±•开行重新从接口获取最新 BOM æ•°æ® |
## æ³¨æ„äº‹é¡¹
- åŽç«¯ç¡®ä¿æ— éœ€é‡å¯å³å¯éªŒè¯ï¼ˆå¦‚果接口已有数据):使用已配置投料的产品ID调用接口
- æŠ•æ–™ BOM æ¥è‡ª `mes_pro_route_product_bom` è¡¨ï¼ˆé…ç½®æŠ•料),不是产品标准 BOM
- BOM ç‰©æ–™åç§°é€šè¿‡ `MesMdItemService` å®žæ—¶æŸ¥è¯¢ï¼Œç¡®ä¿å±•示最新物料名称
- Mapper å±‚使用 MyBatis-Plus `@TableLogic`,已逻辑删除的记录不会返回
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
@@ -5,11 +5,13 @@
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProProcessBomItemVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
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.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
@@ -27,6 +29,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@@ -78,14 +81,41 @@
        return success(buildRouteProcessRespVO(routeProcess));
    }
    // TODO @芋艿:到底叫 list-by-item è¿˜æ˜¯ list-by-product?
    @GetMapping("/list-by-product")
    @Operation(summary = "按产品获得工序列表", description = "根据产品查找关联的工艺路线,返回该路线的工序列表")
    @Operation(summary = "按产品获得工序列表", description = "根据产品查找关联的工艺路线,返回该路线的工序列表(含各工序的投料BOM)")
    @Parameter(name = "productId", description = "产品编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:pro-route:query')")
    public CommonResult<List<MesProRouteProcessRespVO>> getRouteProcessListByProduct(@RequestParam("productId") Long productId) {
        List<MesProRouteProcessDO> list = routeProcessService.getRouteProcessListByProductId(productId);
        return success(buildRouteProcessRespVOList(list));
        List<MesProRouteProcessRespVO> result = buildRouteProcessRespVOList(list);
        // æŸ¥è¯¢é…ç½®æŠ•料并按工序填充
        Map<Long, List<MesProRouteProductBomDO>> bomByProcess = routeProcessService.getRouteProcessBomMapByProductId(productId);
        if (!bomByProcess.isEmpty()) {
            Set<Long> itemIds = bomByProcess.values().stream()
                    .flatMap(List::stream)
                    .map(MesProRouteProductBomDO::getItemId)
                    .collect(Collectors.toSet());
            Map<Long, MesMdItemDO> itemMap = mdItemService.getItemMap(itemIds);
            for (MesProRouteProcessRespVO vo : result) {
                List<MesProRouteProductBomDO> processBoms = bomByProcess.get(vo.getProcessId());
                if (CollUtil.isNotEmpty(processBoms)) {
                    vo.setBomItems(processBoms.stream().map(bom -> {
                        MesProProcessBomItemVO itemVO = new MesProProcessBomItemVO();
                        itemVO.setItemId(bom.getItemId());
                        itemVO.setQuantity(bom.getQuantity());
                        itemVO.setRemark(bom.getRemark());
                        MesMdItemDO item = itemMap.get(bom.getItemId());
                        if (item != null) {
                            itemVO.setItemCode(item.getCode());
                            itemVO.setItemName(item.getName());
                        }
                        return itemVO;
                    }).collect(Collectors.toList()));
                }
            }
        }
        return success(result);
    }
    @GetMapping("/list-by-route")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProProcessBomItemVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "工序投料BOM明细 VO")
@Data
public class MesProProcessBomItemVO {
    @Schema(description = "BOM物料编号", example = "1001")
    private Long itemId;
    @Schema(description = "BOM物料编码", example = "M001")
    private String itemCode;
    @Schema(description = "BOM物料名称", example = "物料A")
    private String itemName;
    @Schema(description = "用料比例", example = "1.0")
    private BigDecimal quantity;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
@@ -4,6 +4,7 @@
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å·¥åº Response VO")
@Data
@@ -66,6 +67,9 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "工序投料BOM列表")
    private List<MesProProcessBomItemVO> bomItems;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java
@@ -4,6 +4,9 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - MES ç”Ÿäº§å·¥å•工序新增/修改 Request VO")
@Data
public class MesProWorkOrderProcessSaveReqVO {
@@ -50,4 +53,27 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "工序投料BOM列表")
    private List<BomItem> bomItems;
    @Data
    public static class BomItem {
        @Schema(description = "BOM物料编号", example = "1001")
        private Long itemId;
        @Schema(description = "BOM物料编码")
        private String itemCode;
        @Schema(description = "BOM物料名称")
        private String itemName;
        @Schema(description = "预计使用量")
        private BigDecimal quantity;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -12,8 +12,8 @@
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
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.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
@@ -22,8 +22,8 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
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.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
@@ -64,7 +64,7 @@
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    @Lazy // é¿å…å¾ªçŽ¯ä¾èµ–
    private MesProTaskService taskService;
@@ -188,8 +188,8 @@
        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());
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                    feedback.getWorkOrderId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java
@@ -67,6 +67,17 @@
    List<MesProRouteProcessDO> getRouteProcessListByProductId(Long productId);
    /**
     * æŒ‰äº§å“èŽ·å¾—å·¥åºæŠ•æ–™BOM Map
     *
     * key = å·¥åºID,value = è¯¥å·¥åºä¸‹çš„æŠ•æ–™BOM明细列表
     *
     * @param productId äº§å“ç¼–号
     * @return å·¥åº -> æŠ•æ–™BOM æ˜ å°„,产品无工艺路线时返回空Map
     */
    java.util.Map<Long, java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO>>
        getRouteProcessBomMapByProductId(Long productId);
    /**
     * æŒ‰å·¥åºèŽ·å¾—å·¥è‰ºè·¯çº¿å·¥åºåˆ—è¡¨
     *
     * @param processId å·¥åºç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
@@ -4,6 +4,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
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.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProcessMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
@@ -17,6 +18,8 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
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.*;
@@ -42,6 +45,9 @@
    @Resource
    @Lazy
    private MesProRouteService routeService;
    @Resource
    @Lazy
    private MesProRouteProductBomService routeProductBomService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -198,6 +204,20 @@
    }
    @Override
    public Map<Long, List<MesProRouteProductBomDO>> getRouteProcessBomMapByProductId(Long productId) {
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        if (routeProduct == null) {
            return Collections.emptyMap();
        }
        List<MesProRouteProductBomDO> bomList = routeProductBomService
                .getRouteProductBomListByRouteIdAndProductId(routeProduct.getRouteId(), productId);
        if (CollUtil.isEmpty(bomList)) {
            return Collections.emptyMap();
        }
        return bomList.stream().collect(Collectors.groupingBy(MesProRouteProductBomDO::getProcessId));
    }
    @Override
    public List<MesProRouteProcessDO> getRouteProcessListByProcessId(Long processId) {
        return routeProcessMapper.selectListByProcessId(processId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductBomService.java
@@ -53,4 +53,13 @@
     */
    void deleteRouteProductBomByRouteIdAndProductId(Long routeId, Long productId);
    /**
     * æŒ‰å·¥è‰ºè·¯çº¿å’Œäº§å“æŸ¥è¯¢ BOM åˆ—表
     *
     * @param routeId å·¥è‰ºè·¯çº¿ç¼–号
     * @param productId äº§å“ç‰©æ–™ç¼–号
     * @return BOM åˆ—表
     */
    List<MesProRouteProductBomDO> getRouteProductBomListByRouteIdAndProductId(Long routeId, Long productId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductBomServiceImpl.java
@@ -132,4 +132,9 @@
        routeProductBomMapper.deleteByRouteIdAndProductId(routeId, productId);
    }
    @Override
    public List<MesProRouteProductBomDO> getRouteProductBomListByRouteIdAndProductId(Long routeId, Long productId) {
        return routeProductBomMapper.selectListByRouteIdAndProductId(routeId, productId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
@@ -5,8 +5,11 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderProcessMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
@@ -44,6 +47,11 @@
    private MesProProcessService processService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    @Lazy
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProWorkOrderBomMapper workOrderBomMapper;
    @Override
    public Long createWorkOrderProcess(MesProWorkOrderProcessSaveReqVO createReqVO) {
@@ -175,10 +183,44 @@
            }
        }
        // 4. é‡å»ºå·¥åºé“¾
        // 4. ä¿å­˜å·¥å• BOM æŠ•料(合并所有工序的 bomItems,按 itemId åŽ»é‡ï¼‰
        saveWorkOrderBomFromProcesses(workOrderId, list);
        // 5. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(workOrderId);
    }
    /**
     * ä»Žå·¥åºåˆ—表中提取 BOM æŠ•料,合并去重后保存到工单 BOM è¡¨
     */
    private void saveWorkOrderBomFromProcesses(Long workOrderId, List<MesProWorkOrderProcessSaveReqVO> list) {
        // æ”¶é›†æ‰€æœ‰å·¥åºçš„ bomItems,按 itemId åŽ»é‡ï¼ˆä¿ç•™ç¬¬ä¸€ä¸ªå‡ºçŽ°çš„ quantity)
        java.util.Map<Long, MesProWorkOrderProcessSaveReqVO.BomItem> merged = new java.util.LinkedHashMap<>();
        for (MesProWorkOrderProcessSaveReqVO item : list) {
            if (CollUtil.isNotEmpty(item.getBomItems())) {
                for (MesProWorkOrderProcessSaveReqVO.BomItem bomItem : item.getBomItems()) {
                    if (bomItem.getItemId() != null) {
                        merged.putIfAbsent(bomItem.getItemId(), bomItem);
                    }
                }
            }
        }
        // åˆ é™¤æ—§ BOM,批量插入新 BOM
        workOrderBomService.deleteWorkOrderBomByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(merged)) {
            List<MesProWorkOrderBomDO> bomList = new ArrayList<>(merged.size());
            for (MesProWorkOrderProcessSaveReqVO.BomItem bomItem : merged.values()) {
                bomList.add(MesProWorkOrderBomDO.builder()
                        .workOrderId(workOrderId)
                        .itemId(bomItem.getItemId())
                        .quantity(bomItem.getQuantity())
                        .remark(bomItem.getRemark())
                        .build());
            }
            workOrderBomMapper.insertBatch(bomList);
        }
    }
    // ==================== ç§æœ‰æ–¹æ³• ====================
    private MesProWorkOrderProcessDO validateWorkOrderProcessExists(Long id) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java
@@ -5,7 +5,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO;
@@ -19,8 +19,7 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -59,9 +58,7 @@
    @Resource
    private MesWmItemConsumeDetailService itemConsumeDetailService;
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesProRouteService routeService;
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
@@ -77,14 +74,13 @@
    @Override
    public MesWmItemConsumeDO generateItemConsume(MesProFeedbackDO feedback) {
        // 1.1 é¿å… routeId ç­‰ä¸º null æ—¶ getRouteProductBomList å›  eqIfPresent æ”¾å®½æ¡ä»¶å¯¼è‡´è¯¯æŸ¥
        if (feedback.getRouteId() == null || feedback.getProcessId() == null || feedback.getItemId() == null) {
        // 1.1 æ ¡éªŒå·¥å•编号有效
        if (feedback.getWorkOrderId() == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        routeService.validateRouteExists(feedback.getRouteId());
        // 1.2 æŸ¥è¯¢å½“前工序的 BOM ç‰©æ–™é…ç½®
        List<MesProRouteProductBomDO> boms = routeProductBomService.getRouteProductBomList(
                feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId());
        // 1.2 æŸ¥è¯¢å·¥å• BOM ç‰©æ–™åˆ—表(倒冲按工单投料,非工艺路线)
        List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                feedback.getWorkOrderId());
        if (CollUtil.isEmpty(boms)) {
            return null;
        }
yudao-server/src/main/resources/logback-spring.xml
@@ -4,6 +4,8 @@
    <!-- CONSOLE_LOG_PATTERN ç›¸æ¯” FILE_LOG_PATTERN å¤šäº† highlight、cyan ç­‰é«˜äº® -->
    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n"/>
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n"/>
    <!-- æŒä¹…化 error æ—¥å¿—路径,按天滚动,保留 15 å¤© -->
    <property name="ERROR_LOG_FILE" value="./logs/error/error.log"/>
    <!-- æŽ§åˆ¶å° Appender -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">     
@@ -34,6 +36,28 @@
        <appender-ref ref="FILE"/>
    </appender>
    <!-- Error æ—¥å¿—专有 Appender:持久化 error æ—¥å¿—,按天滚动,保留 15 å¤© -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- åªå†™å…¥ ERROR çº§åˆ« -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${ERROR_LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${ERROR_LOG_FILE}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>15</maxHistory>
        </rollingPolicy>
    </appender>
    <!-- å¼‚步写入 error æ—¥å¿— -->
    <appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>512</queueSize>
        <appender-ref ref="ERROR_FILE"/>
    </appender>
    <!-- SkyWalking Appender:GRPC æ—¥å¿—收集,实现日志中心 -->
    <!--
    <appender name="SKYWALKING" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
@@ -51,6 +75,8 @@
        <appender-ref ref="ASYNC"/>
        <!-- å¦‚果想接入【SkyWalking æ—¥å¿—服务】,可以取消注释掉本行 -->
        <!-- <appender-ref ref="SKYWALKING"/> -->
        <!-- Error æ—¥å¿—持久化:按天滚动,保留 15 å¤© -->
        <appender-ref ref="ERROR_ASYNC"/>
    </root>
</configuration>