From 3edf8c4fc07f9784e2d04ee7a98692bb457749eb Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 26 十月 2026 16:28:10 +0800
Subject: [PATCH] feat(mes): 新增工艺路线完整导入功能并优化相关验证

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java                        |    3 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java                                      |   12 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java                  |   53 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java                                    |   27 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteCompleteImportWorkbook.java           |   87 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java         |   31 
 yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImplTest.java                       |  104 +++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportExcelVO.java                    |   30 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java                           |   76 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java                       |    9 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportRespVO.java                     |   24 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java                                      |    9 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java            |   18 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java            |   25 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java                          |   89 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java |    9 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java                     |   19 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteCompleteImportService.java                        |  243 ++++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java                        |   47 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java                               |   21 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java                      |   20 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java                             |    6 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java    |   18 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java         |   12 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java                           |  195 ++++++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java                   |   82 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java                   |   30 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java                                  |  177 +++++
 sql/mysql/mes_pro_route_import_menu_20261026.sql                                                                                          |   44 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java                   |   21 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java                       |   72 ++
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java          |   37 +
 sql/mysql/mes_pro_route_output_attribution_20261026.sql                                                                                   |   56 +
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java                                     |   15 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java                            |    3 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java                                                  |    8 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java                             |   11 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java           |   32 +
 38 files changed, 1,712 insertions(+), 63 deletions(-)

diff --git a/sql/mysql/mes_pro_route_import_menu_20261026.sql b/sql/mysql/mes_pro_route_import_menu_20261026.sql
new file mode 100644
index 0000000..1d6adf7
--- /dev/null
+++ b/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
+  );
diff --git a/sql/mysql/mes_pro_route_output_attribution_20261026.sql b/sql/mysql/mes_pro_route_output_attribution_20261026.sql
new file mode 100644
index 0000000..50bfdbd
--- /dev/null
+++ b/sql/mysql/mes_pro_route_output_attribution_20261026.sql
@@ -0,0 +1,56 @@
+-- 宸ヨ壓璺嚎宸ュ簭浜у嚭鎸変骇鍝佸綊灞炲強榛樿鎶ュ伐搴撲綅锛圡ySQL 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;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java
index 02191d3..648556e 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteController.java
+++ b/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();
+        }
+    }
+
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
index 1bc0813..32de347 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
+++ b/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 浜у嚭褰掑睘閰嶇疆锛坧roductId 涓虹┖琛ㄧず澶氫骇鍝侀�氱敤锛�
+        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;
     }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java
index b0eaf5c..bf99a79 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProductController.java
+++ b/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()));
         });
     }
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteCompleteImportWorkbook.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteCompleteImportWorkbook.java
new file mode 100644
index 0000000..739a777
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportExcelVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportExcelVO.java
new file mode 100644
index 0000000..6543c9b
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteImportRespVO.java
new file mode 100644
index 0000000..f89b4d7
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java
index 2c8a50e..cc1e2dd 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRoutePageReqVO.java
+++ b/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;
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessOutputVO.java
new file mode 100644
index 0000000..0ee4fbb
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
index ce2aee4..ae6f012 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
+++ b/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 = "浜у嚭鐗╂枡褰掑睘閰嶇疆锛坧roductId 涓虹┖琛ㄧず澶氫骇鍝侀�氱敤锛�")
+    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;
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
index 400d5a5..b35c4f5 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
+++ b/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锛沺roductId 涓虹┖琛ㄧず澶氫骇鍝侀�氱敤锛�")
+    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;
+
+    }
+
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java
index 8df87e9..e9017fe 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductRespVO.java
+++ b/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;
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java
index db5ecf1..846f6e2 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/product/MesProRouteProductSaveReqVO.java
+++ b/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;
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java
index 1cb8944..d75b067 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java
+++ b/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()) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java
index c54f6ea..90ccc27 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java
+++ b/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;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java
index 6ac741a..ce31f6a 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessSaveReqVO.java
+++ b/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;
 
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java
index e430732..c584c40 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java
+++ b/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;
     /**
      * 搴忓彿
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessOutputDO.java
new file mode 100644
index 0000000..39947f6
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java
index 3c34218..07892a1 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProductDO.java
+++ b/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;
     /**
      * 鐢熶骇鏁伴噺
      */
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java
index 318eb1d..7e757d4 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderProcessDO.java
+++ b/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;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java
index 8972079..7987462 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteMapper.java
+++ b/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) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/route/MesProRouteProcessOutputMapper.java
new file mode 100644
index 0000000..46b9af2
--- /dev/null
+++ b/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));
+    }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java
index c3626a2..247ca56 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderProcessMapper.java
+++ b/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));
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
index 9e011de..7d567b5 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
+++ b/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 鐗╂枡鍦ㄦ宸ュ簭宸茬粡閰嶇疆杩�");
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
index 5a10283..3cec5b6 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
+++ b/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);
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
index b68c0de..8ed8807 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
+++ b/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)) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteCompleteImportService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteCompleteImportService.java
new file mode 100644
index 0000000..fe3d8a8
--- /dev/null
+++ b/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, "璺嚎琛ㄤ腑涓嶅瓨鍦ㄦ璺嚎缂栫爜");
+            }
+        }
+    }
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java
index 403218c..591ae6d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessService.java
+++ b/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);
+
+    /**
      * 鎸変骇鍝佽幏寰楀伐搴忔姇鏂橞OM Map
      *
      * key = 宸ュ簭ID锛寁alue = 璇ュ伐搴忎笅鐨勬姇鏂橞OM鏄庣粏鍒楄〃
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
index 4957169..f999ae7 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
+++ b/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);
+    }
+
+    /**
+     * 璁$畻宸ュ簭浜у嚭鐗╂枡鐨勮仛鍚堜覆锛堥�楀彿鍒嗛殧锛屽幓閲嶏級
+     *
+     * 浼樺厛浣跨敤浜у嚭褰掑睘閰嶇疆锛坥utputs锛夛紝鏈紶鏃跺吋瀹� 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);
     }
 
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java
index d5089de..b879221 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImpl.java
+++ b/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 鏍¢獙浜у搧瀛樺湪銆佸惎鐢ㄤ笖绫诲瀷姝g‘
+        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) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java
index a0aa8de..5a1f0a6 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteService.java
+++ b/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);
+
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
index b0c94a2..a50b366 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
+++ b/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;
+    }
+
 }
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
index 88956ea..bc9586d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
+++ b/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())
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java
index d1d3ef0..1cfd1e9 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessService.java
+++ b/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 宸ュ崟缂栧彿
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
index 55a7581..2364935 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
+++ b/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())) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
index 1c45ee4..e8ed301 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
+++ b/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) { }
+
 }
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductServiceImplTest.java
new file mode 100644
index 0000000..15d7edb
--- /dev/null
+++ b/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;
+    }
+}

--
Gitblit v1.9.3