云
4 天以前 f02d0df2f1dfc7a8b775cf50d45375e57628a5ef
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdProductBomServiceImpl.java
@@ -1,10 +1,15 @@
package cn.iocoder.yudao.module.mes.service.md.item;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdProductBomMapper;
import jakarta.annotation.Resource;
@@ -141,4 +146,132 @@
        productBomMapper.deleteByItemId(itemId);
    }
    @Override
    public MesMdProductBomImportRespVO importProductBomList(List<MesMdProductBomImportExcelVO> importList,
                                                            Boolean updateSupport) {
        if (CollUtil.isEmpty(importList)) {
            throw exception(MD_PRODUCT_BOM_IMPORT_LIST_IS_EMPTY);
        }
        MesMdProductBomImportRespVO respVO = MesMdProductBomImportRespVO.builder()
                .createList(new ArrayList<>()).updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>()).build();
        // 1. 现有 BOM 的父子组合索引 + 邻接表(用于判重和闭环检测)
        Map<String, MesMdProductBomDO> existingPairMap = new HashMap<>();
        Map<Long, Set<Long>> graph = new HashMap<>();
        for (MesMdProductBomDO bom : productBomMapper.selectAll()) {
            existingPairMap.put(pairKey(bom.getItemId(), bom.getBomItemId()), bom);
            graph.computeIfAbsent(bom.getItemId(), key -> new HashSet<>()).add(bom.getBomItemId());
        }
        Map<String, Optional<MesMdItemDO>> itemCache = new HashMap<>();
        Set<String> filePairs = new HashSet<>();
        // 2. 逐行校验 + 导入
        int rowNo = 0;
        for (MesMdProductBomImportExcelVO importVO : importList) {
            rowNo++;
            String parentCode = StrUtil.trim(importVO.getParentItemCode());
            String componentCode = StrUtil.trim(importVO.getBomItemCode());
            String rowKey = "第" + rowNo + "行 " + StrUtil.blankToDefault(parentCode, "(空父项)")
                    + "→" + StrUtil.blankToDefault(componentCode, "(空子项)");
            // 2.1 基础校验
            if (StrUtil.isBlank(parentCode) || StrUtil.isBlank(componentCode)) {
                respVO.getFailureList().put("第" + rowNo + "行",
                        StrUtil.isBlank(parentCode) && StrUtil.isBlank(componentCode) ? "父项物料编码和子项物料编码都不能为空"
                                : StrUtil.isBlank(parentCode) ? "父项物料编码不能为空" : "子项物料编码不能为空");
                continue;
            }
            if (importVO.getQuantity() == null || importVO.getQuantity().signum() <= 0) {
                respVO.getFailureList().put(rowKey, "单位用量必须大于 0,当前为:"
                        + (importVO.getQuantity() == null ? "空" : importVO.getQuantity().toPlainString()));
                continue;
            }
            // 2.2 父项、子项必须已在 MES 物料中维护
            MesMdItemDO parent = lookupItem(itemCache, parentCode);
            MesMdItemDO component = lookupItem(itemCache, componentCode);
            if (parent == null) {
                respVO.getFailureList().put(rowKey, "父项物料编码[" + parentCode
                        + "]在生产物料管理中不存在,请先维护该物料后再导入");
                continue;
            }
            if (component == null) {
                respVO.getFailureList().put(rowKey, "子项物料编码[" + componentCode
                        + "]在生产物料管理中不存在,请先维护该物料后再导入");
                continue;
            }
            if (parent.getId().equals(component.getId())) {
                respVO.getFailureList().put(rowKey, "父项与子项不能是同一物料[" + parentCode + "]");
                continue;
            }
            String pairKey = pairKey(parent.getId(), component.getId());
            // 2.3 已存在的父子组合:默认跳过,勾选“支持更新”时只更新用量/备注(状态保持原值)
            MesMdProductBomDO existing = existingPairMap.get(pairKey);
            if (existing != null) {
                if (!Boolean.TRUE.equals(updateSupport)) {
                    respVO.getFailureList().put(rowKey, "该父子组合已存在 BOM,本次未更新;"
                            + "如需覆盖用量请勾选“支持更新”后重新导入");
                    continue;
                }
                MesMdProductBomDO updateObj = new MesMdProductBomDO().setId(existing.getId())
                        .setQuantity(importVO.getQuantity())
                        .setRemark(StrUtil.blankToDefault(importVO.getRemark(), existing.getRemark()));
                productBomMapper.updateById(updateObj);
                respVO.getUpdateList().add(parentCode + "→" + componentCode);
                continue;
            }
            // 2.4 文件内重复
            if (filePairs.contains(pairKey)) {
                respVO.getFailureList().put(rowKey, "同一父子组合在文件中重复出现,请合并为一行后重新导入");
                continue;
            }
            // 2.5 闭环检测(基于现有 BOM + 本文件已接受的行)
            if (reaches(graph, component.getId(), parent.getId())) {
                respVO.getFailureList().put(rowKey, "新增后 BOM 将形成循环引用(" + parentCode
                        + "→" + componentCode + "),请调整父子关系后重新导入");
                continue;
            }
            // 2.6 新增
            MesMdProductBomSaveReqVO saveReqVO = new MesMdProductBomSaveReqVO();
            saveReqVO.setItemId(parent.getId());
            saveReqVO.setBomItemId(component.getId());
            saveReqVO.setQuantity(importVO.getQuantity());
            saveReqVO.setStatus(0); // 导入的 BOM 默认启用
            saveReqVO.setRemark(StrUtil.blankToDefault(importVO.getRemark(), null));
            createProductBom(saveReqVO);
            filePairs.add(pairKey);
            graph.computeIfAbsent(parent.getId(), key -> new HashSet<>()).add(component.getId());
            respVO.getCreateList().add(parentCode + "→" + componentCode);
        }
        return respVO;
    }
    private MesMdItemDO lookupItem(Map<String, Optional<MesMdItemDO>> cache, String code) {
        return cache.computeIfAbsent(code,
                key -> Optional.ofNullable(itemService.getItemByCode(key))).orElse(null);
    }
    private static String pairKey(Long itemId, Long bomItemId) {
        return itemId + ":" + bomItemId;
    }
    /** 沿现有(含本次已接受的)父子边从 start 出发,判断能否到达 target */
    private static boolean reaches(Map<Long, Set<Long>> graph, Long start, Long target) {
        Deque<Long> stack = new ArrayDeque<>();
        Set<Long> visited = new HashSet<>();
        stack.push(start);
        while (!stack.isEmpty()) {
            Long node = stack.pop();
            if (!visited.add(node)) {
                continue;
            }
            if (node.equals(target)) {
                return true;
            }
            for (Long next : graph.getOrDefault(node, Set.of())) {
                stack.push(next);
            }
        }
        return false;
    }
}