云
6 小时以前 3edf8c4fc07f9784e2d04ee7a98692bb457749eb
feat(mes): 新增工艺路线完整导入功能并优化相关验证

- 添加工艺路线批量导入模板和完整导入功能,支持多工作表导入
- 增加多个工艺路线相关错误码常量,完善错误处理机制
- 优化生产反馈服务,修复主键ID处理问题
- 完善工艺路线查询功能,支持按产品关键词模糊搜索
- 重构工序产出配置逻辑,支持按产品归属配置产出物料
- 添加工序默认报工仓库配置功能,集成仓库区域位置信息
- 实现工艺路线产品验证增强,确保物料类型和成品半成品分类正确
- 优化工序产出验证逻辑,支持中间工序和末道工序的不同配置规则
已修改28个文件
已添加10个文件
1775 ■■■■■ 文件已修改
sql/mysql/mes_pro_route_import_menu_20261026.sql 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_route_output_attribution_20261026.sql 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteCompleteImportWorkbook.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportExcelVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteCompleteImportService.java 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImplTest.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_route_import_menu_20261026.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
-- å·¥è‰ºè·¯çº¿å¯¼å…¥æƒé™ï¼šæ™®é€šå¯¼å…¥å’Œå®Œæ•´è·¯çº¿æ‰¹é‡å¯¼å…¥å…±ç”¨ mes:pro-route:import。
-- ä¼˜å…ˆæŒ‚到“工艺设计 / å·¥è‰ºè·¯çº¿â€èœå•;若目标库只有生产模块旧菜单,则挂到旧菜单。
-- å¯é‡å¤æ‰§è¡Œï¼›ä»…自动授权超级管理员角色,其他角色请在角色管理中按需勾选。
SET @route_menu_id := COALESCE(
    (SELECT id FROM system_menu
     WHERE component LIKE '%process-design/route%' AND type = 2 AND deleted = 0
     ORDER BY id LIMIT 1),
    (SELECT id FROM system_menu
     WHERE component LIKE '%mes/pro/route/index%' AND type = 2 AND deleted = 0
     ORDER BY id LIMIT 1)
);
SET @import_menu_id := (
    SELECT id FROM system_menu
    WHERE permission = 'mes:pro-route:import' AND deleted = 0
    ORDER BY id LIMIT 1
);
SET @new_menu_id := (SELECT COALESCE(MAX(id), 0) + 1 FROM system_menu);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @new_menu_id,
       '工艺路线导入', 'mes:pro-route:import', 3, 6, @route_menu_id,
       '', '#', '', NULL, 0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @route_menu_id IS NOT NULL AND @import_menu_id IS NULL;
SET @import_menu_id := (
    SELECT id FROM system_menu
    WHERE permission = 'mes:pro-route:import' AND deleted = 0
    ORDER BY id LIMIT 1
);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted, tenant_id)
SELECT r.id, @import_menu_id, '1', NOW(), '1', NOW(), 0, r.tenant_id
FROM system_role r
WHERE r.id = 1 AND r.deleted = 0 AND @import_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1 FROM system_role_menu
      WHERE role_id = r.id AND menu_id = @import_menu_id
        AND tenant_id = r.tenant_id AND deleted = 0
  );
sql/mysql/mes_pro_route_output_attribution_20261026.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
-- å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºæŒ‰äº§å“å½’属及默认报工库位(MySQL 8.x)
-- å…ˆæ‰§è¡Œæœ¬è„šæœ¬ï¼Œå†éƒ¨ç½²å¯¹åº”åŽç«¯ç‰ˆæœ¬ï¼›åŽ†å²å·¥è‰ºè·¯çº¿äº§å“å¯ç”¨å‰éœ€è¡¥é½ä»“åº“ã€åº“åŒºã€åº“ä½ã€‚
CREATE TABLE IF NOT EXISTS `mes_pro_route_process_output` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `route_process_id` bigint NOT NULL COMMENT '工艺路线工序编号',
  `route_id` bigint NOT NULL COMMENT '工艺路线编号',
  `process_id` bigint NOT NULL COMMENT '工序编号',
  `product_id` bigint DEFAULT NULL COMMENT '归属产品编号,空为通用',
  `item_id` bigint NOT NULL COMMENT '产出物料编号',
  `sort` int DEFAULT NULL COMMENT '排序',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`),
  KEY `idx_route_process_id` (`route_process_id`),
  KEY `idx_route_id` (`route_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属';
-- å½“前分支保留的工艺参数模板和工序收率;目标库可能尚未执行对应历史迁移。
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_process' AND column_name = 'param_template_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_route_process ADD COLUMN param_template_id BIGINT NULL COMMENT ''工艺参数模板编号'' AFTER process_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_process' AND column_name = 'yield_rate'),
  'SELECT 1', 'ALTER TABLE mes_pro_route_process ADD COLUMN yield_rate DECIMAL(10,4) NULL COMMENT ''工序收率(产出/投入,0~1)'' AFTER param_template_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product' AND column_name = 'warehouse_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_route_product ADD COLUMN warehouse_id BIGINT NULL COMMENT ''报工产出仓库ID'' AFTER item_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product' AND column_name = 'location_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_route_product ADD COLUMN location_id BIGINT NULL COMMENT ''报工产出库区ID'' AFTER warehouse_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_route_product' AND column_name = 'area_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_route_product ADD COLUMN area_id BIGINT NULL COMMENT ''报工产出库位ID'' AFTER location_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'param_template_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_work_order_process ADD COLUMN param_template_id BIGINT NULL COMMENT ''工艺参数模板编号'' AFTER process_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'yield_rate'),
  'SELECT 1', 'ALTER TABLE mes_pro_work_order_process ADD COLUMN yield_rate DECIMAL(10,4) NULL COMMENT ''工序收率(产出/投入,0~1)'' AFTER param_template_id');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'warehouse_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_work_order_process ADD COLUMN warehouse_id BIGINT NULL COMMENT ''报工产出仓库ID''');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'location_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_work_order_process ADD COLUMN location_id BIGINT NULL COMMENT ''报工产出库区ID''');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_pro_work_order_process' AND column_name = 'area_id'),
  'SELECT 1', 'ALTER TABLE mes_pro_work_order_process ADD COLUMN area_id BIGINT NULL COMMENT ''报工产出库位ID''');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java
@@ -7,11 +7,18 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteCompleteImportWorkbook;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteCompleteImportService;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcelFactory;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
@@ -22,8 +29,11 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -38,6 +48,8 @@
    @Resource
    private MesProRouteService routeService;
    @Resource
    private MesProRouteCompleteImportService completeImportService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线")
@@ -113,4 +125,81 @@
        ExcelUtils.write(response, "工艺路线.xls", "数据", MesProRouteRespVO.class, data);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得工艺路线导入模板")
    @PreAuthorize("@ss.hasPermission('mes:pro-route:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // æ‰‹åŠ¨åˆ›å»ºå¯¼å‡º demo
        List<MesProRouteImportExcelVO> list = Collections.singletonList(
                MesProRouteImportExcelVO.builder()
                        .code("ROUTE001").name("示例工艺路线").description("").remark("").build()
        );
        // è¾“出
        ExcelUtils.write(response, "工艺路线导入模板.xls", "工艺路线列表", MesProRouteImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入工艺路线")
    @Parameters({
            @Parameter(name = "file", description = "Excel æ–‡ä»¶", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('mes:pro-route:import')")
    public CommonResult<MesProRouteImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                             @RequestParam(value = "updateSupport", required = false,
                                                                     defaultValue = "false") Boolean updateSupport) throws Exception {
        List<MesProRouteImportExcelVO> list = ExcelUtils.read(file, MesProRouteImportExcelVO.class);
        return success(routeService.importRouteList(list, updateSupport));
    }
    @GetMapping("/get-complete-import-template")
    @Operation(summary = "获得完整工艺路线批量导入模板")
    @PreAuthorize("@ss.hasPermission('mes:pro-route:import')")
    public void completeImportTemplate(HttpServletResponse response) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8("完整工艺路线导入模板.xlsx"));
        ExcelWriter writer = FastExcelFactory.write(response.getOutputStream()).autoCloseStream(false).build();
        try {
            writer.write(Collections.emptyList(), FastExcelFactory.writerSheet(0, "路线")
                    .head(MesProRouteCompleteImportWorkbook.RouteRow.class).build());
            writer.write(Collections.emptyList(), FastExcelFactory.writerSheet(1, "产品")
                    .head(MesProRouteCompleteImportWorkbook.ProductRow.class).build());
            writer.write(Collections.emptyList(), FastExcelFactory.writerSheet(2, "工序")
                    .head(MesProRouteCompleteImportWorkbook.ProcessRow.class).build());
            writer.write(Collections.emptyList(), FastExcelFactory.writerSheet(3, "工序产出")
                    .head(MesProRouteCompleteImportWorkbook.OutputRow.class).build());
            writer.write(Collections.emptyList(), FastExcelFactory.writerSheet(4, "工序投料")
                    .head(MesProRouteCompleteImportWorkbook.InputRow.class).build());
            writer.write(List.of(
                    new MesProRouteCompleteImportWorkbook.GuideRow("路线", "每行一条路线;路线编码唯一。完整导入仅新增,已有编码会报错,导入后默认停用。"),
                    new MesProRouteCompleteImportWorkbook.GuideRow("产品", "路线编码与路线表一致;每条路线至少一个产品。物料、仓库、库区、库位填写已有业务编码;时间单位填 MINUTE/HOUR/DAY。"),
                    new MesProRouteCompleteImportWorkbook.GuideRow("工序", "每条路线至少一道工序;工序编码须已存在,排序唯一。布尔字段填是或否;关系填 0=开始-开始、1=结束-结束、2=开始-结束、3=结束-开始。"),
                    new MesProRouteCompleteImportWorkbook.GuideRow("工序产出", "按路线编码和工序编码关联;归属产品编码留空表示多产品通用。末道工序应配置全部成品。"),
                    new MesProRouteCompleteImportWorkbook.GuideRow("工序投料", "按路线、工序和归属产品编码关联;投料物料须已在该产品 BOM ä¸­ã€‚")
            ), FastExcelFactory.writerSheet(5, "填写说明")
                    .head(MesProRouteCompleteImportWorkbook.GuideRow.class).build());
        } finally {
            writer.finish();
        }
    }
    @PostMapping("/import-complete")
    @Operation(summary = "批量导入完整工艺路线(多工作表)")
    @PreAuthorize("@ss.hasPermission('mes:pro-route:import')")
    public CommonResult<MesProRouteImportRespVO> importComplete(@RequestParam("file") MultipartFile file) throws IOException {
        MesProRouteCompleteImportWorkbook workbook = new MesProRouteCompleteImportWorkbook();
        workbook.setRoutes(readSheet(file, 0, MesProRouteCompleteImportWorkbook.RouteRow.class));
        workbook.setProducts(readSheet(file, 1, MesProRouteCompleteImportWorkbook.ProductRow.class));
        workbook.setProcesses(readSheet(file, 2, MesProRouteCompleteImportWorkbook.ProcessRow.class));
        workbook.setOutputs(readSheet(file, 3, MesProRouteCompleteImportWorkbook.OutputRow.class));
        workbook.setInputs(readSheet(file, 4, MesProRouteCompleteImportWorkbook.InputRow.class));
        return success(completeImportService.importRoutes(workbook));
    }
    private <T> List<T> readSheet(MultipartFile file, int index, Class<T> type) throws IOException {
        try (InputStream input = file.getInputStream()) {
            return FastExcelFactory.read(input, type, null).autoCloseStream(false).sheet(index).doReadSync();
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
@@ -6,15 +6,25 @@
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.MesProRouteProcessOutputVO;
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.MesProRouteProcessOutputDO;
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.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
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;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -43,6 +53,14 @@
    private MesProProcessService processService;
    @Resource
    private MesMdItemService mdItemService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线工序")
@@ -111,7 +129,35 @@
                }
            }
        }
        // å¡«å……工艺路线产品上配置的默认报工仓库(下单时作为工序默认值)
        fillDefaultOutputWarehouse(result, productId);
        return success(result);
    }
    private void fillDefaultOutputWarehouse(List<MesProRouteProcessRespVO> list, Long productId) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        if (routeProduct == null || routeProduct.getWarehouseId() == null) {
            return;
        }
        MesWmWarehouseDO warehouse = warehouseService.getWarehouse(routeProduct.getWarehouseId());
        MesWmWarehouseLocationDO location = routeProduct.getLocationId() != null
                ? warehouseLocationService.getWarehouseLocation(routeProduct.getLocationId()) : null;
        MesWmWarehouseAreaDO area = routeProduct.getAreaId() != null
                ? warehouseAreaService.getWarehouseArea(routeProduct.getAreaId()) : null;
        String warehouseName = warehouse != null ? warehouse.getName() : null;
        String locationName = location != null ? location.getName() : null;
        String areaName = area != null ? area.getName() : null;
        for (MesProRouteProcessRespVO vo : list) {
            vo.setWarehouseId(routeProduct.getWarehouseId());
            vo.setWarehouseName(warehouseName);
            vo.setLocationId(routeProduct.getLocationId());
            vo.setLocationName(locationName);
            vo.setAreaId(routeProduct.getAreaId());
            vo.setAreaName(areaName);
        }
    }
    @GetMapping("/list-by-route")
@@ -161,6 +207,41 @@
        }
        Map<Long, MesMdItemDO> outputItemMap = allOutputItemIds.isEmpty()
                ? Collections.emptyMap() : mdItemService.getItemMap(allOutputItemIds);
        // 2.1 äº§å‡ºå½’属配置(productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰
        Map<Long, List<MesProRouteProcessOutputVO>> outputsByRouteProcessId = new HashMap<>();
        Set<Long> routeProcessIds = convertSet(list, MesProRouteProcessDO::getId);
        routeProcessIds.removeAll(Collections.singleton(null));
        if (CollUtil.isNotEmpty(routeProcessIds)) {
            List<MesProRouteProcessOutputDO> outputList = routeProcessService
                    .getRouteProcessOutputListByRouteProcessIds(routeProcessIds);
            if (CollUtil.isNotEmpty(outputList)) {
                Set<Long> ownerItemIds = new HashSet<>();
                outputList.forEach(output -> {
                    if (output.getProductId() != null) {
                        ownerItemIds.add(output.getProductId());
                    }
                    if (output.getItemId() != null) {
                        ownerItemIds.add(output.getItemId());
                    }
                });
                Map<Long, MesMdItemDO> ownerItemMap = mdItemService.getItemMap(ownerItemIds);
                for (MesProRouteProcessOutputDO output : outputList) {
                    MesProRouteProcessOutputVO outputVO = new MesProRouteProcessOutputVO();
                    outputVO.setId(output.getId());
                    outputVO.setRouteProcessId(output.getRouteProcessId());
                    outputVO.setProductId(output.getProductId());
                    outputVO.setItemId(output.getItemId());
                    outputVO.setSort(output.getSort());
                    outputVO.setRemark(output.getRemark());
                    MapUtils.findAndThen(ownerItemMap, output.getItemId(),
                            item -> outputVO.setItemCode(item.getCode()).setItemName(item.getName()));
                    MapUtils.findAndThen(ownerItemMap, output.getProductId(),
                            product -> outputVO.setProductName(product.getName()));
                    outputsByRouteProcessId.computeIfAbsent(output.getRouteProcessId(),
                            key -> new ArrayList<>()).add(outputVO);
                }
            }
        }
        // 3. æ‹¼æŽ¥ VO
        List<MesProRouteProcessRespVO> result = BeanUtils.toBean(list, MesProRouteProcessRespVO.class);
        for (int i = 0; i < result.size(); i++) {
@@ -188,6 +269,7 @@
                    vo.setOutputItemName(firstItem.getName());
                }
            }
            vo.setOutputs(outputsByRouteProcessId.getOrDefault(vo.getId(), Collections.emptyList()));
        }
        return result;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java
@@ -10,9 +10,15 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -42,6 +48,12 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线产品")
@@ -96,6 +108,12 @@
                convertSet(list, MesProRouteProductDO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(list, MesProRouteProductDO::getWarehouseId));
        Map<Long, MesWmWarehouseLocationDO> locationMap = warehouseLocationService.getWarehouseLocationMap(
                convertSet(list, MesProRouteProductDO::getLocationId));
        Map<Long, MesWmWarehouseAreaDO> areaMap = warehouseAreaService.getWarehouseAreaMap(
                convertSet(list, MesProRouteProductDO::getAreaId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProRouteProductRespVO.class, vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
@@ -104,6 +122,9 @@
                MapUtils.findAndThen(unitMap, item.getUnitMeasureId(),
                        unit -> vo.setUnitName(unit.getName()));
            });
            MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName()));
            MapUtils.findAndThen(locationMap, vo.getLocationId(), location -> vo.setLocationName(location.getName()));
            MapUtils.findAndThen(areaMap, vo.getAreaId(), area -> vo.setAreaName(area.getName()));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteCompleteImportWorkbook.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/** å¤šå·¥ä½œè¡¨å®Œæ•´å·¥è‰ºè·¯çº¿å¯¼å…¥æ•°æ®ï¼Œæ‰€æœ‰å…³è”通过业务编码定位。 */
@Data
public class MesProRouteCompleteImportWorkbook {
    private List<RouteRow> routes;
    private List<ProductRow> products;
    private List<ProcessRow> processes;
    private List<OutputRow> outputs;
    private List<InputRow> inputs;
    @Data
    public static class RouteRow {
        @ExcelProperty("路线编码") private String routeCode;
        @ExcelProperty("路线名称") private String name;
        @ExcelProperty("路线说明") private String description;
        @ExcelProperty("备注") private String remark;
    }
    @Data
    public static class ProductRow {
        @ExcelProperty("路线编码") private String routeCode;
        @ExcelProperty("产品物料编码") private String itemCode;
        @ExcelProperty("仓库编码") private String warehouseCode;
        @ExcelProperty("库区编码") private String locationCode;
        @ExcelProperty("库位编码") private String areaCode;
        @ExcelProperty("生产数量") private Integer quantity;
        @ExcelProperty("生产用时") private BigDecimal productionTime;
        @ExcelProperty("时间单位") private String timeUnitType;
        @ExcelProperty("备注") private String remark;
    }
    @Data
    public static class ProcessRow {
        @ExcelProperty("路线编码") private String routeCode;
        @ExcelProperty("工序编码") private String processCode;
        @ExcelProperty("排序") private Integer sort;
        @ExcelProperty("工艺参数模板ID") private Long paramTemplateId;
        @ExcelProperty("工序收率") private BigDecimal yieldRate;
        @ExcelProperty("与下一工序关系") private Integer linkType;
        @ExcelProperty("准备时间(分钟)") private Integer prepareTime;
        @ExcelProperty("等待时间(分钟)") private Integer waitTime;
        @ExcelProperty("甘特图颜色") private String colorCode;
        @ExcelProperty("关键工序(是/否)") private String keyFlag;
        @ExcelProperty("质检工序(是/否)") private String checkFlag;
        @ExcelProperty("倒冲(是/否)") private String backflushFlag;
        @ExcelProperty("备注") private String remark;
    }
    @Data
    public static class OutputRow {
        @ExcelProperty("路线编码") private String routeCode;
        @ExcelProperty("工序编码") private String processCode;
        @ExcelProperty("产出物料编码") private String itemCode;
        @ExcelProperty("归属产品编码(空为通用)") private String productCode;
        @ExcelProperty("排序") private Integer sort;
        @ExcelProperty("备注") private String remark;
    }
    @Data
    public static class InputRow {
        @ExcelProperty("路线编码") private String routeCode;
        @ExcelProperty("工序编码") private String processCode;
        @ExcelProperty("归属产品编码") private String productCode;
        @ExcelProperty("投料物料编码") private String itemCode;
        @ExcelProperty("用料比例") private BigDecimal quantity;
        @ExcelProperty("备注") private String remark;
    }
    @Data
    public static class GuideRow {
        @ExcelProperty("工作表") private String sheet;
        @ExcelProperty("填写说明") private String description;
        public GuideRow(String sheet, String description) {
            this.sheet = sheet;
            this.description = description;
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * MES å·¥è‰ºè·¯çº¿ Excel å¯¼å…¥ VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MesProRouteImportExcelVO {
    @ExcelProperty("工艺路线编码")
    private String code;
    @ExcelProperty("工艺路线名称")
    private String name;
    @ExcelProperty("工艺路线说明")
    private String description;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å¯¼å…¥ Response VO")
@Data
@Builder
public class MesProRouteImportRespVO {
    @Schema(description = "创建成功的工艺路线编码数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createCodes;
    @Schema(description = "更新成功的工艺路线编码数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateCodes;
    @Schema(description = "导入失败的工艺路线集合,key ä¸ºå·¥è‰ºè·¯çº¿ç¼–码,value ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureCodes;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java
@@ -23,6 +23,9 @@
    @Schema(description = "工艺路线名称", example = "标准路线")
    private String name;
    @Schema(description = "关联产品名称或编码(模糊搜索)", example = "黑果枸杞")
    private String productKeyword;
    @Schema(description = "状态", example = "0")
    private Integer status;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 Response VO")
@Data
public class MesProRouteProcessOutputVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "工艺路线工序编号", example = "1")
    private Long routeProcessId;
    @Schema(description = "归属产品编号,为空表示多产品通用")
    private Long productId;
    @Schema(description = "归属产品名称")
    private String productName;
    @Schema(description = "产出物料编号", example = "100")
    private Long itemId;
    @Schema(description = "产出物料编码")
    private String itemCode;
    @Schema(description = "产出物料名称")
    private String itemName;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
@@ -3,9 +3,9 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å·¥åº Response VO")
@Data
@@ -19,10 +19,8 @@
    @Schema(description = "工序编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long processId;
    @Schema(description = "工艺参数模板编号", example = "1")
    private Long paramTemplateId;
    @Schema(description = "工序收率(产出/投入,0~1)", example = "0.98")
    private BigDecimal yieldRate;
@@ -77,6 +75,27 @@
    @Schema(description = "产出产品物料名称(逗号拼接)", example = "产品A, äº§å“B")
    private String outputItemNames;
    @Schema(description = "产出物料归属配置(productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰")
    private List<MesProRouteProcessOutputVO> outputs;
    @Schema(description = "默认报工仓库编号(取自工艺路线产品的报工仓库配置)", example = "10")
    private Long warehouseId;
    @Schema(description = "默认报工仓库名称")
    private String warehouseName;
    @Schema(description = "默认报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "默认报工库区名称")
    private String locationName;
    @Schema(description = "默认报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "默认报工库位名称")
    private String areaName;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
@@ -4,8 +4,8 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES å·¥è‰ºè·¯çº¿å·¥åºæ–°å¢ž/修改 Request VO")
@Data
@@ -18,16 +18,16 @@
    @NotNull(message = "工艺路线编号不能为空")
    private Long routeId;
    @Schema(description = "工序编号", example = "1")
    @Schema(description = "工序编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工序编号不能为空")
    private Long processId;
    @Schema(description = "工艺参数模板编号", example = "1")
    private Long paramTemplateId;
    @Schema(description = "工序收率(产出/投入,0~1)", example = "0.98")
    private BigDecimal yieldRate;
    @Schema(description = "序号", example = "1")
    @Schema(description = "序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "序号不能为空")
    private Integer sort;
    @Schema(description = "与下一道工序关系", example = "0")
@@ -54,7 +54,28 @@
    @Schema(description = "产出产品物料编号列表(多选,为空时默认用工单成品)", example = "[100, 101]")
    private List<Long> outputItemIds;
    @Schema(description = "产出物料归属配置(优先于 outputItemIds;productId ä¸ºç©ºè¡¨ç¤ºå¤šäº§å“é€šç”¨ï¼‰")
    private List<OutputItem> outputs;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "产出物料归属")
    @Data
    public static class OutputItem {
        @Schema(description = "归属产品编号,为空表示多产品通用")
        private Long productId;
        @Schema(description = "产出物料编号", requiredMode = Schema.RequiredMode.REQUIRED)
        private Long itemId;
        @Schema(description = "排序")
        private Integer sort;
        @Schema(description = "备注")
        private String remark;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java
@@ -19,6 +19,24 @@
    @Schema(description = "产品物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
    @Schema(description = "报工产出仓库编号")
    private Long warehouseId;
    @Schema(description = "报工产出仓库名称")
    private String warehouseName;
    @Schema(description = "报工产出库区编号")
    private Long locationId;
    @Schema(description = "报工产出库区名称")
    private String locationName;
    @Schema(description = "报工产出库位编号")
    private Long areaId;
    @Schema(description = "报工产出库位名称")
    private String areaName;
    @Schema(description = "产品物料编码")
    private String itemCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java
@@ -21,6 +21,18 @@
    @NotNull(message = "产品物料编号不能为空")
    private Long itemId;
    @Schema(description = "报工产出仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出仓库不能为空")
    private Long warehouseId;
    @Schema(description = "报工产出库区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出库区不能为空")
    private Long locationId;
    @Schema(description = "报工产出库位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "报工产出库位不能为空")
    private Long areaId;
    @Schema(description = "生产数量", example = "100")
    private Integer quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java
@@ -14,10 +14,16 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
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.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
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.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -47,6 +53,12 @@
    private MesProProcessService processService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService warehouseLocationService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @PostMapping("/create")
    @Operation(summary = "创建工单工序")
@@ -150,6 +162,19 @@
        }
        Map<Long, MesMdItemDO> itemMap = CollUtil.isNotEmpty(allOutputItemIds)
                ? itemService.getItemMap(allOutputItemIds) : Collections.emptyMap();
        // èŽ·å–æŠ¥å·¥ä»“åº“/库区/库位
        Set<Long> warehouseIds = convertSet(list, MesProWorkOrderProcessDO::getWarehouseId);
        warehouseIds.removeAll(Collections.singleton(null));
        Set<Long> locationIds = convertSet(list, MesProWorkOrderProcessDO::getLocationId);
        locationIds.removeAll(Collections.singleton(null));
        Set<Long> areaIds = convertSet(list, MesProWorkOrderProcessDO::getAreaId);
        areaIds.removeAll(Collections.singleton(null));
        Map<Long, MesWmWarehouseDO> warehouseMap = CollUtil.isNotEmpty(warehouseIds)
                ? warehouseService.getWarehouseMap(warehouseIds) : Collections.emptyMap();
        Map<Long, MesWmWarehouseLocationDO> locationMap = CollUtil.isNotEmpty(locationIds)
                ? warehouseLocationService.getWarehouseLocationMap(locationIds) : Collections.emptyMap();
        Map<Long, MesWmWarehouseAreaDO> areaMap = CollUtil.isNotEmpty(areaIds)
                ? warehouseAreaService.getWarehouseAreaMap(areaIds) : Collections.emptyMap();
        // 2. æŸ¥è¯¢æŠ•æ–™BOM(按工序分组)
        Set<Long> workOrderIds = convertSet(list, MesProWorkOrderProcessDO::getWorkOrderId);
@@ -185,6 +210,13 @@
            MapUtils.findAndThen(processMap, vo.getNextProcessId(), process -> {
                vo.setNextProcessName(process.getName());
            });
            // æŠ¥å·¥ä»“库/库区/库位名称
            MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(),
                    warehouse -> vo.setWarehouseName(warehouse.getName()));
            MapUtils.findAndThen(locationMap, vo.getLocationId(),
                    location -> vo.setLocationName(location.getName()));
            MapUtils.findAndThen(areaMap, vo.getAreaId(),
                    area -> vo.setAreaName(area.getName()));
            // äº§å‡ºç‰©æ–™ï¼ˆæ”¯æŒå¤šé€‰ï¼‰
            List<Long> ids = voOutputItemIdsMap.getOrDefault((long) i, Collections.emptyList());
            if (!ids.isEmpty()) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java
@@ -90,6 +90,24 @@
    @Schema(description = "产出产品物料名称(逗号拼接)", example = "产品A, äº§å“B")
    private String outputItemNames;
    @Schema(description = "报工仓库编号", example = "10")
    private Long warehouseId;
    @Schema(description = "报工仓库名称", example = "成品仓")
    private String warehouseName;
    @Schema(description = "报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "报工库区名称", example = "成品仓通用库区")
    private String locationName;
    @Schema(description = "报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "报工库位名称", example = "成品仓默认库位")
    private String areaName;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java
@@ -58,6 +58,15 @@
    @Schema(description = "产出产品物料编号列表(多选,为空时默认用工单成品)", example = "[100, 101]")
    private List<Long> outputItemIds;
    @Schema(description = "报工仓库编号", example = "10")
    private Long warehouseId;
    @Schema(description = "报工库区编号", example = "110")
    private Long locationId;
    @Schema(description = "报工库位编号", example = "210")
    private Long areaId;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java
@@ -7,7 +7,6 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import java.math.BigDecimal;
/**
@@ -42,15 +41,9 @@
     * å…³è” {@link MesProProcessDO#getId()}
     */
    private Long processId;
    /**
     * å·¥è‰ºå‚数模板编号
     */
    /** å·¥è‰ºå‚数模板编号 */
    private Long paramTemplateId;
    /**
     * å·¥åºæ”¶çŽ‡ï¼ˆäº§å‡º/投入,0~1)
     *
     * æŽ’产时按此收率从末道往前倒推各道工序的产出数量;为空按 1.0(不放大)处理
     */
    /** å·¥åºæ”¶çŽ‡ï¼ˆäº§å‡º/投入,0~1) */
    private BigDecimal yieldRate;
    /**
     * åºå·
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.route;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 DO
 *
 * åŒä¸€é“工序的产出可以按产品区分:归属产品为空表示该路线下所有产品通用。
 *
 * @author è¶…级管理员
 */
@TableName("mes_pro_route_process_output")
@KeySequence("mes_pro_route_process_output_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesProRouteProcessOutputDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å·¥è‰ºè·¯çº¿å·¥åºç¼–号
     *
     * å…³è” {@link MesProRouteProcessDO#getId()}
     */
    private Long routeProcessId;
    /**
     * å·¥è‰ºè·¯çº¿ç¼–号
     *
     * å…³è” {@link MesProRouteDO#getId()}
     */
    private Long routeId;
    /**
     * å·¥åºç¼–号
     *
     * å…³è” {@link MesProProcessDO#getId()}
     */
    private Long processId;
    /**
     * å½’属产品编号,为空表示多产品通用
     *
     * å…³è” {@link MesMdItemDO#getId()}
     */
    private Long productId;
    /**
     * äº§å‡ºç‰©æ–™ç¼–号
     *
     * å…³è” {@link MesMdItemDO#getId()}
     */
    private Long itemId;
    /**
     * æŽ’序
     */
    private Integer sort;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java
@@ -40,6 +40,12 @@
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO#getId()}
     */
    private Long itemId;
    /** æŠ¥å·¥äº§å‡ºä»“库 */
    private Long warehouseId;
    /** æŠ¥å·¥äº§å‡ºåº“区 */
    private Long locationId;
    /** æŠ¥å·¥äº§å‡ºåº“位 */
    private Long areaId;
    /**
     * ç”Ÿäº§æ•°é‡
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java
@@ -102,6 +102,25 @@
     */
    private String outputItemId;
    /**
     * æŠ¥å·¥ä»“库编号
     *
     * ä¸‹å•时从工艺路线产品带入,可手工调整;报工产出按此仓库入库
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO#getId()}
     */
    private Long warehouseId;
    /**
     * æŠ¥å·¥åº“区编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO#getId()}
     */
    private Long locationId;
    /**
     * æŠ¥å·¥åº“位编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO#getId()}
     */
    private Long areaId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -18,12 +19,20 @@
public interface MesProRouteMapper extends BaseMapperX<MesProRouteDO> {
    default PageResult<MesProRouteDO> selectPage(MesProRoutePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProRouteDO>()
        LambdaQueryWrapperX<MesProRouteDO> query = new LambdaQueryWrapperX<MesProRouteDO>()
                .likeIfPresent(MesProRouteDO::getCode, reqVO.getCode())
                .likeIfPresent(MesProRouteDO::getName, reqVO.getName())
                .eqIfPresent(MesProRouteDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesProRouteDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MesProRouteDO::getId));
                .betweenIfPresent(MesProRouteDO::getCreateTime, reqVO.getCreateTime());
        if (StrUtil.isNotBlank(reqVO.getProductKeyword())) {
            String keyword = reqVO.getProductKeyword().trim();
            query.apply("EXISTS (SELECT 1 FROM mes_pro_route_product rp "
                    + "JOIN mes_md_item item ON item.id = rp.item_id AND item.deleted = 0 "
                    + "WHERE rp.route_id = mes_pro_route.id AND rp.deleted = 0 "
                    + "AND (item.code LIKE CONCAT('%', {0}, '%') "
                    + "OR item.name LIKE CONCAT('%', {1}, '%')))", keyword, keyword);
        }
        return selectPage(reqVO, query.orderByDesc(MesProRouteDO::getId));
    }
    default MesProRouteDO selectByCode(String code) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.route;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES å·¥è‰ºè·¯çº¿å·¥åºäº§å‡ºå½’属 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesProRouteProcessOutputMapper extends BaseMapperX<MesProRouteProcessOutputDO> {
    default List<MesProRouteProcessOutputDO> selectListByRouteProcessId(Long routeProcessId) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessId)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default List<MesProRouteProcessOutputDO> selectListByRouteProcessIds(Collection<Long> routeProcessIds) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .in(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessIds)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default List<MesProRouteProcessOutputDO> selectListByRouteId(Long routeId) {
        return selectList(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteId, routeId)
                .orderByAsc(MesProRouteProcessOutputDO::getSort));
    }
    default void deleteByRouteProcessId(Long routeProcessId) {
        delete(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteProcessId, routeProcessId));
    }
    default void deleteByRouteId(Long routeId) {
        delete(new LambdaQueryWrapperX<MesProRouteProcessOutputDO>()
                .eq(MesProRouteProcessOutputDO::getRouteId, routeId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -29,6 +30,14 @@
                .orderByAsc(MesProWorkOrderProcessDO::getSort));
    }
    default MesProWorkOrderProcessDO selectByWorkOrderIdAndProcessId(Long workOrderId, Long processId) {
        return selectOne(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId)
                .eq(MesProWorkOrderProcessDO::getProcessId, processId)
                .orderByAsc(MesProWorkOrderProcessDO::getSort)
                .last("LIMIT 1"));
    }
    default MesProWorkOrderProcessDO selectLastByWorkOrderId(Long workOrderId) {
        return selectOne(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId)
@@ -36,6 +45,17 @@
                .last("LIMIT 1"));
    }
    /**
     * æ›´æ–°æŠ¥å·¥ä»“库:允许置空(updateById ä¼šå¿½ç•¥ null å­—段,故单独处理)
     */
    default void updateWarehouseById(Long id, Long warehouseId, Long locationId, Long areaId) {
        update(null, new LambdaUpdateWrapper<MesProWorkOrderProcessDO>()
                .set(MesProWorkOrderProcessDO::getWarehouseId, warehouseId)
                .set(MesProWorkOrderProcessDO::getLocationId, locationId)
                .set(MesProWorkOrderProcessDO::getAreaId, areaId)
                .eq(MesProWorkOrderProcessDO::getId, id));
    }
    default void deleteByWorkOrderId(Long workOrderId) {
        delete(new LambdaQueryWrapperX<MesProWorkOrderProcessDO>()
                .eq(MesProWorkOrderProcessDO::getWorkOrderId, workOrderId));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -254,6 +254,7 @@
    ErrorCode PRO_ROUTE_ENABLE_NO_KEY_PROCESS = new ErrorCode(1_040_501_003, "工艺路线必须要有关键工序");
    ErrorCode PRO_ROUTE_ENABLE_PRODUCT_NO_BOM = new ErrorCode(1_040_501_004, "产品 {} æœªé…ç½®å·¥åºçš„ BOM æ¶ˆè€—");
    ErrorCode PRO_ROUTE_IS_ENABLE = new ErrorCode(1_040_501_005, "工艺路线已启用,不允许操作");
    ErrorCode PRO_ROUTE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_501_006, "导入工艺路线数据不能为空");
    // ========== MES ç”Ÿäº§ç®¡ç†-工艺路线工序(1-040-501-100) ==========
    ErrorCode PRO_ROUTE_PROCESS_NOT_EXISTS = new ErrorCode(1_040_501_100, "工艺路线工序不存在");
    ErrorCode PRO_ROUTE_PROCESS_SORT_DUPLICATE = new ErrorCode(1_040_501_101, "序号已存在");
@@ -262,6 +263,13 @@
    // ========== MES ç”Ÿäº§ç®¡ç†-工艺路线产品(1-040-501-200) ==========
    ErrorCode PRO_ROUTE_PRODUCT_NOT_EXISTS = new ErrorCode(1_040_501_200, "工艺路线产品不存在");
    ErrorCode PRO_ROUTE_PRODUCT_ITEM_DUPLICATE = new ErrorCode(1_040_501_201, "此产品已配置了工艺路线");
    ErrorCode PRO_ROUTE_PRODUCT_ITEM_TYPE_INVALID = new ErrorCode(1_040_501_202, "物料【{}】的主数据类型及MES分类均非成品/半成品,请先核对物料类型");
    ErrorCode PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED = new ErrorCode(1_040_501_203, "产品【{}】未配置报工入库仓库、库区和库位");
    ErrorCode PRO_ROUTE_PROCESS_OUTPUT_REQUIRED = new ErrorCode(1_040_501_204, "工序【{}】未配置产出产品");
    ErrorCode PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND = new ErrorCode(1_040_501_205, "工序产出产品【{}】未绑定到当前工艺路线");
    ErrorCode PRO_ROUTE_MIDDLE_OUTPUT_INVALID = new ErrorCode(1_040_501_206, "中间工序【{}】只能产出半成品分类产品");
    ErrorCode PRO_ROUTE_LAST_OUTPUT_INVALID = new ErrorCode(1_040_501_207, "末道工序【{}】只能产出成品分类产品,且必须包含全部绑定成品");
    ErrorCode PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED = new ErrorCode(1_040_501_208, "末道工序【{}】的成品产出必须按产品归属配置,每个成品归属到自身,不能使用“通用”");
    // ========== MES ç”Ÿäº§ç®¡ç†-工艺路线产品BOM(1-040-501-300) ==========
    ErrorCode PRO_ROUTE_PRODUCT_BOM_NOT_EXISTS = new ErrorCode(1_040_501_300, "工艺路线产品 BOM ä¸å­˜åœ¨");
    ErrorCode PRO_ROUTE_PRODUCT_BOM_DUPLICATE = new ErrorCode(1_040_501_301, "当前 BOM ç‰©æ–™åœ¨æ­¤å·¥åºå·²ç»é…ç½®è¿‡");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -117,8 +117,9 @@
        // 1. æ ¡éªŒ
        MesProTaskDO task = validateFeedbackData(createReqVO);
        // 2. æ’入(自动填充 itemId)
        // 2. æ’入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
        MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
                .setId(null)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
        feedbackMapper.insert(feedback);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -540,8 +540,9 @@
        }
        Long routeId = routeProduct.getRouteId();
        // 2. æ ¡éªŒå·¥è‰ºè·¯çº¿æ˜¯å¦æœ‰å·¥åº
        List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId);
        // 2. æ ¡éªŒå·¥è‰ºè·¯çº¿æ˜¯å¦æœ‰å·¥åºï¼ˆæŒ‰äº§å“è¿‡æ»¤å…¶å®ƒäº§å“çš„产出物料)
        List<MesProRouteProcessDO> routeProcesses = routeProcessService
                .getRouteProcessListByProductId(mps.getProductId());
        if (CollUtil.isEmpty(routeProcesses)) {
            throw exception(PRO_ROUTE_ENABLE_NO_PROCESS);
        }
@@ -581,6 +582,10 @@
                        .map(Long::valueOf).collect(Collectors.toList()));
            }
            process.setColorCode(rp.getColorCode());
            // å¸¦å…¥å·¥è‰ºè·¯çº¿äº§å“é…ç½®çš„æŠ¥å·¥ä»“库
            process.setWarehouseId(routeProduct.getWarehouseId());
            process.setLocationId(routeProduct.getLocationId());
            process.setAreaId(routeProduct.getAreaId());
            // å¸¦å…¥å·¥åºæŠ•æ–™BOM
            List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null;
            if (CollUtil.isNotEmpty(routeBomItems)) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteCompleteImportService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
package cn.iocoder.yudao.module.mes.service.pro.route;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteCompleteImportWorkbook;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.product.MesProRouteProductSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.productbom.MesProRouteProductBomSaveReqVO;
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.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.process.MesProProcessMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.PRO_ROUTE_IMPORT_LIST_IS_EMPTY;
/** æŒ‰è·¯çº¿åˆ†äº‹åŠ¡æ‰¹é‡å¯¼å…¥å®Œæ•´é…ç½®ï¼›å¤±è´¥è·¯çº¿ä¸ä¼šç•™ä¸‹éƒ¨åˆ†äº§å“æˆ–å·¥åºã€‚ */
@Service
public class MesProRouteCompleteImportService {
    @Resource private TransactionTemplate transactionTemplate;
    @Resource private MesProRouteService routeService;
    @Resource private MesProRouteProductService productService;
    @Resource private MesProRouteProcessService processService;
    @Resource private MesProRouteProductBomService bomService;
    @Resource private MesMdItemService itemService;
    @Resource private MesProProcessMapper processMapper;
    @Resource private MesWmWarehouseService warehouseService;
    @Resource private MesWmWarehouseLocationService locationService;
    @Resource private MesWmWarehouseAreaService areaService;
    public MesProRouteImportRespVO importRoutes(MesProRouteCompleteImportWorkbook book) {
        if (book.getRoutes() == null || book.getRoutes().isEmpty()) {
            throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY);
        }
        MesProRouteImportRespVO result = MesProRouteImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        Set<String> seen = new HashSet<>();
        int rowNumber = 1;
        for (MesProRouteCompleteImportWorkbook.RouteRow row : book.getRoutes()) {
            rowNumber++;
            String code = trim(row.getRouteCode());
            String key = StrUtil.isBlank(code) ? "路线表第 " + rowNumber + " è¡Œ" : code;
            if (StrUtil.isBlank(code) || StrUtil.isBlank(row.getName())) {
                result.getFailureCodes().put(key, "路线编码和名称不能为空");
                continue;
            }
            if (!seen.add(code)) {
                result.getFailureCodes().put("路线表第 " + rowNumber + " è¡Œï¼ˆ" + code + ")", "导入文件中路线编码重复");
                continue;
            }
            try {
                transactionTemplate.execute(status -> {
                    importOne(book, row, code);
                    return null;
                });
                result.getCreateCodes().add(code);
            } catch (RuntimeException ex) {
                result.getFailureCodes().put(key, ex.getMessage());
            }
        }
        Set<String> known = book.getRoutes().stream().map(r -> trim(r.getRouteCode()))
                .collect(Collectors.toSet());
        collectUnknownRoutes(result.getFailureCodes(), known, "产品", book.getProducts() == null ? List.of() :
                book.getProducts().stream().map(MesProRouteCompleteImportWorkbook.ProductRow::getRouteCode).toList());
        collectUnknownRoutes(result.getFailureCodes(), known, "工序", book.getProcesses() == null ? List.of() :
                book.getProcesses().stream().map(MesProRouteCompleteImportWorkbook.ProcessRow::getRouteCode).toList());
        collectUnknownRoutes(result.getFailureCodes(), known, "工序产出", book.getOutputs() == null ? List.of() :
                book.getOutputs().stream().map(MesProRouteCompleteImportWorkbook.OutputRow::getRouteCode).toList());
        collectUnknownRoutes(result.getFailureCodes(), known, "工序投料", book.getInputs() == null ? List.of() :
                book.getInputs().stream().map(MesProRouteCompleteImportWorkbook.InputRow::getRouteCode).toList());
        return result;
    }
    private void importOne(MesProRouteCompleteImportWorkbook book,
                           MesProRouteCompleteImportWorkbook.RouteRow row, String code) {
        List<MesProRouteCompleteImportWorkbook.ProductRow> products = rows(book.getProducts()).stream()
                .filter(r -> code.equals(trim(r.getRouteCode()))).toList();
        List<MesProRouteCompleteImportWorkbook.ProcessRow> processes = rows(book.getProcesses()).stream()
                .filter(r -> code.equals(trim(r.getRouteCode())))
                .sorted(Comparator.comparing(MesProRouteCompleteImportWorkbook.ProcessRow::getSort,
                        Comparator.nullsLast(Comparator.naturalOrder()))).toList();
        if (products.isEmpty() || processes.isEmpty()) {
            throw new IllegalArgumentException("至少需要一条产品和一道工序");
        }
        MesProRouteSaveReqVO route = new MesProRouteSaveReqVO();
        route.setCode(code);
        route.setName(trim(row.getName()));
        route.setDescription(row.getDescription());
        route.setRemark(row.getRemark());
        Long routeId = routeService.createRoute(route);
        Set<String> productCodes = new HashSet<>();
        for (MesProRouteCompleteImportWorkbook.ProductRow product : products) {
            String itemCode = trim(product.getItemCode());
            if (!productCodes.add(itemCode)) {
                throw new IllegalArgumentException("产品编码重复:" + itemCode);
            }
            MesProRouteProductSaveReqVO request = new MesProRouteProductSaveReqVO();
            request.setRouteId(routeId);
            request.setItemId(itemId(itemCode));
            MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(trim(product.getWarehouseCode()));
            MesWmWarehouseLocationDO location = locationService.getWarehouseLocationByCode(trim(product.getLocationCode()));
            MesWmWarehouseAreaDO area = areaService.getWarehouseAreaByCode(trim(product.getAreaCode()));
            if (warehouse == null || location == null || area == null) {
                throw new IllegalArgumentException("产品 " + itemCode + " çš„仓库、库区或库位编码不存在");
            }
            request.setWarehouseId(warehouse.getId());
            request.setLocationId(location.getId());
            request.setAreaId(area.getId());
            request.setQuantity(product.getQuantity());
            request.setProductionTime(product.getProductionTime());
            request.setTimeUnitType(product.getTimeUnitType());
            request.setRemark(product.getRemark());
            productService.createRouteProduct(request);
        }
        Set<String> processCodes = new HashSet<>();
        Set<Integer> sorts = new HashSet<>();
        for (MesProRouteCompleteImportWorkbook.ProcessRow process : processes) {
            String processCode = trim(process.getProcessCode());
            if (!processCodes.add(processCode) || process.getSort() == null || !sorts.add(process.getSort())) {
                throw new IllegalArgumentException("工序编码或排序重复/为空:" + processCode);
            }
            MesProProcessDO processDO = processMapper.selectByCode(processCode);
            if (processDO == null) {
                throw new IllegalArgumentException("工序编码不存在:" + processCode);
            }
            MesProRouteProcessSaveReqVO request = new MesProRouteProcessSaveReqVO();
            request.setRouteId(routeId);
            request.setProcessId(processDO.getId());
            request.setSort(process.getSort());
            request.setParamTemplateId(process.getParamTemplateId());
            request.setYieldRate(process.getYieldRate());
            request.setLinkType(process.getLinkType());
            request.setPrepareTime(process.getPrepareTime());
            request.setWaitTime(process.getWaitTime());
            request.setColorCode(process.getColorCode());
            request.setKeyFlag(flag(process.getKeyFlag(), processCode, "关键工序"));
            request.setCheckFlag(flag(process.getCheckFlag(), processCode, "质检工序"));
            request.setBackflushFlag(flag(process.getBackflushFlag(), processCode, "倒冲"));
            request.setRemark(process.getRemark());
            List<MesProRouteProcessSaveReqVO.OutputItem> outputs = new ArrayList<>();
            for (MesProRouteCompleteImportWorkbook.OutputRow output : rows(book.getOutputs())) {
                if (!code.equals(trim(output.getRouteCode())) || !processCode.equals(trim(output.getProcessCode()))) {
                    continue;
                }
                MesProRouteProcessSaveReqVO.OutputItem value = new MesProRouteProcessSaveReqVO.OutputItem();
                value.setItemId(itemId(output.getItemCode()));
                if (StrUtil.isNotBlank(output.getProductCode())) {
                    String ownerCode = trim(output.getProductCode());
                    if (!productCodes.contains(ownerCode)) {
                        throw new IllegalArgumentException("产出归属产品未关联路线:" + ownerCode);
                    }
                    value.setProductId(itemId(ownerCode));
                }
                value.setSort(output.getSort());
                value.setRemark(output.getRemark());
                outputs.add(value);
            }
            request.setOutputs(outputs);
            processService.createRouteProcess(request);
        }
        for (MesProRouteCompleteImportWorkbook.OutputRow output : rows(book.getOutputs())) {
            if (code.equals(trim(output.getRouteCode())) && !processCodes.contains(trim(output.getProcessCode()))) {
                throw new IllegalArgumentException("产出引用了未配置的工序:" + output.getProcessCode());
            }
        }
        for (MesProRouteCompleteImportWorkbook.InputRow input : rows(book.getInputs())) {
            if (!code.equals(trim(input.getRouteCode()))) {
                continue;
            }
            String processCode = trim(input.getProcessCode());
            String productCode = trim(input.getProductCode());
            if (!processCodes.contains(processCode) || !productCodes.contains(productCode)) {
                throw new IllegalArgumentException("投料引用了未配置的工序或产品:" + processCode + "/" + productCode);
            }
            MesProRouteProductBomSaveReqVO request = new MesProRouteProductBomSaveReqVO();
            request.setRouteId(routeId);
            request.setProcessId(processMapper.selectByCode(processCode).getId());
            request.setProductId(itemId(productCode));
            request.setItemId(itemId(input.getItemCode()));
            request.setQuantity(input.getQuantity());
            request.setRemark(input.getRemark());
            bomService.createRouteProductBom(request);
        }
    }
    private Long itemId(String code) {
        String normalized = trim(code);
        MesMdItemDO item = StrUtil.isBlank(normalized) ? null : itemService.getItemByCode(normalized);
        if (item == null) {
            throw new IllegalArgumentException("物料编码不存在:" + normalized);
        }
        return item.getId();
    }
    private static boolean flag(String value, String processCode, String field) {
        String normalized = trim(value);
        if (StrUtil.isBlank(normalized) || "否".equals(normalized)
                || "false".equalsIgnoreCase(normalized) || "0".equals(normalized)) {
            return false;
        }
        if ("是".equals(normalized) || "true".equalsIgnoreCase(normalized) || "1".equals(normalized)) {
            return true;
        }
        throw new IllegalArgumentException("工序 " + processCode + " çš„" + field + "只能填写是或否");
    }
    private static String trim(String value) {
        return StrUtil.trim(value);
    }
    private static <T> List<T> rows(List<T> values) {
        return values == null ? List.of() : values;
    }
    private static void collectUnknownRoutes(Map<String, String> failures, Set<String> known,
                                             String sheet, List<String> codes) {
        for (String code : codes) {
            if (!known.contains(trim(code))) {
                failures.put(sheet + "表路线 " + code, "路线表中不存在此路线编码");
            }
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java
@@ -67,6 +67,27 @@
    List<MesProRouteProcessDO> getRouteProcessListByProductId(Long productId);
    /**
     * èŽ·å¾—å·¥åºåœ¨æŒ‡å®šäº§å“ä¸‹çš„äº§å‡ºç‰©æ–™ç¼–å·
     *
     * æŒ‰äº§å‡ºå½’属过滤:归属为空(多产品通用)或归属等于指定产品的产出。
     * æ— å½’属数据时返回 {@code null},调用方需自行回退;有归属数据但该产品无产出时返回空列表。
     *
     * @param routeProcessId å·¥è‰ºè·¯çº¿å·¥åºç¼–号
     * @param productId      äº§å“ç¼–号
     * @return äº§å‡ºç‰©æ–™ç¼–号列表(按排序),无归属数据时为 null
     */
    List<Long> getOutputItemIdsByRouteProcessId(Long routeProcessId, Long productId);
    /**
     * æŒ‰å·¥è‰ºè·¯çº¿å·¥åºç¼–号批量获得产出归属配置
     *
     * @param routeProcessIds å·¥è‰ºè·¯çº¿å·¥åºç¼–号数组
     * @return äº§å‡ºå½’属列表
     */
    List<cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO>
        getRouteProcessOutputListByRouteProcessIds(Collection<Long> routeProcessIds);
    /**
     * æŒ‰äº§å“èŽ·å¾—å·¥åºæŠ•æ–™BOM Map
     *
     * key = å·¥åºID,value = è¯¥å·¥åºä¸‹çš„æŠ•æ–™BOM明细列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
@@ -1,12 +1,15 @@
package cn.iocoder.yudao.module.mes.service.pro.route;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
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.MesProRouteProcessOutputDO;
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.dal.mysql.pro.route.MesProRouteProcessOutputMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@@ -14,11 +17,16 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -35,6 +43,8 @@
    @Resource
    private MesProRouteProcessMapper routeProcessMapper;
    @Resource
    private MesProRouteProcessOutputMapper routeProcessOutputMapper;
    @Resource
    @Lazy
@@ -63,13 +73,10 @@
        // 2. æ’å…¥
        MesProRouteProcessDO routeProcess = BeanUtils.toBean(createReqVO, MesProRouteProcessDO.class);
        if (CollUtil.isNotEmpty(createReqVO.getOutputItemIds())) {
            routeProcess.setOutputItemId(
                createReqVO.getOutputItemIds().stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(",")));
        }
        routeProcess.setOutputItemId(resolveOutputItemId(createReqVO));
        routeProcessMapper.insert(routeProcess);
        // 2.1 ä¿å­˜äº§å‡ºå½’属配置
        saveRouteProcessOutputs(routeProcess, createReqVO);
        // 3. æ›´æ–°å·¥åºé“¾
        rebuildProcessChain(createReqVO.getRouteId());
@@ -81,7 +88,7 @@
    public void updateRouteProcess(MesProRouteProcessSaveReqVO updateReqVO) {
        // 1.0 å·²å¯ç”¨çš„工艺路线,不允许操作
        routeService.validateRouteNotEnable(updateReqVO.getRouteId());
        // 1.1 æ ¡éªŒå­˜åœ¨ï¼›éƒ¨åˆ†æ›´æ–°ï¼ˆå¦‚仅调整产出产品)时未传的必填字段用现有值回填
        // 1.1 æ ¡éªŒå­˜åœ¨ï¼›å…¼å®¹åªè°ƒæ•´äº§å‡ºé…ç½®çš„部分更新请求
        MesProRouteProcessDO existing = validateRouteProcessExists(updateReqVO.getId());
        if (updateReqVO.getProcessId() == null) {
            updateReqVO.setProcessId(existing.getProcessId());
@@ -98,13 +105,11 @@
        // 2. æ›´æ–°
        MesProRouteProcessDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProcessDO.class);
        if (CollUtil.isNotEmpty(updateReqVO.getOutputItemIds())) {
            updateObj.setOutputItemId(
                updateReqVO.getOutputItemIds().stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(",")));
        }
        // ç©ºæ•°ç»„也要显式写入空串,否则 MyBatis-Plus é»˜è®¤å¿½ç•¥ null,用户清空产出品后旧配置仍会残留。
        updateObj.setOutputItemId(resolveOutputItemId(updateReqVO));
        routeProcessMapper.updateById(updateObj);
        // 2.1 è¦†ç›–产出归属配置
        saveRouteProcessOutputs(updateObj, updateReqVO);
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(updateReqVO.getRouteId());
@@ -118,8 +123,9 @@
        // 1.2 å·²å¯ç”¨çš„工艺路线,不允许操作
        routeService.validateRouteNotEnable(routeProcess.getRouteId());
        // 2. åˆ é™¤
        // 2. åˆ é™¤ï¼ˆå«äº§å‡ºå½’属配置)
        routeProcessMapper.deleteById(id);
        routeProcessOutputMapper.deleteByRouteProcessId(id);
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(routeProcess.getRouteId());
@@ -218,7 +224,165 @@
            return Collections.emptyList();
        }
        // 2. è¿”回该工艺路线的工序列表
        return routeProcessMapper.selectListByRouteId(routeProduct.getRouteId());
        List<MesProRouteProcessDO> list = routeProcessMapper.selectListByRouteId(routeProduct.getRouteId());
        // 3. æŒ‰äº§å‡ºå½’属过滤:通用产出 + å½“前产品专属产出
        Map<Long, List<MesProRouteProcessOutputDO>> outputMap = routeProcessOutputMapper
                .selectListByRouteId(routeProduct.getRouteId()).stream()
                .collect(Collectors.groupingBy(MesProRouteProcessOutputDO::getRouteProcessId));
        for (MesProRouteProcessDO process : list) {
            List<MesProRouteProcessOutputDO> outputs = outputMap.get(process.getId());
            if (CollUtil.isEmpty(outputs)) {
                // å­˜é‡æ•°æ®æœªæ‹†åˆ†å½’属时,退回旧的“剔除同路线其它产品”逻辑
                filterOtherProductOutputs(Collections.singletonList(process), routeProduct);
                continue;
            }
            String filtered = outputs.stream()
                    .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                    .map(MesProRouteProcessOutputDO::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
            process.setOutputItemId(filtered.isEmpty() ? null : filtered);
        }
        return list;
    }
    @Override
    public List<Long> getOutputItemIdsByRouteProcessId(Long routeProcessId, Long productId) {
        List<MesProRouteProcessOutputDO> outputs = routeProcessOutputMapper.selectListByRouteProcessId(routeProcessId);
        if (CollUtil.isEmpty(outputs)) {
            // æœªé…ç½®äº§å‡ºå½’属(存量数据)返回 null,便于调用方回退聚合产出
            return null;
        }
        return outputs.stream()
                .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                .map(MesProRouteProcessOutputDO::getItemId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
    }
    @Override
    public List<MesProRouteProcessOutputDO> getRouteProcessOutputListByRouteProcessIds(Collection<Long> routeProcessIds) {
        if (CollUtil.isEmpty(routeProcessIds)) {
            return Collections.emptyList();
        }
        return routeProcessOutputMapper.selectListByRouteProcessIds(routeProcessIds);
    }
    /**
     * è®¡ç®—工序产出物料的聚合串(逗号分隔,去重)
     *
     * ä¼˜å…ˆä½¿ç”¨äº§å‡ºå½’属配置(outputs),未传时兼容 outputItemIds。
     */
    private String resolveOutputItemId(MesProRouteProcessSaveReqVO reqVO) {
        List<Long> itemIds;
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            itemIds = reqVO.getOutputs().stream()
                    .filter(Objects::nonNull)
                    .sorted(Comparator.comparing(MesProRouteProcessSaveReqVO.OutputItem::getSort,
                            Comparator.nullsLast(Comparator.naturalOrder())))
                    .map(MesProRouteProcessSaveReqVO.OutputItem::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
        } else if (CollUtil.isNotEmpty(reqVO.getOutputItemIds())) {
            itemIds = reqVO.getOutputItemIds().stream()
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
        } else {
            itemIds = Collections.emptyList();
        }
        return itemIds.stream().map(String::valueOf).collect(Collectors.joining(","));
    }
    /**
     * è¦†ç›–保存工序产出归属配置
     */
    private void saveRouteProcessOutputs(MesProRouteProcessDO routeProcess,
                                         MesProRouteProcessSaveReqVO reqVO) {
        routeProcessOutputMapper.deleteByRouteProcessId(routeProcess.getId());
        if (CollUtil.isEmpty(reqVO.getOutputs()) && CollUtil.isEmpty(reqVO.getOutputItemIds())) {
            return;
        }
        Set<Long> routeProductIds = routeProductService.getRouteProductListByRouteId(routeProcess.getRouteId())
                .stream().map(MesProRouteProductDO::getItemId).filter(Objects::nonNull).collect(Collectors.toSet());
        List<MesProRouteProcessOutputDO> outputs = new ArrayList<>();
        Set<String> keys = new HashSet<>();
        int index = 0;
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            for (MesProRouteProcessSaveReqVO.OutputItem output : reqVO.getOutputs()) {
                if (output == null || output.getItemId() == null) {
                    continue;
                }
                index++;
                if (!keys.add(output.getProductId() + "_" + output.getItemId())) {
                    continue;
                }
                outputs.add(buildOutput(routeProcess, output.getProductId(), output.getItemId(),
                        output.getSort() != null ? output.getSort() : index, output.getRemark()));
            }
        } else {
            // å…¼å®¹åªä¼  outputItemIds çš„调用方:产出是路线产品时归属到自身,否则视为通用
            for (Long itemId : reqVO.getOutputItemIds()) {
                if (itemId == null) {
                    continue;
                }
                index++;
                Long productId = routeProductIds.contains(itemId) ? itemId : null;
                if (!keys.add(productId + "_" + itemId)) {
                    continue;
                }
                outputs.add(buildOutput(routeProcess, productId, itemId, index, null));
            }
        }
        if (CollUtil.isNotEmpty(outputs)) {
            routeProcessOutputMapper.insertBatch(outputs);
        }
    }
    private MesProRouteProcessOutputDO buildOutput(MesProRouteProcessDO routeProcess, Long productId,
                                                   Long itemId, Integer sort, String remark) {
        return MesProRouteProcessOutputDO.builder()
                .routeProcessId(routeProcess.getId())
                .routeId(routeProcess.getRouteId())
                .processId(routeProcess.getProcessId())
                .productId(productId)
                .itemId(itemId)
                .sort(sort)
                .remark(remark)
                .build();
    }
    /**
     * è¿‡æ»¤äº§å‡ºç‰©æ–™ï¼šå‰”除同一工艺路线下其它产品的产出,只保留当前产品的产出和半成品产出
     */
    private void filterOtherProductOutputs(List<MesProRouteProcessDO> list, MesProRouteProductDO routeProduct) {
        if (CollUtil.isEmpty(list) || routeProduct.getItemId() == null) {
            return;
        }
        Set<Long> otherProductIds = routeProductService.getRouteProductListByRouteId(routeProduct.getRouteId())
                .stream()
                .map(MesProRouteProductDO::getItemId)
                .filter(itemId -> itemId != null && !itemId.equals(routeProduct.getItemId()))
                .collect(Collectors.toSet());
        if (otherProductIds.isEmpty()) {
            return;
        }
        for (MesProRouteProcessDO process : list) {
            String outputItemId = process.getOutputItemId();
            if (outputItemId == null || outputItemId.isBlank()) {
                continue;
            }
            String filtered = Arrays.stream(outputItemId.split(","))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .filter(s -> !otherProductIds.contains(NumberUtil.parseLong(s, -1L)))
                    .collect(Collectors.joining(","));
            process.setOutputItemId(filtered.isEmpty() ? null : filtered);
        }
    }
    @Override
@@ -243,6 +407,7 @@
    @Override
    public void deleteRouteProcessByRouteId(Long routeId) {
        routeProcessMapper.deleteByRouteId(routeId);
        routeProcessOutputMapper.deleteByRouteId(routeId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java
@@ -4,7 +4,17 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.product.MesProRouteProductSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
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.mysql.pro.route.MesProRouteProductMapper;
import cn.iocoder.yudao.module.mes.enums.md.MesMdItemTypeEnum;
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.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -12,6 +22,9 @@
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -34,13 +47,25 @@
    @Resource
    @Lazy
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    public Long createRouteProduct(MesProRouteProductSaveReqVO createReqVO) {
        // 1.0 å·²å¯ç”¨çš„工艺路线,不允许操作
        routeService.validateRouteNotEnable(createReqVO.getRouteId());
        // 1.1 æ ¡éªŒäº§å“å”¯ä¸€æ€§ï¼ˆä¸€ä¸ªäº§å“åªèƒ½å…³è”一条工艺路线)
        // 1.1 æ ¡éªŒäº§å“å­˜åœ¨ã€å¯ç”¨ä¸”类型正确
        validateProductItem(createReqVO.getItemId());
        // 1.2 æ ¡éªŒäº§å“å”¯ä¸€æ€§ï¼ˆä¸€ä¸ªäº§å“åªèƒ½å…³è”一条工艺路线)
        validateItemUnique(null, createReqVO.getItemId());
        warehouseAreaService.validateWarehouseAreaExists(createReqVO.getWarehouseId(),
                createReqVO.getLocationId(), createReqVO.getAreaId());
        // 2. æ’å…¥
        MesProRouteProductDO routeProduct = BeanUtils.toBean(createReqVO, MesProRouteProductDO.class);
@@ -53,9 +78,16 @@
        // 1.0 å·²å¯ç”¨çš„工艺路线,不允许操作
        routeService.validateRouteNotEnable(updateReqVO.getRouteId());
        // 1.1 æ ¡éªŒå­˜åœ¨
        validateRouteProductExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒäº§å“å”¯ä¸€æ€§
        MesProRouteProductDO existing = routeProductMapper.selectById(updateReqVO.getId());
        validateRouteProductExists(existing);
        // åŽ†å²è·¯çº¿äº§å“å¯èƒ½é‡‡ç”¨æ—§åˆ†ç±»ï¼›åªä¿®æ”¹ä»“ä½ç­‰é…ç½®æ—¶ä¸é‡æ–°æ‹¦æˆªåŽŸæœ‰ç‰©æ–™ã€‚
        if (ObjUtil.notEqual(existing.getItemId(), updateReqVO.getItemId())) {
            validateProductItem(updateReqVO.getItemId());
        }
        // 1.3 æ ¡éªŒäº§å“å”¯ä¸€æ€§
        validateItemUnique(updateReqVO.getId(), updateReqVO.getItemId());
        warehouseAreaService.validateWarehouseAreaExists(updateReqVO.getWarehouseId(),
                updateReqVO.getLocationId(), updateReqVO.getAreaId());
        // 2. æ›´æ–°
        MesProRouteProductDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteProductDO.class);
@@ -89,6 +121,44 @@
        }
    }
    private void validateProductItem(Long itemId) {
        MesMdItemDO item = itemService.validateItemExistsAndEnable(itemId);
        Long typeId = item.getItemTypeId();
        Set<Long> visited = new HashSet<>();
        while (typeId != null && !MesMdItemTypeDO.PARENT_ID_ROOT.equals(typeId) && visited.add(typeId)) {
            MesMdItemTypeDO itemType = itemTypeService.getItemType(typeId);
            if (itemType == null) {
                break;
            }
            // å…¼å®¹æ—§åˆ†ç±»ï¼šå¯èƒ½æœªè®¾ç½® PRODUCT æ ‡è¯†ï¼Œä¹Ÿæœªé‡‡ç”¨ FG/SF ç¼–码。
            if (MesMdItemTypeEnum.isProduct(itemType.getItemOrProduct())
                    || "FG".equalsIgnoreCase(itemType.getCode())
                    || "SF".equalsIgnoreCase(itemType.getCode())
                    || isProductCategoryName(itemType.getName())) {
                return;
            }
            typeId = itemType.getParentId();
        }
        // MES å…¼å®¹åˆ†ç±»å¯èƒ½ä»æ ‡è®°ä¸º ITEM;以 MDM ç‰©æ–™ä¸»æ•°æ®çš„æˆå“/半成品类型兜底。
        CommonResult<MdmItemRespDTO> mdmResult = mdmItemApi.getItemByCode(item.getCode());
        if (mdmResult != null && mdmResult.isSuccess() && mdmResult.getData() != null) {
            MdmItemRespDTO mdmItem = mdmResult.getData();
            Integer mdmType = mdmItem.getItemType();
            // åŽ†å²åŒæ­¥æ›¾å°†åˆ†ç±» ID å†™å…¥ itemType;与 categoryId ç›¸åŒçš„值不能当作类型枚举。
            if (!Objects.equals(mdmItem.getCategoryId(), mdmType == null ? null : mdmType.longValue())
                    && (MdmItemTypeEnum.PRODUCT.getType().equals(mdmType)
                    || MdmItemTypeEnum.SEMI_FINISHED.getType().equals(mdmType))) {
                return;
            }
        }
        throw exception(PRO_ROUTE_PRODUCT_ITEM_TYPE_INVALID, item.getName());
    }
    private boolean isProductCategoryName(String name) {
        return name != null && (name.contains("半成品") || name.contains("产成品")
                || (name.contains("成品") && !name.contains("非成品")));
    }
    private void validateItemUnique(Long id, Long itemId) {
        MesProRouteProductDO existing = routeProductMapper.selectByItemId(itemId);
        if (existing == null) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mes.service.pro.route;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
@@ -90,4 +92,14 @@
        return convertMap(getRouteList(ids), MesProRouteDO::getId);
    }
    /**
     * æ‰¹é‡å¯¼å…¥å·¥è‰ºè·¯çº¿
     *
     * @param importList    å¯¼å…¥å·¥è‰ºè·¯çº¿åˆ—表
     * @param updateSupport æ˜¯å¦æ”¯æŒæ›´æ–°
     * @return å¯¼å…¥ç»“æžœ
     */
    MesProRouteImportRespVO importRouteList(List<MesProRouteImportExcelVO> importList,
                                            boolean updateSupport);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
@@ -3,27 +3,46 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.pro.route.vo.MesProRouteImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
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.mysql.pro.route.MesProRouteMapper;
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.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Comparator;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicInteger;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -52,6 +71,12 @@
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    public Long createRoute(MesProRouteSaveReqVO createReqVO) {
@@ -141,13 +166,115 @@
        // 3. æ‰€æœ‰äº§å“å¿…须配置了 BOM æ¶ˆè€—
        List<MesProRouteProductDO> productList = routeProductService.getRouteProductListByRouteId(routeId);
        for (MesProRouteProductDO product : productList) {
            MesMdItemDO item = itemService.validateItemExists(product.getItemId());
            if (product.getWarehouseId() == null || product.getLocationId() == null || product.getAreaId() == null) {
                throw exception(PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED, item.getName());
            }
            warehouseAreaService.validateWarehouseAreaExists(product.getWarehouseId(),
                    product.getLocationId(), product.getAreaId());
            List<MesProRouteProductBomDO> bomList = routeProductBomService
                    .getRouteProductBomList(routeId, null, product.getItemId());
            if (CollUtil.isEmpty(bomList)) {
                MesMdItemDO item = itemService.validateItemExists(product.getItemId());
                throw exception(PRO_ROUTE_ENABLE_PRODUCT_NO_BOM, item.getName());
            }
        }
        validateProcessOutputs(processList, productList);
    }
    private void validateProcessOutputs(List<MesProRouteProcessDO> processList,
                                        List<MesProRouteProductDO> productList) {
        if (CollUtil.isEmpty(productList)) {
            throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, processList.get(processList.size() - 1).getProcessId());
        }
        Set<Long> boundItemIds = productList.stream().map(MesProRouteProductDO::getItemId).collect(Collectors.toSet());
        Set<Long> finishedItemIds = productList.stream()
                .map(MesProRouteProductDO::getItemId)
                .filter(itemId -> isItemUnderCategory(itemId, "FG"))
                .collect(Collectors.toSet());
        MesProRouteProcessDO lastProcess = processList.stream()
                .max(Comparator.comparing(MesProRouteProcessDO::getSort)).orElseThrow();
        for (MesProRouteProcessDO process : processList) {
            Set<Long> outputIds = parseOutputItemIds(process.getOutputItemId());
            boolean last = process.getId().equals(lastProcess.getId());
            if (CollUtil.isEmpty(outputIds)) {
                // ä¸­é—´å·¥åºä¸ä¸€å®šå½¢æˆå¯å…¥åº“的独立半成品,允许不配置产出;末道工序必须产出成品。
                if (last) {
                    throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId());
                }
                continue;
            }
            for (Long outputId : outputIds) {
                if (!boundItemIds.contains(outputId)) {
                    MesMdItemDO item = itemService.validateItemExists(outputId);
                    throw exception(PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND, item.getName());
                }
            }
            if (last) {
                if (finishedItemIds.isEmpty() || !outputIds.equals(finishedItemIds)) {
                    throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId());
                }
            } else if (outputIds.stream().anyMatch(itemId -> !isItemUnderCategory(itemId, "SF"))) {
                throw exception(PRO_ROUTE_MIDDLE_OUTPUT_INVALID, process.getProcessId());
            }
        }
        // 4. æœ«é“工序成品产出必须按产品归属,避免“通用”产出串到其它产品的工单
        List<MesProRouteProcessOutputDO> lastOutputs = routeProcessService
                .getRouteProcessOutputListByRouteProcessIds(Collections.singletonList(lastProcess.getId()));
        if (CollUtil.isNotEmpty(lastOutputs)) {
            boolean attributionInvalid = lastOutputs.stream().anyMatch(output ->
                    output.getProductId() == null || !output.getProductId().equals(output.getItemId()));
            Set<Long> attributedProducts = lastOutputs.stream()
                    .map(MesProRouteProcessOutputDO::getProductId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            if (attributionInvalid || !attributedProducts.containsAll(finishedItemIds)) {
                throw exception(PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED, lastProcess.getProcessId());
            }
        }
    }
    private Set<Long> parseOutputItemIds(String value) {
        if (StrUtil.isBlank(value)) {
            return Collections.emptySet();
        }
        return Arrays.stream(value.split(",")).map(String::trim).filter(StrUtil::isNotBlank)
                .map(Long::valueOf).collect(Collectors.toSet());
    }
    private boolean isItemUnderCategory(Long itemId, String rootCode) {
        MesMdItemDO item = itemService.validateItemExists(itemId);
        MesMdItemTypeDO type = itemTypeService.getItemType(item.getItemTypeId());
        while (type != null) {
            if ("SF".equalsIgnoreCase(type.getCode())
                    || (type.getName() != null && type.getName().contains("半成品"))) {
                return "SF".equals(rootCode);
            }
            if ("FG".equalsIgnoreCase(type.getCode()) || isFinishedCategoryName(type.getName())) {
                return "FG".equals(rootCode);
            }
            if (type.getParentId() == null || MesMdItemTypeDO.PARENT_ID_ROOT.equals(type.getParentId())) {
                break;
            }
            type = itemTypeService.getItemType(type.getParentId());
        }
        // MES åˆ†ç±»æœªä½¿ç”¨ FG/SF ç¼–码时,按 MDM ä¸»æ•°æ®çš„物料类型判定。
        CommonResult<MdmItemRespDTO> mdmResult = mdmItemApi.getItemByCode(item.getCode());
        if (mdmResult == null || !mdmResult.isSuccess() || mdmResult.getData() == null) {
            return false;
        }
        MdmItemRespDTO mdmItem = mdmResult.getData();
        Integer mdmType = mdmItem.getItemType();
        // åŽ†å²åŒæ­¥å¯èƒ½æŠŠåˆ†ç±» ID å­˜åœ¨ itemType,不能把分类 2/3 è¯¯åˆ¤ä¸ºæˆå“/半成品。
        if (Objects.equals(mdmItem.getCategoryId(), mdmType == null ? null : mdmType.longValue())) {
            return false;
        }
        return "FG".equals(rootCode) ? MdmItemTypeEnum.PRODUCT.getType().equals(mdmType)
                : "SF".equals(rootCode) && MdmItemTypeEnum.SEMI_FINISHED.getType().equals(mdmType);
    }
    private boolean isFinishedCategoryName(String name) {
        return name != null && !name.contains("半成品") && !name.contains("非成品")
                && (name.contains("产成品") || name.contains("成品"));
    }
    @Override
@@ -181,4 +308,52 @@
        return routeMapper.selectByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProRouteImportRespVO importRouteList(List<MesProRouteImportExcelVO> importList,
                                                   boolean updateSupport) {
        // 1. æ ¡éªŒéžç©º
        if (CollUtil.isEmpty(importList)) {
            throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY);
        }
        // 2. é€è¡Œå¤„理
        MesProRouteImportRespVO respVO = MesProRouteImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        AtomicInteger index = new AtomicInteger(1);
        importList.forEach(importItem -> {
            int currentIndex = index.getAndIncrement();
            String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " è¡Œ");
            if (StrUtil.isBlank(importItem.getCode())) {
                respVO.getFailureCodes().put(key, "工艺路线编码不能为空");
                return;
            }
            if (StrUtil.isBlank(importItem.getName())) {
                respVO.getFailureCodes().put(key, "工艺路线名称不能为空");
                return;
            }
            // 2.1 åˆ¤æ–­ï¼šåˆ›å»º or æ›´æ–°
            MesProRouteDO existRoute = routeMapper.selectByCode(importItem.getCode());
            if (existRoute == null) {
                // 2.1.1 æ–°å¢žï¼ˆä¸Žæ‰‹å·¥åˆ›å»ºä¸€è‡´ï¼šé»˜è®¤ç¦ç”¨ï¼Œé€šè¿‡å¯ç”¨æŽ¥å£æ ¡éªŒåŽå¯ç”¨ï¼‰
                MesProRouteDO route = BeanUtils.toBean(importItem, MesProRouteDO.class);
                route.setId(null);
                route.setStatus(CommonStatusEnum.DISABLE.getStatus());
                routeMapper.insert(route);
                respVO.getCreateCodes().add(importItem.getCode());
            } else if (updateSupport) {
                // 2.1.2 æ›´æ–°ï¼ˆçŠ¶æ€ç”±å¯ç”¨/禁用接口单独维护,导入不修改状态)
                MesProRouteDO updateObj = BeanUtils.toBean(importItem, MesProRouteDO.class);
                updateObj.setId(existRoute.getId());
                updateObj.setCode(existRoute.getCode());
                routeMapper.updateById(updateObj);
                respVO.getUpdateCodes().add(importItem.getCode());
            } else {
                respVO.getFailureCodes().put(key, "工艺路线编码已存在");
            }
        });
        return respVO;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -150,11 +150,7 @@
        if (itemId == null) {
            MesProRouteProcessDO routeProcess = routeProcessService
                    .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId());
            if (routeProcess != null && routeProcess.getOutputItemId() != null) {
                itemId = Long.valueOf(routeProcess.getOutputItemId().split(",")[0].trim());
            } else {
                itemId = workOrder.getProductId();
            }
            itemId = resolveTaskItemId(routeProcess, workOrder.getProductId());
        }
        MesMdItemDO item = itemService.validateItemExists(itemId);
@@ -350,6 +346,25 @@
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_ENABLE_NO_PROCESS);
        }
        return routeProcesses;
    }
    /** å½“前产品的任务产出,优先使用工序的产品归属配置。 */
    private Long resolveTaskItemId(MesProRouteProcessDO routeProcess, Long productId) {
        if (routeProcess == null) {
            return productId;
        }
        if (routeProcess.getId() != null) {
            List<Long> outputIds = routeProcessService
                    .getOutputItemIdsByRouteProcessId(routeProcess.getId(), productId);
            if (outputIds != null) {
                return CollUtil.isNotEmpty(outputIds) ? outputIds.get(0) : productId;
            }
        }
        String outputItemId = routeProcess.getOutputItemId();
        if (outputItemId != null && !outputItemId.isBlank()) {
            return Long.valueOf(outputItemId.split(",")[0].trim());
        }
        return productId;
    }
    @Override
@@ -563,7 +578,7 @@
        List<Long> taskIds = new java.util.ArrayList<>();
        for (MesProRouteProcessDO rp : routeProcesses) {
            cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO proc = procMap.get(rp.getProcessId());
            Long itemId = rp.getOutputItemId() != null ? Long.valueOf(rp.getOutputItemId().split(",")[0].trim()) : workOrder.getProductId();
            Long itemId = resolveTaskItemId(rp, workOrder.getProductId());
            cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO item = itemService.validateItemExists(itemId);
            MesProTaskDO task = new MesProTaskDO()
                    .setWorkOrderId(workOrder.getId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java
@@ -62,6 +62,15 @@
    List<MesProWorkOrderProcessDO> getWorkOrderProcessListByWorkOrderId(Long workOrderId);
    /**
     * æ ¹æ®å·¥å•编号 + å·¥åºç¼–号获得工单工序
     *
     * @param workOrderId å·¥å•编号
     * @param processId   å·¥åºç¼–号
     * @return å·¥å•工序
     */
    MesProWorkOrderProcessDO getWorkOrderProcessByWorkOrderIdAndProcessId(Long workOrderId, Long processId);
    /**
     * æ ¹æ®å·¥å•编号获得最后一道工序
     *
     * @param workOrderId å·¥å•编号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
@@ -7,12 +7,15 @@
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.route.MesProRouteProductDO;
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;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -48,6 +51,10 @@
    private MesProProcessService processService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    @Lazy
    private MesProWorkOrderBomService workOrderBomService;
@@ -89,6 +96,9 @@
                    .collect(Collectors.joining(",")));
        }
        workOrderProcessMapper.updateById(updateObj);
        // æŠ¥å·¥ä»“库允许置空,单独更新
        workOrderProcessMapper.updateWarehouseById(updateReqVO.getId(), updateReqVO.getWarehouseId(),
                updateReqVO.getLocationId(), updateReqVO.getAreaId());
        // 3. é‡å»ºå·¥åºé“¾
        rebuildProcessChain(updateReqVO.getWorkOrderId());
@@ -122,6 +132,11 @@
    }
    @Override
    public MesProWorkOrderProcessDO getWorkOrderProcessByWorkOrderIdAndProcessId(Long workOrderId, Long processId) {
        return workOrderProcessMapper.selectByWorkOrderIdAndProcessId(workOrderId, processId);
    }
    @Override
    public MesProWorkOrderProcessDO getLastWorkOrderProcessByWorkOrderId(Long workOrderId) {
        return workOrderProcessMapper.selectLastByWorkOrderId(workOrderId);
    }
@@ -146,6 +161,8 @@
        if (CollUtil.isEmpty(routeProcessList)) {
            return Collections.emptyList();
        }
        // å·¥è‰ºè·¯çº¿äº§å“ä¸Šé…ç½®çš„æŠ¥å·¥ä»“库作为工序默认值,下单时可再修改
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        // è½¬æ¢ä¸ºå·¥å•工序 DO(不设置 workOrderId,前端保存时再设置)
        List<MesProWorkOrderProcessDO> processList = new ArrayList<>(routeProcessList.size());
        for (MesProRouteProcessDO routeProcess : routeProcessList) {
@@ -163,6 +180,9 @@
                    .checkFlag(routeProcess.getCheckFlag())
                    .backflushFlag(routeProcess.getBackflushFlag())
                    .outputItemId(routeProcess.getOutputItemId())
                    .warehouseId(routeProduct != null ? routeProduct.getWarehouseId() : null)
                    .locationId(routeProduct != null ? routeProduct.getLocationId() : null)
                    .areaId(routeProduct != null ? routeProduct.getAreaId() : null)
                    .remark(routeProcess.getRemark())
                    .build());
        }
@@ -214,6 +234,9 @@
                    updateObj.setOutputItemId(item.getOutputItemIds().stream().map(String::valueOf).collect(Collectors.joining(",")));
                }
                workOrderProcessMapper.updateById(updateObj);
                // æŠ¥å·¥ä»“库允许置空,单独更新
                workOrderProcessMapper.updateWarehouseById(item.getId(), item.getWarehouseId(),
                        item.getLocationId(), item.getAreaId());
            }
        }
@@ -279,7 +302,12 @@
                throw exception(PRO_WORK_ORDER_PROCESS_DUPLICATE);
            }
        }
        // 4. æ ¡éªŒå…³é”®å·¥åºå”¯ä¸€
        // 4. æ ¡éªŒæŠ¥å·¥ä»“库层级(仓库/库区/库位同时填写时校验归属关系)
        if (reqVO.getWarehouseId() != null && reqVO.getLocationId() != null && reqVO.getAreaId() != null) {
            warehouseAreaService.validateWarehouseAreaExists(reqVO.getWarehouseId(),
                    reqVO.getLocationId(), reqVO.getAreaId());
        }
        // 5. æ ¡éªŒå…³é”®å·¥åºå”¯ä¸€
        if (Boolean.TRUE.equals(reqVO.getKeyFlag())) {
            for (MesProWorkOrderProcessDO process : list) {
                if (reqVO.getId() != null && reqVO.getId().equals(process.getId())) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -8,6 +8,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
@@ -21,6 +23,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -56,6 +60,10 @@
    private MesWmProductProduceDetailService productProduceDetailService;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesWmBatchService batchService;
    @Resource
@@ -152,10 +160,7 @@
        Long batchId = batch != null ? batch.getId() : null;
        String batchCode = batch != null ? batch.getCode() : null;
        // 1.5 èŽ·å–è™šæ‹Ÿçº¿è¾¹åº“
        MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        WarehouseTarget outputTarget = resolveOutputTarget(feedback.getWorkOrderId(), feedback.getProcessId(), feedback.getItemId());
        // 3. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒåˆ†æ”¯å¤„理
        if (checkFlag) {
@@ -179,7 +184,7 @@
                productProduceLineService.createProductProduceLine(unqualifiedLine);
                MesWmProductProduceDetailDO unqualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        unqualifiedLine.getId(), unqualifiedQty,
                        virtualWarehouse.getId(), virtualLocation.getId(), virtualArea.getId());
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(unqualifiedDetail);
            }
            // 3.2.2 åˆæ ¼å“è¡Œ + æ˜Žç»†
@@ -189,7 +194,7 @@
                productProduceLineService.createProductProduceLine(qualifiedLine);
                MesWmProductProduceDetailDO qualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        qualifiedLine.getId(), qualifiedQty,
                        virtualWarehouse.getId(), virtualLocation.getId(), virtualArea.getId());
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(qualifiedDetail);
            }
        }
@@ -249,13 +254,9 @@
        if (produce == null) {
            throw exception(WM_PRODUCT_PRODUCE_NOT_EXISTS);
        }
        // 1.2 èŽ·å–è™šæ‹Ÿçº¿è¾¹åº“
        MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        // 2. é€šè¿‡ sourceLineId ç›´æŽ¥å®šä½å¾…检验行
        MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId);
        WarehouseTarget outputTarget = resolveOutputTarget(produce.getWorkOrderId(), produce.getProcessId(), pendingLine.getItemId());
        // 3A. æƒ…况一:存在不合格品数量,需要拆分行
        if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
@@ -272,7 +273,7 @@
                    .produceId(produce.getId()).lineId(unqualifiedLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
            // 3A.2 åˆæ ¼å“ï¼šå¤ç”¨åŽŸå¾…æ£€è¡Œï¼Œæ›´æ–°æ•°é‡å’ŒçŠ¶æ€ + æ˜Žç»†
            pendingLine.setQuantity(ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO));
@@ -283,7 +284,7 @@
                        .produceId(produce.getId()).lineId(pendingLine.getId())
                        .itemId(pendingLine.getItemId()).quantity(qualifiedQty)
                        .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                        .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                        .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                        .build());
            }
        } else {
@@ -294,7 +295,7 @@
                    .produceId(produce.getId()).lineId(pendingLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(pendingLine.getQuantity())
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
        }
@@ -302,4 +303,28 @@
        finishProductProduce(produce.getId());
    }
    private WarehouseTarget resolveOutputTarget(Long workOrderId, Long processId, Long itemId) {
        if (workOrderId != null && processId != null) {
            MesProWorkOrderProcessDO process = workOrderProcessService
                    .getWorkOrderProcessByWorkOrderIdAndProcessId(workOrderId, processId);
            if (process != null && process.getWarehouseId() != null
                    && process.getLocationId() != null && process.getAreaId() != null) {
                areaService.validateWarehouseAreaExists(process.getWarehouseId(), process.getLocationId(), process.getAreaId());
                return new WarehouseTarget(process.getWarehouseId(), process.getLocationId(), process.getAreaId());
            }
        }
        MesProRouteProductDO product = itemId == null ? null : routeProductService.getRouteProductByItemId(itemId);
        if (product != null && product.getWarehouseId() != null
                && product.getLocationId() != null && product.getAreaId() != null) {
            areaService.validateWarehouseAreaExists(product.getWarehouseId(), product.getLocationId(), product.getAreaId());
            return new WarehouseTarget(product.getWarehouseId(), product.getLocationId(), product.getAreaId());
        }
        MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        MesWmWarehouseLocationDO location = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO area = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        return new WarehouseTarget(warehouse.getId(), location.getId(), area.getId());
    }
    private record WarehouseTarget(Long warehouseId, Long locationId, Long areaId) { }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImplTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.mes.service.pro.route;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.product.MesProRouteProductSaveReqVO;
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.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProductMapper;
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.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MesProRouteProductServiceImplTest {
    @InjectMocks
    private MesProRouteProductServiceImpl service;
    @Mock
    private MesProRouteProductMapper routeProductMapper;
    @Mock
    private MesProRouteService routeService;
    @Mock
    private MesMdItemService itemService;
    @Mock
    private MesMdItemTypeService itemTypeService;
    @Mock
    private MesWmWarehouseAreaService warehouseAreaService;
    @Mock
    private MdmItemApi mdmItemApi;
    @Test
    void updateWarehouseForExistingItemDoesNotRevalidateLegacyCategory() {
        MesProRouteProductSaveReqVO request = request(88L, 815L);
        MesProRouteProductDO existing = new MesProRouteProductDO().setId(88L).setItemId(815L);
        when(routeProductMapper.selectById(88L)).thenReturn(existing);
        when(routeProductMapper.selectByItemId(815L)).thenReturn(existing);
        assertDoesNotThrow(() -> service.updateRouteProduct(request));
        verifyNoInteractions(itemService, itemTypeService);
        verify(routeProductMapper).updateById(any(MesProRouteProductDO.class));
    }
    @Test
    void createAcceptsSemiFinishedItemInSfCategoryTree() {
        MesProRouteProductSaveReqVO request = request(null, 815L);
        when(itemService.validateItemExistsAndEnable(815L))
                .thenReturn(new MesMdItemDO().setId(815L).setName("封装半成品").setItemTypeId(23L));
        when(itemTypeService.getItemType(23L)).thenReturn(new MesMdItemTypeDO()
                .setId(23L).setCode("PACKAGED").setItemOrProduct("ITEM").setParentId(2L));
        when(itemTypeService.getItemType(2L)).thenReturn(new MesMdItemTypeDO()
                .setId(2L).setCode("SF").setParentId(0L));
        assertDoesNotThrow(() -> service.createRouteProduct(request));
        verify(routeProductMapper).insert(any(MesProRouteProductDO.class));
    }
    @Test
    void createRejectsRawMaterialOutsideProductTree() {
        MesProRouteProductSaveReqVO request = request(null, 815L);
        when(itemService.validateItemExistsAndEnable(815L))
                .thenReturn(new MesMdItemDO().setId(815L).setName("原料").setItemTypeId(23L));
        when(itemTypeService.getItemType(23L)).thenReturn(new MesMdItemTypeDO()
                .setId(23L).setCode("RAW").setItemOrProduct("ITEM").setParentId(0L));
        assertThrows(ServiceException.class, () -> service.createRouteProduct(request));
        verify(routeProductMapper, never()).insert(any(MesProRouteProductDO.class));
    }
    @Test
    void createAcceptsNamedSemiFinishedInLegacyItemCategory() {
        MesProRouteProductSaveReqVO request = request(null, 815L);
        MesMdItemDO item = new MesMdItemDO().setId(815L).setCode("74000432")
                .setName("黑果枸杞半成品").setItemTypeId(23L);
        when(itemService.validateItemExistsAndEnable(815L)).thenReturn(item);
        when(itemTypeService.getItemType(23L)).thenReturn(new MesMdItemTypeDO()
                .setId(23L).setCode("FOOD").setName("黑果枸杞半成品")
                .setItemOrProduct("ITEM").setParentId(0L));
        assertDoesNotThrow(() -> service.createRouteProduct(request));
        verifyNoInteractions(mdmItemApi);
        verify(routeProductMapper).insert(any(MesProRouteProductDO.class));
    }
    private static MesProRouteProductSaveReqVO request(Long id, Long itemId) {
        MesProRouteProductSaveReqVO request = new MesProRouteProductSaveReqVO();
        request.setId(id);
        request.setRouteId(44L);
        request.setItemId(itemId);
        request.setWarehouseId(24L);
        request.setLocationId(9L);
        request.setAreaId(9L);
        return request;
    }
}