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<ProductModel> allProductModels = productModelMapper.selectList(
|
Wrappers.<ProductModel>lambdaQuery()
|
.isNotNull(ProductModel::getProductId)
|
.orderByAsc(ProductModel::getProductId)
|
.orderByAsc(ProductModel::getId)
|
);
|
if (CollectionUtils.isEmpty(allProductModels)) {
|
throw new IllegalStateException("未查询到任何产品规格");
|
}
|
|
Map<Long, List<ProductModel>> productModelGroups = allProductModels.stream()
|
.collect(Collectors.groupingBy(ProductModel::getProductId, LinkedHashMap::new, Collectors.toList()));
|
|
List<CopySummary> summaries = new ArrayList<>();
|
for (Map.Entry<Long, List<ProductModel>> entry : productModelGroups.entrySet()) {
|
CopySummary summary = copyBomStructure(entry.getKey(), entry.getValue());
|
summaries.add(summary);
|
printProductSummary(summary);
|
}
|
|
printBatchSummary(summaries);
|
}
|
|
private CopySummary copyBomStructure(Long productId, List<ProductModel> productModels) {
|
if (CollectionUtils.isEmpty(productModels)) {
|
return CopySummary.skipped(productId, 0, 0, "未查询到产品大类对应的规格");
|
}
|
|
Map<Long, ProductModel> productModelMap = productModels.stream()
|
.collect(Collectors.toMap(ProductModel::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
List<Long> productModelIds = new ArrayList<>(productModelMap.keySet());
|
|
List<ProductBom> bomList = productBomMapper.selectList(
|
Wrappers.<ProductBom>lambdaQuery()
|
.in(ProductBom::getProductModelId, productModelIds)
|
.orderByAsc(ProductBom::getId)
|
);
|
if (CollectionUtils.isEmpty(bomList)) {
|
return CopySummary.skipped(productId, productModels.size(), 0, "未查询到产品大类对应的BOM");
|
}
|
|
Map<Integer, List<ProductStructure>> structureMap = loadStructureMap(bomList);
|
ProductBom templateBom = findTemplateBomOrNull(bomList, structureMap);
|
if (templateBom == null) {
|
return CopySummary.skipped(productId, productModels.size(), bomList.size(), "当前产品大类下没有可作为模板的完整BOM结构");
|
}
|
|
List<ProductStructure> templateStructures = structureMap.get(templateBom.getId());
|
ProductStructure templateRoot = requireSingleRoot(templateBom, templateStructures);
|
List<ProductStructure> 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<ProductStructure> 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<Integer, List<ProductStructure>> loadStructureMap(List<ProductBom> bomList) {
|
List<Integer> bomIds = bomList.stream()
|
.map(ProductBom::getId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toList());
|
if (bomIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
|
List<ProductStructure> structures = productStructureMapper.selectList(
|
Wrappers.<ProductStructure>lambdaQuery()
|
.in(ProductStructure::getBomId, bomIds)
|
.orderByAsc(ProductStructure::getId)
|
);
|
|
Map<Integer, List<ProductStructure>> structureMap = new HashMap<>();
|
for (ProductStructure structure : structures) {
|
structureMap.computeIfAbsent(structure.getBomId(), key -> new ArrayList<>()).add(structure);
|
}
|
return structureMap;
|
}
|
|
private ProductBom findTemplateBomOrNull(List<ProductBom> bomList, Map<Integer, List<ProductStructure>> 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<ProductStructure> structures) {
|
if (structures == null) {
|
return 0;
|
}
|
int count = 0;
|
for (ProductStructure structure : structures) {
|
if (!isRoot(structure)) {
|
count++;
|
}
|
}
|
return count;
|
}
|
|
private boolean hasChildNodes(List<ProductStructure> structures) {
|
return childNodeCount(structures) > 0;
|
}
|
|
private ProductStructure requireSingleRoot(ProductBom bom, List<ProductStructure> structures) {
|
List<ProductStructure> 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<ProductStructure> targetStructures) {
|
List<ProductStructure> 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<ProductStructure> templateChildren,
|
ProductBom targetBom,
|
ProductStructure targetRoot) {
|
Map<Long, Long> idMapping = new HashMap<>();
|
idMapping.put(templateRoot.getId(), targetRoot.getId());
|
|
List<NodePair> 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<ProductStructure> findRoots(List<ProductStructure> 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<CopySummary> 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;
|
}
|
}
|
}
|