gongchunyi
2026-08-02 49e79814c7680f259b3be26d8fe7f5486a8cc33d
暂存
已添加2个文件
839 ■■■■■ 文件已修改
src/test/java/com/ruoyi/production/BomStructureCopyTest.java 487 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/production/ProcessRouteItemCopyTest.java 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/production/BomStructureCopyTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,487 @@
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;
        }
    }
}
src/test/java/com/ruoyi/production/ProcessRouteItemCopyTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,352 @@
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.ProcessRouteItemMapper;
import com.ruoyi.production.mapper.ProcessRouteMapper;
import com.ruoyi.production.pojo.ProcessRoute;
import com.ruoyi.production.pojo.ProcessRouteItem;
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.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;
/**
 * å·¥è‰ºè·¯çº¿é¡¹ç›®è¡¥é½æµ‹è¯•。
 */
@SpringBootTest
public class ProcessRouteItemCopyTest {
    @Autowired
    private ProductModelMapper productModelMapper;
    @Autowired
    private ProcessRouteMapper processRouteMapper;
    @Autowired
    private ProcessRouteItemMapper processRouteItemMapper;
    @Test
    @Rollback(false)
    @Transactional(rollbackFor = Exception.class)
    public void copyAllProcessRouteItemsByProductId() {
        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 = copyProcessRouteItems(entry.getKey(), entry.getValue());
            summaries.add(summary);
            printProductSummary(summary);
        }
        printBatchSummary(summaries);
    }
    private CopySummary copyProcessRouteItems(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<ProcessRoute> routeList = processRouteMapper.selectList(
                Wrappers.<ProcessRoute>lambdaQuery()
                        .in(ProcessRoute::getProductModelId, productModelIds)
                        .orderByAsc(ProcessRoute::getId)
        );
        if (CollectionUtils.isEmpty(routeList)) {
            return CopySummary.skipped(productId, productModels.size(), 0, "未查询到产品大类对应的工艺路线");
        }
        Map<Long, List<ProcessRouteItem>> routeItemMap = loadRouteItemMap(routeList);
        ProcessRoute templateRoute = findTemplateRouteOrNull(routeList, routeItemMap);
        if (templateRoute == null) {
            return CopySummary.skipped(productId, productModels.size(), routeList.size(), "当前产品大类下没有可作为模板的完整路线项目");
        }
        List<ProcessRouteItem> templateItems = routeItemMap.getOrDefault(templateRoute.getId(), Collections.emptyList())
                .stream()
                .sorted(Comparator
                        .comparing(ProcessRouteItem::getDragSort, Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(ProcessRouteItem::getId))
                .collect(Collectors.toList());
        int copiedRouteCount = 0;
        int skippedRouteCount = 0;
        int insertedRouteItemCount = 0;
        for (ProcessRoute targetRoute : routeList) {
            if (Objects.equals(templateRoute.getId(), targetRoute.getId())) {
                skippedRouteCount++;
                continue;
            }
            List<ProcessRouteItem> targetItems = routeItemMap.getOrDefault(targetRoute.getId(), Collections.emptyList());
            if (!targetItems.isEmpty()) {
                skippedRouteCount++;
                continue;
            }
            int copiedItems = copyTemplateItems(templateItems, targetRoute);
            copiedRouteCount++;
            insertedRouteItemCount += copiedItems;
        }
        return CopySummary.copied(
                productId,
                productModels.size(),
                templateRoute.getId(),
                templateRoute.getProcessRouteCode(),
                routeList.size(),
                copiedRouteCount,
                skippedRouteCount,
                insertedRouteItemCount
        );
    }
    private Map<Long, List<ProcessRouteItem>> loadRouteItemMap(List<ProcessRoute> routeList) {
        List<Long> routeIds = routeList.stream()
                .map(ProcessRoute::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (routeIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<ProcessRouteItem> routeItems = processRouteItemMapper.selectList(
                Wrappers.<ProcessRouteItem>lambdaQuery()
                        .in(ProcessRouteItem::getRouteId, routeIds)
                        .orderByAsc(ProcessRouteItem::getRouteId)
                        .orderByAsc(ProcessRouteItem::getDragSort)
                        .orderByAsc(ProcessRouteItem::getId)
        );
        Map<Long, List<ProcessRouteItem>> routeItemMap = new HashMap<>();
        for (ProcessRouteItem routeItem : routeItems) {
            routeItemMap.computeIfAbsent(routeItem.getRouteId(), key -> new ArrayList<>()).add(routeItem);
        }
        return routeItemMap;
    }
    private ProcessRoute findTemplateRouteOrNull(List<ProcessRoute> routeList,
                                                 Map<Long, List<ProcessRouteItem>> routeItemMap) {
        return routeList.stream()
                .filter(item -> routeItemCount(routeItemMap.getOrDefault(item.getId(), Collections.emptyList())) > 0)
                .max(Comparator.comparingInt(item -> routeItemCount(routeItemMap.getOrDefault(item.getId(), Collections.emptyList()))))
                .orElse(null);
    }
    private int routeItemCount(List<ProcessRouteItem> routeItems) {
        return routeItems == null ? 0 : routeItems.size();
    }
    private int copyTemplateItems(List<ProcessRouteItem> templateItems, ProcessRoute targetRoute) {
        int insertedCount = 0;
        for (int i = 0; i < templateItems.size(); i++) {
            ProcessRouteItem sourceItem = templateItems.get(i);
            ProcessRouteItem copiedItem = new ProcessRouteItem();
            copiedItem.setRouteId(targetRoute.getId());
            copiedItem.setProductModelId(sourceItem.getProductModelId());
            copiedItem.setProcessId(sourceItem.getProcessId());
            copiedItem.setTenantId(firstNotNull(targetRoute.getTenantId(), sourceItem.getTenantId()));
            copiedItem.setDragSort(sourceItem.getDragSort() == null ? i + 1 : sourceItem.getDragSort());
            copiedItem.setIsQuality(sourceItem.getIsQuality());
            processRouteItemMapper.insert(copiedItem);
            insertedCount++;
        }
        return insertedCount;
    }
    private Long firstNotNull(Long first, Long second) {
        return first != null ? first : second;
    }
    private void printProductSummary(CopySummary summary) {
        if (summary.isProductSkipped()) {
            System.out.printf(
                    "跳过产品分类:productId=%s,规格数=%s,工艺路线总数=%s,原因=%s%n",
                    summary.getProductId(),
                    summary.getProductModelCount(),
                    summary.getTotalRouteCount(),
                    summary.getSkipReason()
            );
            return;
        }
        System.out.printf(
                "处理产品分类:productId=%s,模板工艺路线=%s(%s),规格数=%s,工艺路线总数=%s,已补齐路线=%s,跳过路线=%s,新增路线项目=%s%n",
                summary.getProductId(),
                summary.getTemplateRouteCode(),
                summary.getTemplateRouteId(),
                summary.getProductModelCount(),
                summary.getTotalRouteCount(),
                summary.getCopiedRouteCount(),
                summary.getSkippedRouteCount(),
                summary.getInsertedRouteItemCount()
        );
    }
    private void printBatchSummary(List<CopySummary> summaries) {
        int productCount = summaries.size();
        int skippedProductCount = 0;
        int totalRouteCount = 0;
        int copiedRouteCount = 0;
        int skippedRouteCount = 0;
        int insertedRouteItemCount = 0;
        for (CopySummary summary : summaries) {
            if (summary.isProductSkipped()) {
                skippedProductCount++;
            }
            totalRouteCount += summary.getTotalRouteCount();
            copiedRouteCount += summary.getCopiedRouteCount();
            skippedRouteCount += summary.getSkippedRouteCount();
            insertedRouteItemCount += summary.getInsertedRouteItemCount();
        }
        System.out.printf(
                "工艺路线项目批量复制完成:产品分类数=%s,已处理分类=%s,跳过分类=%s,工艺路线总数=%s,已补齐路线=%s,跳过路线=%s,新增路线项目=%s%n",
                productCount,
                productCount - skippedProductCount,
                skippedProductCount,
                totalRouteCount,
                copiedRouteCount,
                skippedRouteCount,
                insertedRouteItemCount
        );
    }
    private static class CopySummary {
        private final Long productId;
        private final int productModelCount;
        private final Long templateRouteId;
        private final String templateRouteCode;
        private final int totalRouteCount;
        private final int copiedRouteCount;
        private final int skippedRouteCount;
        private final int insertedRouteItemCount;
        private final String skipReason;
        private CopySummary(Long productId,
                            int productModelCount,
                            Long templateRouteId,
                            String templateRouteCode,
                            int totalRouteCount,
                            int copiedRouteCount,
                            int skippedRouteCount,
                            int insertedRouteItemCount,
                            String skipReason) {
            this.productId = productId;
            this.productModelCount = productModelCount;
            this.templateRouteId = templateRouteId;
            this.templateRouteCode = templateRouteCode;
            this.totalRouteCount = totalRouteCount;
            this.copiedRouteCount = copiedRouteCount;
            this.skippedRouteCount = skippedRouteCount;
            this.insertedRouteItemCount = insertedRouteItemCount;
            this.skipReason = skipReason;
        }
        private static CopySummary copied(Long productId,
                                          int productModelCount,
                                          Long templateRouteId,
                                          String templateRouteCode,
                                          int totalRouteCount,
                                          int copiedRouteCount,
                                          int skippedRouteCount,
                                          int insertedRouteItemCount) {
            return new CopySummary(
                    productId,
                    productModelCount,
                    templateRouteId,
                    templateRouteCode,
                    totalRouteCount,
                    copiedRouteCount,
                    skippedRouteCount,
                    insertedRouteItemCount,
                    null
            );
        }
        private static CopySummary skipped(Long productId, int productModelCount, int totalRouteCount, String skipReason) {
            return new CopySummary(
                    productId,
                    productModelCount,
                    null,
                    null,
                    totalRouteCount,
                    0,
                    totalRouteCount,
                    0,
                    skipReason
            );
        }
        private Long getProductId() {
            return productId;
        }
        private int getProductModelCount() {
            return productModelCount;
        }
        private Long getTemplateRouteId() {
            return templateRouteId;
        }
        private String getTemplateRouteCode() {
            return templateRouteCode;
        }
        private int getTotalRouteCount() {
            return totalRouteCount;
        }
        private int getCopiedRouteCount() {
            return copiedRouteCount;
        }
        private int getSkippedRouteCount() {
            return skippedRouteCount;
        }
        private int getInsertedRouteItemCount() {
            return insertedRouteItemCount;
        }
        private String getSkipReason() {
            return skipReason;
        }
        private boolean isProductSkipped() {
            return skipReason != null;
        }
    }
}