package cn.iocoder.yudao.module.mes.service.md.item; import cn.hutool.core.util.StrUtil; 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 cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO; import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportModuleAdapter; import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO; import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO; import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportRowReceipt; import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException; import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal; import cn.iocoder.yudao.module.system.service.agent.imports.SystemAgentGenericImportService; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.*; /** * BOM 导入:父/子物料必须已同步到 MES;已存在父子组合跳过,只新建不更新。 */ @Component public class MesBomImportAdapter implements AgentImportModuleAdapter { public static final String MODULE_ID = "mes.bom"; private static final String PERMISSION = "mes:md-item:create"; @Resource private MesMdItemService itemService; @Resource private MesMdProductBomService productBomService; @Resource private MesMdProductBomMapper productBomMapper; @Override public String moduleId() { return MODULE_ID; } @Override public String permission() { return PERMISSION; } @Override public AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request) { // 1. 现有 BOM 图与父子组合索引,用于跳过与环检测 Map> graph = new HashMap<>(); Map pairs = new HashMap<>(); for (MesMdProductBomDO bom : productBomMapper.selectAll()) { graph.computeIfAbsent(bom.getItemId(), key -> new HashSet<>()).add(bom.getBomItemId()); pairs.put(pairKey(bom.getItemId(), bom.getBomItemId()), bom.getId()); } Map> itemCache = new HashMap<>(); Set filePairs = new HashSet<>(); List rows = new ArrayList<>(); List> resolvedPayload = new ArrayList<>(); int create = 0; int skip = 0; int blocked = 0; for (AgentImportPreflightReqVO.Row row : request.getRows()) { List blockers = new ArrayList<>(); List warnings = new ArrayList<>(); String parentCode = value(row, "parentCode"); String componentCode = value(row, "componentCode"); if (parentCode.isEmpty()) { blockers.add(issue("PARENT_CODE_REQUIRED", "parentCode", "父项物料编码不能为空")); } if (componentCode.isEmpty()) { blockers.add(issue("COMPONENT_CODE_REQUIRED", "componentCode", "子项物料编码不能为空")); } BigDecimal quantity = parseQuantity(row.getValues().get("quantity"), blockers); Integer status = parseStatus(row.getValues().get("status"), blockers); MesMdItemDO parent = parentCode.isEmpty() ? null : lookup(itemCache, parentCode); MesMdItemDO component = componentCode.isEmpty() ? null : lookup(itemCache, componentCode); if (!parentCode.isEmpty() && parent == null) { blockers.add(issue("MES_ITEM_NOT_SYNCED", "parentCode", "父项物料编码 " + parentCode + " 在 MES 物料中不存在,请先导入并同步物料")); } if (!componentCode.isEmpty() && component == null) { blockers.add(issue("MES_ITEM_NOT_SYNCED", "componentCode", "子项物料编码 " + componentCode + " 在 MES 物料中不存在,请先导入并同步物料")); } String action; Long targetId = null; if (!blockers.isEmpty()) { action = "BLOCKED"; blocked++; } else { String key = pairKey(parent.getId(), component.getId()); if (parent.getId().equals(component.getId())) { blockers.add(issue("BOM_SELF_REFERENCE", "componentCode", "父项与子项不能是同一物料")); action = "BLOCKED"; blocked++; } else if (pairs.containsKey(key)) { action = "SKIP"; targetId = pairs.get(key); skip++; warnings.add(issue("BOM_EXISTS", "componentCode", "该父子物料组合已存在 BOM,已跳过且不会更新用量")); } else if (filePairs.contains(key)) { blockers.add(issue("DUPLICATE_IN_FILE", "componentCode", "文件内父项与子项组合重复,请核实用量版本")); action = "BLOCKED"; blocked++; } else if (reaches(graph, component.getId(), parent.getId())) { blockers.add(issue("BOM_CYCLE", "componentCode", "新增后 BOM 将形成循环引用,请调整父子关系")); action = "BLOCKED"; blocked++; } else { action = "CREATE"; create++; filePairs.add(key); graph.computeIfAbsent(parent.getId(), item -> new HashSet<>()).add(component.getId()); } } Map resolved = new LinkedHashMap<>(); resolved.put("candidate_id", row.getCandidateId()); resolved.put("parent_item_id", parent == null ? null : parent.getId()); resolved.put("component_item_id", component == null ? null : component.getId()); resolved.put("quantity", quantity); resolved.put("status", status); resolved.put("action", action); resolved.put("target_id", targetId); resolvedPayload.add(resolved); Map resolvedValues = new LinkedHashMap<>(); resolvedValues.put("parentCode", parentCode); resolvedValues.put("componentCode", componentCode); resolvedValues.put("quantity", quantity); resolvedValues.put("status", status); rows.add(AgentImportPreflightRespVO.Row.builder() .candidateId(row.getCandidateId()) .rowNo(row.getRowNo()) .action(action) .resolved(resolvedValues) .blockers(blockers) .warnings(warnings) .targetId(targetId) .build()); } int total = rows.size(); return AgentImportPreflightRespVO.builder() .moduleId(MODULE_ID) .preflightHash(SystemAgentGenericImportService.preflightHash(MODULE_ID, request, resolvedPayload)) .source(AgentImportPreflightRespVO.Source.builder() .attachmentId(request.getAttachmentId()) .sessionId(request.getSessionId()) .sourceSha256(request.getSourceSha256()) .proposalRevision(request.getProposalRevision()) .build()) .summary(AgentImportPreflightRespVO.Summary.builder() .total(total).create(create).skip(skip).blocked(blocked).build()) .canCommit(blocked == 0) .sideEffects(List.of("创建 BOM 父子用料关系", "不更新已存在的 BOM")) .rows(rows) .blockingItems(List.of()) .build(); } @Override public AgentImportRowReceipt commitRow(AgentPrincipal principal, AgentImportCommitReqVO request, AgentImportPreflightRespVO.Row planned, AgentImportPreflightReqVO.Row row) { String parentCode = value(row, "parentCode"); String componentCode = value(row, "componentCode"); MesMdItemDO parent = itemService.getItemByCode(parentCode); MesMdItemDO component = itemService.getItemByCode(componentCode); if (parent == null || component == null) { throw new AgentBridgeException(HttpStatus.CONFLICT, "MES_ITEM_NOT_SYNCED", "物料在 MES 中已不存在,请重新同步后重试", true, null); } BigDecimal quantity = new BigDecimal(value(row, "quantity")); if (quantity.signum() <= 0) { throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "INVALID_QUANTITY", "用量必须大于 0", false, null); } MesMdProductBomSaveReqVO saveReqVO = new MesMdProductBomSaveReqVO(); saveReqVO.setItemId(parent.getId()); saveReqVO.setBomItemId(component.getId()); saveReqVO.setQuantity(quantity); saveReqVO.setStatus(parseStatus(row.getValues().get("status"), null)); saveReqVO.setRemark(StrUtil.blankToDefault(row.getValues().get("remark"), null)); Long id = productBomService.createProductBom(saveReqVO); return AgentImportRowReceipt.created(id, parentCode, componentCode); } private MesMdItemDO lookup(Map> cache, String code) { return cache.computeIfAbsent(code, key -> Optional.ofNullable(itemService.getItemByCode(key))).orElse(null); } /** 沿已有(含本次已接受的)父子边从 start 出发,判断能否到达 target。 */ private static boolean reaches(Map> graph, Long start, Long target) { Deque stack = new ArrayDeque<>(); Set 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; } private static String pairKey(Long parentId, Long componentId) { return parentId + ":" + componentId; } private static AgentImportPreflightRespVO.Issue issue(String code, String field, String message) { return AgentImportPreflightRespVO.Issue.builder() .code(code).field(field).message(message).retryable(false).build(); } private static String value(AgentImportPreflightReqVO.Row row, String key) { String raw = row.getValues() == null ? null : row.getValues().get(key); return raw == null ? "" : raw.trim(); } private static BigDecimal parseQuantity(String raw, List blockers) { if (StrUtil.isBlank(raw)) { if (blockers != null) { blockers.add(issue("INVALID_QUANTITY", "quantity", "用量必须是大于 0 的数字")); } return null; } try { BigDecimal quantity = new BigDecimal(raw.trim()); if (quantity.signum() <= 0) { if (blockers != null) { blockers.add(issue("INVALID_QUANTITY", "quantity", "用量必须大于 0")); } return quantity; } return quantity; } catch (NumberFormatException ex) { if (blockers != null) { blockers.add(issue("INVALID_QUANTITY", "quantity", "用量必须是大于 0 的数字")); } return null; } } private static Integer parseStatus(String raw, List blockers) { if (StrUtil.isBlank(raw)) { return 0; } String value = raw.trim(); if ("0".equals(value) || "1".equals(value)) { return Integer.valueOf(value); } if (blockers != null) { blockers.add(issue("INVALID_STATUS", "status", "状态只能是 0(启用)或 1(禁用)")); } return 0; } }