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/service/pro/route/MesProRouteServiceImpl.java |  177 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 176 insertions(+), 1 deletions(-)

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;
+    }
+
 }

--
Gitblit v1.9.3