From 49e79814c7680f259b3be26d8fe7f5486a8cc33d Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期日, 02 八月 2026 23:02:21 +0800
Subject: [PATCH] 暂存

---
 src/test/java/com/ruoyi/production/ProcessRouteItemCopyTest.java |  352 +++++++++++++++++++++++++
 src/test/java/com/ruoyi/production/BomStructureCopyTest.java     |  487 ++++++++++++++++++++++++++++++++++
 2 files changed, 839 insertions(+), 0 deletions(-)

diff --git a/src/test/java/com/ruoyi/production/BomStructureCopyTest.java b/src/test/java/com/ruoyi/production/BomStructureCopyTest.java
new file mode 100644
index 0000000..332b9e0
--- /dev/null
+++ b/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, "鏈煡璇㈠埌浜у搧澶х被瀵瑰簲鐨凚OM");
+        }
+
+        Map<Integer, List<ProductStructure>> structureMap = loadStructureMap(bomList);
+        ProductBom templateBom = findTemplateBomOrNull(bomList, structureMap);
+        if (templateBom == null) {
+            return CopySummary.skipped(productId, productModels.size(), bomList.size(), "褰撳墠浜у搧澶х被涓嬫病鏈夊彲浣滀负妯℃澘鐨勫畬鏁碆OM缁撴瀯");
+        }
+
+        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鍏宠仈鐨勮鏍间笉灞炰簬褰撳墠浜у搧澶х被锛宐omId=" + targetBom.getId()
+                        + "锛宲roductModelId=" + 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() + "锛宺ootCount=" + 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() + "锛宺ootCount=" + 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瀛樺湪鏃犳硶鏄犲皠鐨勭埗鑺傜偣锛宼emplateBomId=" + templateBom.getId()
+                        + "锛宻ourceNodeId=" + insertedNode.getSourceNode().getId()
+                        + "锛宻ourceParentId=" + 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(
+                    "璺宠繃浜у搧鍒嗙被锛歱roductId=%s锛岃鏍兼暟=%s锛孊OM鎬绘暟=%s锛屽師鍥�=%s%n",
+                    summary.getProductId(),
+                    summary.getProductModelCount(),
+                    summary.getTotalBomCount(),
+                    summary.getSkipReason()
+            );
+            return;
+        }
+
+        System.out.printf(
+                "澶勭悊浜у搧鍒嗙被锛歱roductId=%s锛屾ā鏉緽OM=%s(%s)锛岃鏍兼暟=%s锛孊OM鎬绘暟=%s锛屽凡琛ラ綈BOM=%s锛岃烦杩嘊OM=%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锛孊OM鎬绘暟=%s锛屽凡琛ラ綈BOM=%s锛岃烦杩嘊OM=%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;
+        }
+    }
+}
diff --git a/src/test/java/com/ruoyi/production/ProcessRouteItemCopyTest.java b/src/test/java/com/ruoyi/production/ProcessRouteItemCopyTest.java
new file mode 100644
index 0000000..bedfc31
--- /dev/null
+++ b/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(
+                    "璺宠繃浜у搧鍒嗙被锛歱roductId=%s锛岃鏍兼暟=%s锛屽伐鑹鸿矾绾挎�绘暟=%s锛屽師鍥�=%s%n",
+                    summary.getProductId(),
+                    summary.getProductModelCount(),
+                    summary.getTotalRouteCount(),
+                    summary.getSkipReason()
+            );
+            return;
+        }
+
+        System.out.printf(
+                "澶勭悊浜у搧鍒嗙被锛歱roductId=%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;
+        }
+    }
+}

--
Gitblit v1.9.3