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