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;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.util.*;
|
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap2;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 产品BOM Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesMdProductBomServiceImpl implements MesMdProductBomService {
|
|
@Resource
|
private MesMdProductBomMapper productBomMapper;
|
|
@Resource
|
@Lazy // 避免循环依赖
|
private MesMdItemService itemService;
|
|
@Override
|
public Long createProductBom(MesMdProductBomSaveReqVO createReqVO) {
|
// 1.1 校验数据
|
validateProductBomSaveData(createReqVO);
|
// 1.2 校验不能形成闭环
|
if (hasBomCycle(createReqVO.getItemId(), createReqVO.getBomItemId())) {
|
throw exception(MD_PRODUCT_BOM_CIRCULAR);
|
}
|
|
// 2. 插入
|
MesMdProductBomDO productBom = BeanUtils.toBean(createReqVO, MesMdProductBomDO.class);
|
productBomMapper.insert(productBom);
|
return productBom.getId();
|
}
|
|
@Override
|
public void updateProductBom(MesMdProductBomSaveReqVO updateReqVO) {
|
// 1.1 校验存在
|
validateProductBomExists(updateReqVO.getId());
|
// 1.2 校验数据
|
validateProductBomSaveData(updateReqVO);
|
|
// 更新
|
MesMdProductBomDO updateObj = BeanUtils.toBean(updateReqVO, MesMdProductBomDO.class);
|
productBomMapper.updateById(updateObj);
|
}
|
|
private void validateProductBomSaveData(MesMdProductBomSaveReqVO reqVO) {
|
// 校验物料产品存在
|
validateItemExists(reqVO.getItemId());
|
// 校验BOM物料存在
|
validateItemExists(reqVO.getBomItemId());
|
// 校验不能自引用
|
if (reqVO.getItemId().equals(reqVO.getBomItemId())) {
|
throw exception(MD_PRODUCT_BOM_SELF_REFERENCE);
|
}
|
}
|
|
@Override
|
public void deleteProductBom(Long id) {
|
// 校验存在
|
validateProductBomExists(id);
|
// 删除
|
productBomMapper.deleteById(id);
|
}
|
|
private void validateProductBomExists(Long id) {
|
if (productBomMapper.selectById(id) == null) {
|
throw exception(MD_PRODUCT_BOM_NOT_EXISTS);
|
}
|
}
|
|
private void validateItemExists(Long itemId) {
|
if (itemService.getItem(itemId) == null) {
|
throw exception(MD_ITEM_NOT_EXISTS);
|
}
|
}
|
|
/**
|
* 检测新增边 (itemId -> bomItemId) 后,BOM 图是否存在闭环
|
*
|
* @param itemId 父物料编号
|
* @param bomItemId BOM子物料编号
|
* @return 是否存在闭环
|
*/
|
private boolean hasBomCycle(Long itemId, Long bomItemId) {
|
// 1.1 获取所有已有的 BOM 记录
|
List<MesMdProductBomDO> allBoms = productBomMapper.selectAll();
|
// 1.2 构建邻接表:parent -> Set<child>
|
Map<Long, Set<Long>> graph = convertMultiMap2(allBoms,
|
MesMdProductBomDO::getItemId, MesMdProductBomDO::getBomItemId);
|
// 1.3 添加待新增的边
|
graph.computeIfAbsent(itemId, k -> new HashSet<>()).add(bomItemId);
|
// 2. DFS 检测环
|
for (Long node : graph.keySet()) {
|
if (CollectionUtils.dfs(node, graph)) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
@Override
|
public MesMdProductBomDO getProductBom(Long id) {
|
return productBomMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesMdProductBomDO> getProductBomPage(MesMdProductBomPageReqVO pageReqVO) {
|
return productBomMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<MesMdProductBomDO> getProductBomListByItemId(Long itemId) {
|
return productBomMapper.selectByItemId(itemId);
|
}
|
|
@Override
|
public List<MesMdProductBomDO> getProductBomListByItemIds(Collection<Long> itemIds) {
|
return productBomMapper.selectByItemIds(itemIds);
|
}
|
|
@Override
|
public void deleteProductBomByItemId(Long itemId) {
|
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;
|
}
|
|
}
|