package com.ruoyi.production; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.ruoyi.basic.mapper.ProductModelMapper; import com.ruoyi.basic.pojo.ProductModel; import com.ruoyi.production.mapper.ProductBomMapper; import com.ruoyi.production.mapper.ProductStructureMapper; import com.ruoyi.production.pojo.ProductBom; import com.ruoyi.production.pojo.ProductStructure; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; /** * BOM结构补齐测试。 */ @SpringBootTest public class BomStructureCopyTest { @Autowired private ProductModelMapper productModelMapper; @Autowired private ProductBomMapper productBomMapper; @Autowired private ProductStructureMapper productStructureMapper; @Test @Rollback(false) @Transactional(rollbackFor = Exception.class) public void copyAllProductBomStructureByProductId() { List allProductModels = productModelMapper.selectList( Wrappers.lambdaQuery() .isNotNull(ProductModel::getProductId) .orderByAsc(ProductModel::getProductId) .orderByAsc(ProductModel::getId) ); if (CollectionUtils.isEmpty(allProductModels)) { throw new IllegalStateException("未查询到任何产品规格"); } Map> productModelGroups = allProductModels.stream() .collect(Collectors.groupingBy(ProductModel::getProductId, LinkedHashMap::new, Collectors.toList())); List summaries = new ArrayList<>(); for (Map.Entry> entry : productModelGroups.entrySet()) { CopySummary summary = copyBomStructure(entry.getKey(), entry.getValue()); summaries.add(summary); printProductSummary(summary); } printBatchSummary(summaries); } private CopySummary copyBomStructure(Long productId, List productModels) { if (CollectionUtils.isEmpty(productModels)) { return CopySummary.skipped(productId, 0, 0, "未查询到产品大类对应的规格"); } Map productModelMap = productModels.stream() .collect(Collectors.toMap(ProductModel::getId, item -> item, (left, right) -> left, LinkedHashMap::new)); List productModelIds = new ArrayList<>(productModelMap.keySet()); List bomList = productBomMapper.selectList( Wrappers.lambdaQuery() .in(ProductBom::getProductModelId, productModelIds) .orderByAsc(ProductBom::getId) ); if (CollectionUtils.isEmpty(bomList)) { return CopySummary.skipped(productId, productModels.size(), 0, "未查询到产品大类对应的BOM"); } Map> structureMap = loadStructureMap(bomList); ProductBom templateBom = findTemplateBomOrNull(bomList, structureMap); if (templateBom == null) { return CopySummary.skipped(productId, productModels.size(), bomList.size(), "当前产品大类下没有可作为模板的完整BOM结构"); } List templateStructures = structureMap.get(templateBom.getId()); ProductStructure templateRoot = requireSingleRoot(templateBom, templateStructures); List templateChildren = templateStructures.stream() .filter(item -> !isRoot(item)) .sorted(Comparator.comparing(ProductStructure::getId)) .collect(Collectors.toList()); if (templateChildren.isEmpty()) { throw new IllegalStateException("模板BOM没有可复制的子结构,bomId=" + templateBom.getId()); } int copiedBomCount = 0; int skippedBomCount = 0; int insertedStructureCount = 0; for (ProductBom targetBom : bomList) { if (Objects.equals(templateBom.getId(), targetBom.getId())) { skippedBomCount++; continue; } List targetStructures = structureMap.getOrDefault(targetBom.getId(), Collections.emptyList()); if (hasChildNodes(targetStructures)) { skippedBomCount++; continue; } ProductModel targetModel = productModelMap.get(targetBom.getProductModelId()); if (targetModel == null) { throw new IllegalStateException("BOM关联的规格不属于当前产品大类,bomId=" + targetBom.getId() + ",productModelId=" + targetBom.getProductModelId()); } RootNode targetRoot = ensureTargetRoot(targetBom, targetModel, templateRoot, targetStructures); int copiedChildren = copyTemplateChildren(templateBom, templateRoot, templateChildren, targetBom, targetRoot.getNode()); copiedBomCount++; insertedStructureCount += copiedChildren + (targetRoot.isInserted() ? 1 : 0); } return CopySummary.copied( productId, productModels.size(), templateBom.getId(), templateBom.getBomNo(), bomList.size(), copiedBomCount, skippedBomCount, insertedStructureCount ); } private Map> loadStructureMap(List bomList) { List bomIds = bomList.stream() .map(ProductBom::getId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (bomIds.isEmpty()) { return Collections.emptyMap(); } List structures = productStructureMapper.selectList( Wrappers.lambdaQuery() .in(ProductStructure::getBomId, bomIds) .orderByAsc(ProductStructure::getId) ); Map> structureMap = new HashMap<>(); for (ProductStructure structure : structures) { structureMap.computeIfAbsent(structure.getBomId(), key -> new ArrayList<>()).add(structure); } return structureMap; } private ProductBom findTemplateBomOrNull(List bomList, Map> structureMap) { return bomList.stream() .filter(item -> hasChildNodes(structureMap.getOrDefault(item.getId(), Collections.emptyList()))) .max(Comparator.comparingInt(item -> childNodeCount(structureMap.getOrDefault(item.getId(), Collections.emptyList())))) .orElse(null); } private int childNodeCount(List structures) { if (structures == null) { return 0; } int count = 0; for (ProductStructure structure : structures) { if (!isRoot(structure)) { count++; } } return count; } private boolean hasChildNodes(List structures) { return childNodeCount(structures) > 0; } private ProductStructure requireSingleRoot(ProductBom bom, List structures) { List roots = findRoots(structures); if (roots.size() != 1) { throw new IllegalStateException("BOM根节点数量异常,bomId=" + bom.getId() + ",rootCount=" + roots.size()); } return roots.get(0); } private RootNode ensureTargetRoot(ProductBom targetBom, ProductModel targetModel, ProductStructure templateRoot, List targetStructures) { List roots = findRoots(targetStructures); if (roots.size() > 1) { throw new IllegalStateException("目标BOM根节点数量异常,bomId=" + targetBom.getId() + ",rootCount=" + roots.size()); } if (roots.size() == 1) { return new RootNode(roots.get(0), false); } ProductStructure targetRoot = new ProductStructure(); targetRoot.setParentId(null); targetRoot.setBomId(targetBom.getId()); targetRoot.setProductModelId(targetBom.getProductModelId()); targetRoot.setProcessId(templateRoot.getProcessId()); targetRoot.setUnitQuantity(templateRoot.getUnitQuantity() == null ? BigDecimal.ONE : templateRoot.getUnitQuantity()); targetRoot.setDemandedQuantity(templateRoot.getDemandedQuantity()); targetRoot.setUnit(targetModel.getUnit()); targetRoot.setTenantId(firstNotNull(targetBom.getTenantId(), targetModel.getTenantId(), templateRoot.getTenantId())); productStructureMapper.insert(targetRoot); return new RootNode(targetRoot, true); } private int copyTemplateChildren(ProductBom templateBom, ProductStructure templateRoot, List templateChildren, ProductBom targetBom, ProductStructure targetRoot) { Map idMapping = new HashMap<>(); idMapping.put(templateRoot.getId(), targetRoot.getId()); List insertedNodes = new ArrayList<>(); for (ProductStructure sourceNode : templateChildren) { ProductStructure copiedNode = new ProductStructure(); copiedNode.setParentId(null); copiedNode.setProductModelId(sourceNode.getProductModelId()); copiedNode.setProcessId(sourceNode.getProcessId()); copiedNode.setUnitQuantity(sourceNode.getUnitQuantity()); copiedNode.setDemandedQuantity(sourceNode.getDemandedQuantity()); copiedNode.setUnit(sourceNode.getUnit()); copiedNode.setTenantId(firstNotNull(targetBom.getTenantId(), sourceNode.getTenantId())); copiedNode.setBomId(targetBom.getId()); productStructureMapper.insert(copiedNode); idMapping.put(sourceNode.getId(), copiedNode.getId()); insertedNodes.add(new NodePair(sourceNode, copiedNode)); } for (NodePair insertedNode : insertedNodes) { Long sourceParentId = insertedNode.getSourceNode().getParentId(); Long newParentId = idMapping.get(sourceParentId); if (newParentId == null) { throw new IllegalStateException("模板BOM存在无法映射的父节点,templateBomId=" + templateBom.getId() + ",sourceNodeId=" + insertedNode.getSourceNode().getId() + ",sourceParentId=" + sourceParentId); } ProductStructure copiedNode = insertedNode.getCopiedNode(); copiedNode.setParentId(newParentId); productStructureMapper.updateById(copiedNode); } return insertedNodes.size(); } private List findRoots(List structures) { if (structures == null) { return Collections.emptyList(); } return structures.stream() .filter(this::isRoot) .collect(Collectors.toList()); } private boolean isRoot(ProductStructure structure) { return structure.getParentId() == null || Long.valueOf(0L).equals(structure.getParentId()); } private Long firstNotNull(Long first, Long second) { return first != null ? first : second; } private Long firstNotNull(Long first, Long second, Long third) { if (first != null) { return first; } return second != null ? second : third; } private void printProductSummary(CopySummary summary) { if (summary.isProductSkipped()) { System.out.printf( "跳过产品分类:productId=%s,规格数=%s,BOM总数=%s,原因=%s%n", summary.getProductId(), summary.getProductModelCount(), summary.getTotalBomCount(), summary.getSkipReason() ); return; } System.out.printf( "处理产品分类:productId=%s,模板BOM=%s(%s),规格数=%s,BOM总数=%s,已补齐BOM=%s,跳过BOM=%s,新增结构节点=%s%n", summary.getProductId(), summary.getTemplateBomNo(), summary.getTemplateBomId(), summary.getProductModelCount(), summary.getTotalBomCount(), summary.getCopiedBomCount(), summary.getSkippedBomCount(), summary.getInsertedStructureCount() ); } private void printBatchSummary(List summaries) { int productCount = summaries.size(); int skippedProductCount = 0; int totalBomCount = 0; int copiedBomCount = 0; int skippedBomCount = 0; int insertedStructureCount = 0; for (CopySummary summary : summaries) { if (summary.isProductSkipped()) { skippedProductCount++; } totalBomCount += summary.getTotalBomCount(); copiedBomCount += summary.getCopiedBomCount(); skippedBomCount += summary.getSkippedBomCount(); insertedStructureCount += summary.getInsertedStructureCount(); } System.out.printf( "BOM结构批量复制完成:产品分类数=%s,已处理分类=%s,跳过分类=%s,BOM总数=%s,已补齐BOM=%s,跳过BOM=%s,新增结构节点=%s%n", productCount, productCount - skippedProductCount, skippedProductCount, totalBomCount, copiedBomCount, skippedBomCount, insertedStructureCount ); } private static class RootNode { private final ProductStructure node; private final boolean inserted; private RootNode(ProductStructure node, boolean inserted) { this.node = node; this.inserted = inserted; } private ProductStructure getNode() { return node; } private boolean isInserted() { return inserted; } } private static class NodePair { private final ProductStructure sourceNode; private final ProductStructure copiedNode; private NodePair(ProductStructure sourceNode, ProductStructure copiedNode) { this.sourceNode = sourceNode; this.copiedNode = copiedNode; } private ProductStructure getSourceNode() { return sourceNode; } private ProductStructure getCopiedNode() { return copiedNode; } } private static class CopySummary { private final Long productId; private final int productModelCount; private final Integer templateBomId; private final String templateBomNo; private final int totalBomCount; private final int copiedBomCount; private final int skippedBomCount; private final int insertedStructureCount; private final String skipReason; private CopySummary(Long productId, int productModelCount, Integer templateBomId, String templateBomNo, int totalBomCount, int copiedBomCount, int skippedBomCount, int insertedStructureCount, String skipReason) { this.productId = productId; this.productModelCount = productModelCount; this.templateBomId = templateBomId; this.templateBomNo = templateBomNo; this.totalBomCount = totalBomCount; this.copiedBomCount = copiedBomCount; this.skippedBomCount = skippedBomCount; this.insertedStructureCount = insertedStructureCount; this.skipReason = skipReason; } private static CopySummary copied(Long productId, int productModelCount, Integer templateBomId, String templateBomNo, int totalBomCount, int copiedBomCount, int skippedBomCount, int insertedStructureCount) { return new CopySummary( productId, productModelCount, templateBomId, templateBomNo, totalBomCount, copiedBomCount, skippedBomCount, insertedStructureCount, null ); } private static CopySummary skipped(Long productId, int productModelCount, int totalBomCount, String skipReason) { return new CopySummary( productId, productModelCount, null, null, totalBomCount, 0, totalBomCount, 0, skipReason ); } private Long getProductId() { return productId; } private int getProductModelCount() { return productModelCount; } private Integer getTemplateBomId() { return templateBomId; } private String getTemplateBomNo() { return templateBomNo; } private int getTotalBomCount() { return totalBomCount; } private int getCopiedBomCount() { return copiedBomCount; } private int getSkippedBomCount() { return skippedBomCount; } private int getInsertedStructureCount() { return insertedStructureCount; } private String getSkipReason() { return skipReason; } private boolean isProductSkipped() { return skipReason != null; } } }