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<Long, Set<Long>> graph = new HashMap<>();
|
Map<String, Long> 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<String, Optional<MesMdItemDO>> itemCache = new HashMap<>();
|
Set<String> filePairs = new HashSet<>();
|
List<AgentImportPreflightRespVO.Row> rows = new ArrayList<>();
|
List<Map<String, Object>> resolvedPayload = new ArrayList<>();
|
int create = 0;
|
int skip = 0;
|
int blocked = 0;
|
|
for (AgentImportPreflightReqVO.Row row : request.getRows()) {
|
List<AgentImportPreflightRespVO.Issue> blockers = new ArrayList<>();
|
List<AgentImportPreflightRespVO.Issue> 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<String, Object> 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<String, Object> 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<String, Optional<MesMdItemDO>> cache, String code) {
|
return cache.computeIfAbsent(code,
|
key -> Optional.ofNullable(itemService.getItemByCode(key))).orElse(null);
|
}
|
|
/** 沿已有(含本次已接受的)父子边从 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;
|
}
|
|
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<AgentImportPreflightRespVO.Issue> 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<AgentImportPreflightRespVO.Issue> 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;
|
}
|
}
|