From a290399ad89d6996d1ebf9e953037d8fdde4e900 Mon Sep 17 00:00:00 2001 From: aike <17730485278@139.com> Date: Fri, 27 Jun 2025 16:38:10 +0800 Subject: [PATCH 1/4] =?UTF-8?q?demand:=20=20=E7=BB=84=E6=A0=91=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=A2=9E=E5=8A=A0=E6=A0=B9=E6=8D=AE=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=BF=87=E6=BB=A4=EF=BC=8C=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E6=8E=A5=E5=8F=A3=E9=80=82=E5=BA=94=E6=89=93?= =?UTF-8?q?=E6=95=A3=E7=9A=84=E4=BA=A7=E5=93=81=E7=B1=BB=E5=9E=8B=EF=BC=8C?= =?UTF-8?q?=E7=BB=84=E6=89=B9=E6=AC=A1=E4=BE=9B=E5=BA=94=E5=95=86=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E4=B8=8D=E5=87=86=E7=A1=AE=E4=BF=AE=E5=A4=8D=EF=BC=8C?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=8E=A5=E5=8F=A3=E9=87=8D=E5=A4=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20committer:=20heyu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erp/emis/service/EmisBaseService.java | 180 ++++++++++++------ .../impl/EmisTmsSiteBatchMissServiceImpl.java | 4 +- .../impl/EmisTransPlanRuleServiceImpl.java | 77 +++++++- .../resources/mapper/EmisSupplierMapper.xml | 4 +- .../mapper/EmisTransPlanRuleMapper.xml | 4 +- 5 files changed, 200 insertions(+), 69 deletions(-) diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java index 3e517bad0..d3f955d64 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java @@ -4011,7 +4011,7 @@ public class EmisBaseService { return weekDays; } - public List buildTree(List allRules) { + public List buildTree(List allRules, EmisTransPlanRule emisTransPlanRule) { // 按lineCode分组 Map> groupByLine = allRules.stream() .collect(Collectors.groupingBy(EmisTransPlanRule::getLineCode)); @@ -4067,6 +4067,19 @@ public class EmisBaseService { entry.setValue(deduped); } + Map> tempMap = new HashMap<>(); + if (StringUtil.isNotBlank(emisTransPlanRule.getProductType())) { + Iterator>> iterator = groupByProductType.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if (emisTransPlanRule.getProductType().equals(entry.getKey())) { + tempMap.put(entry.getKey(), entry.getValue()); + groupByProductType = tempMap; + break; + } + } + } + // 处理每个productType分组 for (Map.Entry> productEntry : groupByProductType.entrySet()) { List productRules = productEntry.getValue(); @@ -4527,7 +4540,7 @@ public class EmisBaseService { public List selectEmisTransPlanRuleTree(List lineCodes) { log.info("Start querying supplier route plan tree in EmisBaseService"); List list = emisTransPlanRuleMapper.selectEmisTransPlanRuleByLineCodes(lineCodes); - List tree = buildTree(list); + List tree = buildTree(list, new EmisTransPlanRule()); log.info("Queried supplier route plan tree in EmisBaseService, node count={}", tree != null ? tree.size() : 0); return tree; } @@ -4773,68 +4786,127 @@ public class EmisBaseService { return hasCurrentLevelMatch && hasNextLevelMatch; } + /** + * 新版漏组分组与筛选逻辑: + * 1. 先将rule从根节点拆成每个children只有一个的所有路径(allPaths)。 + * 2. 统计每条path的匹配度(path上所有节点与missRecords的匹配数之和)。 + * 3. 过滤出匹配度最高的path(可多个)。 + * 4. 对这些path的节点分组去重并做漏组处理。 + */ private void setMissRecords(List missRecords, EmisTransPlanRule rule, List filterRules, EmisWaybill waybill) { - // 1. 递归收集所有节点,按ancestors长度分组 - Map> levelMap = new HashMap<>(); - collectByLevel(rule, levelMap); + // 1. 获取所有路径 + List> allPaths = new ArrayList<>(); + getAllPaths(rule, new ArrayList<>(), allPaths); - // 2. 逐级处理 - for (Map.Entry> entry : levelMap.entrySet()) - { - List levelRules = entry.getValue(); - boolean matched = false; - for (EmisTransPlanRule levelRule : levelRules) { - boolean isMatched = filterRules.stream() - .anyMatch(filterRule -> - filterRule.getStartSiteCode().equals(levelRule.getStartSiteCode()) && - filterRule.getNextSiteCode().equals(levelRule.getNextSiteCode()) && - compareSupplierCodes(filterRule.getSupplierCode(), - levelRule.getSupplierCode())); - if (isMatched) { - matched = true; - break; + // 2. 统计每条路径的匹配度 + Map, Integer> pathMatchCountMap = new HashMap<>(); + for (List path : allPaths) { + int pathMatchCount = 0; + for (EmisTransPlanRule node : path) { + for (EmisTransPlanRule filterRule : filterRules) { + if (Objects.equals(node.getStartSiteCode(), filterRule.getStartSiteCode()) && + Objects.equals(node.getNextSiteCode(), filterRule.getNextSiteCode()) && + compareSupplierCodes(node.getSupplierCode(), filterRule.getSupplierCode())) { + pathMatchCount++; + } } } - if (!matched) { - // 该级无匹配,只生成一条漏组记录,字段用逗号拼接 - String startSiteCodes = - levelRules.stream().map(EmisTransPlanRule::getStartSiteCode).distinct() - .collect(Collectors.joining(",")); - String nextSiteCodes = - levelRules.stream().map(EmisTransPlanRule::getNextSiteCode).distinct() - .collect(Collectors.joining(",")); - String supplierCodes = - levelRules.stream().map(EmisTransPlanRule::getSupplierCode).distinct() - .collect(Collectors.joining(",")); - String managers = - levelRules.stream().map(EmisTransPlanRule::getManager).filter(Objects::nonNull) - .distinct().collect(Collectors.joining(",")); - String missReason = levelRules.stream() - .map(r -> String.format("%s-%s-%s", r.getStartSiteName(), - r.getNextSiteName(), r.getSupplierName())) - .distinct() - .collect(Collectors.joining(" 或 ")); - EmisTmsSiteBatchMiss miss = createMissRecord(waybill); - miss.setMissReason("组批次有遗漏/错误,请根据推荐路线排查: " + missReason); - miss.setStartSiteCode(startSiteCodes); - miss.setNextSiteCode(nextSiteCodes); - miss.setSupplierCode(supplierCodes); - miss.setManager(managers); - // 其他字段取第一个节点 - EmisTransPlanRule first = levelRules.get(0); - miss.setTransPlanRuleId(first.getId()); - miss.setLineCode(first.getLineCode()); - miss.setProductType(first.getProductType()); - miss.setStartDate(first.getStartDate()); - miss.setEndDate(first.getEndDate()); - miss.setMissType("0"); - missRecords.add(miss); + pathMatchCountMap.put(path, pathMatchCount); + } + + // 3. 过滤出匹配度最高的path(可多个) + int maxMatch = pathMatchCountMap.values().stream().max(Integer::compareTo).orElse(0); + List> bestPaths = pathMatchCountMap.entrySet().stream() + .filter(e -> e.getValue() == maxMatch) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // 4. 对这些path的节点分组去重并做漏组处理 + // 先收集bestPaths中的所有节点 + List allNodes = new ArrayList<>(); + for (List path : bestPaths) { + allNodes.addAll(path); + } + + // 按ancestors长度分组去重 + Map> levelMap = new HashMap<>(); + for (EmisTransPlanRule node : allNodes) { + int level = 0; + if (node.getAncestors() != null && !node.getAncestors().isEmpty()) { + level = node.getAncestors().split(",").length; + } + levelMap.computeIfAbsent(level, k -> new ArrayList<>()).add(node); + } + + // 每一级仅按ID去重(不再合并供应商) + for (List rules : levelMap.values()) { + List distinct = rules.stream().collect(Collectors.collectingAndThen( + Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(EmisTransPlanRule::getId))), + ArrayList::new)); + rules.clear(); + rules.addAll(distinct); + } + + // 每一级与filterRules做比较,生成漏组记录 + for (Map.Entry> entry : levelMap.entrySet()) { + List levelRules = entry.getValue(); + // 按 startSiteCode + nextSiteCode 分组 + Map> groupMap = new HashMap<>(); + for (EmisTransPlanRule ruleItem : levelRules) { + String key = ruleItem.getStartSiteCode() + "_" + ruleItem.getNextSiteCode(); + groupMap.computeIfAbsent(key, k -> new ArrayList<>()).add(ruleItem); + } + for (List group : groupMap.values()) { + // 判断该组是否有匹配 + boolean matched = group.stream().anyMatch(levelRule -> filterRules.stream().anyMatch( + filterRule -> Objects.equals(filterRule.getStartSiteCode(), levelRule.getStartSiteCode()) && + Objects.equals(filterRule.getNextSiteCode(), levelRule.getNextSiteCode()) && + compareSupplierCodes(filterRule.getSupplierCode(), levelRule.getSupplierCode()))); + if (!matched && !group.isEmpty()) { + // 聚合supplierCode、supplierName等 + String supplierCodes = group.stream().map(EmisTransPlanRule::getSupplierCode) + .filter(Objects::nonNull).distinct().collect(Collectors.joining(",")); + String supplierNames = group.stream().map(EmisTransPlanRule::getSupplierName) + .filter(Objects::nonNull).distinct().collect(Collectors.joining(",")); + EmisTransPlanRule first = group.get(0); + String missReason = String.format("%s-%s-%s", first.getStartSiteName(), + first.getNextSiteName(), supplierNames); + EmisTmsSiteBatchMiss miss = createMissRecord(waybill); + miss.setMissReason("组批次有遗漏/错误,请根据推荐路线排查: " + missReason); + miss.setStartSiteCode(first.getStartSiteCode()); + miss.setNextSiteCode(first.getNextSiteCode()); + miss.setSupplierCode(supplierCodes); + miss.setManager(first.getManager()); + miss.setTransPlanRuleId(first.getId()); + miss.setLineCode(first.getLineCode()); + miss.setProductType(first.getProductType()); + miss.setStartDate(first.getStartDate()); + miss.setEndDate(first.getEndDate()); + miss.setMissType("0"); + missRecords.add(miss); + } } } } + // 辅助方法:递归获取所有从根到叶子的路径 + private void getAllPaths(EmisTransPlanRule node, List current, + List> result) { + if (node == null) + return; + current.add(node); + if (node.getChildren() == null || node.getChildren().isEmpty()) { + result.add(new ArrayList<>(current)); + } else { + for (EmisTransPlanRule child : node.getChildren()) { + getAllPaths(child, current, result); + } + } + current.remove(current.size() - 1); + } + // 辅助方法:递归收集所有节点,按ancestors长度分组 private void collectByLevel(EmisTransPlanRule node, Map> levelMap) { int level = 0; diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTmsSiteBatchMissServiceImpl.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTmsSiteBatchMissServiceImpl.java index e7f22f735..4a52fcc53 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTmsSiteBatchMissServiceImpl.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTmsSiteBatchMissServiceImpl.java @@ -7,12 +7,10 @@ import java.util.concurrent.Future; import java.util.concurrent.Executors; import cn.hutool.core.collection.CollectionUtil; -import com.beust.jcommander.internal.Lists; import com.xdadan.erp.emis.domain.*; import com.xdadan.erp.emis.mapper.*; import com.xdadan.erp.emis.service.EmisBaseService; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -304,7 +302,7 @@ public class EmisTmsSiteBatchMissServiceImpl extends EmisBaseService implements } } executor.shutdown(); - List tree = buildTree(allRules); + List tree = buildTree(allRules, new EmisTransPlanRule()); log.info("Queried supplier route plan tree, node count={}", tree != null ? tree.size() : 0); return tree; } diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java index f933798a9..fa8a0fdbb 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java @@ -123,7 +123,7 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm } // 构建树形结构 - return buildTree(allRules); + return buildTree(allRules, emisTransPlanRule); } @Override @@ -206,21 +206,82 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm } private void checkDuplicate(EmisTransPlanRule emisTransPlanRule) throws EmisBizError { - List existingRules = emisTransPlanRuleMapper.checkDuplicate(emisTransPlanRule); + // 检查正向规则(起始网点到下一网点) + List existingRules = checkDuplicateDirection(emisTransPlanRule, + emisTransPlanRule.getStartSiteCode(), emisTransPlanRule.getNextSiteCode()); if (existingRules != null && !existingRules.isEmpty()) { throw new EmisBizError(EmisBizErrorType.EXISTS, "已存在相同的规则配置"); } - EmisTransPlanRule emisTransPlanRule1 = new EmisTransPlanRule(); - emisTransPlanRule1.setLineCode(emisTransPlanRule.getLineCode()); - emisTransPlanRule1.setProductType(emisTransPlanRule.getProductType()); - emisTransPlanRule1.setStartSiteCode(emisTransPlanRule.getNextSiteCode()); - emisTransPlanRule1.setNextSiteCode(emisTransPlanRule.getStartSiteCode()); - List existingRules1 = emisTransPlanRuleMapper.checkDuplicate(emisTransPlanRule); + + // 检查反向规则(下一网点到起始网点) + List existingRules1 = checkDuplicateDirection(emisTransPlanRule, + emisTransPlanRule.getNextSiteCode(), emisTransPlanRule.getStartSiteCode()); if (existingRules1 != null && !existingRules1.isEmpty()) { throw new EmisBizError(EmisBizErrorType.EXISTS, "已存在相同的规则配置"); } } + private List checkDuplicateDirection(EmisTransPlanRule emisTransPlanRule, + String startSiteCode, String nextSiteCode) { + // 构建查询条件 + EmisTransPlanRule queryRule = new EmisTransPlanRule(); + queryRule.setLineCode(emisTransPlanRule.getLineCode()); + queryRule.setStartSiteCode(startSiteCode); + queryRule.setNextSiteCode(nextSiteCode); + queryRule.setStartDate(emisTransPlanRule.getStartDate()); + queryRule.setEndDate(emisTransPlanRule.getEndDate()); + + // 查询所有匹配的规则 + List allRules = emisTransPlanRuleMapper.checkDuplicate(queryRule); + + if (allRules == null || allRules.isEmpty()) { + return new ArrayList<>(); + } + + // 检查productType和supplierCode是否有交集 + String newProductType = emisTransPlanRule.getProductType(); + String newSupplierCode = emisTransPlanRule.getSupplierCode(); + + for (EmisTransPlanRule existingRule : allRules) { + // 检查产品类型是否有交集 + boolean productTypeOverlap = hasOverlap(newProductType, existingRule.getProductType()); + // 检查供应商编码是否有交集 + boolean supplierCodeOverlap = hasOverlap(newSupplierCode, existingRule.getSupplierCode()); + + // 如果产品类型和供应商编码都有交集,则认为重复 + if (productTypeOverlap && supplierCodeOverlap) { + return Arrays.asList(existingRule); + } + } + + return new ArrayList<>(); + } + + /** + * 检查两个逗号分隔的字符串是否有交集 + * + * @param str1 第一个字符串(逗号分隔) + * @param str2 第二个字符串(逗号分隔) + * @return 是否有交集 + */ + private boolean hasOverlap(String str1, String str2) { + if (StringUtil.isBlank(str1) || StringUtil.isBlank(str2)) { + return false; + } + + Set set1 = new HashSet<>(Arrays.asList(str1.split(","))); + Set set2 = new HashSet<>(Arrays.asList(str2.split(","))); + + // 检查是否有交集 + for (String item : set1) { + if (set2.contains(item.trim())) { + return true; + } + } + + return false; + } + private static void checkName(EmisTransPlanRule emisTransPlanRule) throws EmisBizError { if (StringUtil.isBlank(emisTransPlanRule.getManager())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "节点负责人不能为空"); diff --git a/emis-biz/src/main/resources/mapper/EmisSupplierMapper.xml b/emis-biz/src/main/resources/mapper/EmisSupplierMapper.xml index cf0e30d22..19fc28ed4 100644 --- a/emis-biz/src/main/resources/mapper/EmisSupplierMapper.xml +++ b/emis-biz/src/main/resources/mapper/EmisSupplierMapper.xml @@ -260,11 +260,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" From 2d5da6200d936b939633b4ddfbec885a5c92f489 Mon Sep 17 00:00:00 2001 From: aike <17730485278@139.com> Date: Mon, 30 Jun 2025 09:26:06 +0800 Subject: [PATCH 2/4] =?UTF-8?q?demand:=20=20=E6=96=B0=E5=A2=9E=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=A2=9E=E5=8A=A0=E5=BE=AA=E7=8E=AF=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=A0=A1=E9=AA=8C=E7=BB=84=E6=A0=91?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20committer:=20heyu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/emis/EmisTransPlanRuleController.java | 16 +- .../emis/mapper/EmisTransPlanRuleMapper.java | 7 + .../erp/emis/service/EmisBaseService.java | 86 ++-- .../service/IEmisTransPlanRuleService.java | 8 + .../impl/EmisTransPlanRuleServiceImpl.java | 420 +++++++++++++++++- .../mapper/EmisTransPlanRuleMapper.xml | 17 + 6 files changed, 510 insertions(+), 44 deletions(-) diff --git a/emis-biz-web/src/main/java/com/xdadan/erp/web/emis/EmisTransPlanRuleController.java b/emis-biz-web/src/main/java/com/xdadan/erp/web/emis/EmisTransPlanRuleController.java index 84cd5e2bc..cc1acc16a 100644 --- a/emis-biz-web/src/main/java/com/xdadan/erp/web/emis/EmisTransPlanRuleController.java +++ b/emis-biz-web/src/main/java/com/xdadan/erp/web/emis/EmisTransPlanRuleController.java @@ -128,9 +128,23 @@ public class EmisTransPlanRuleController extends EmisBaseController { /** * 查询供应商线路规划树结构 */ - @PreAuthorize("@ss.hasPermi('emis:emisTransPlanRule:tree')") + // @PreAuthorize("@ss.hasPermi('emis:emisTransPlanRule:tree')") @GetMapping("/tree") public TableDataInfo tree(EmisTransPlanRule emisTransPlanRule) { return getDataTable(emisTransPlanRuleService.selectEmisTransPlanRuleTree(emisTransPlanRule)); } + + /** + * 校验规则是否能组树 + */ + // @PreAuthorize("@ss.hasPermi('emis:emisTransPlanRule:tree')") + @GetMapping("/checkTree") + public TableDataInfo checkTree(EmisTransPlanRule emisTransPlanRule) { + List cycles = emisTransPlanRuleService.checkTree(emisTransPlanRule); + if (cycles == null || cycles.isEmpty()) { + cycles = new java.util.ArrayList<>(); + cycles.add("未检测到循环引用"); + } + return getDataTable(cycles); + } } \ No newline at end of file diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/mapper/EmisTransPlanRuleMapper.java b/emis-biz/src/main/java/com/xdadan/erp/emis/mapper/EmisTransPlanRuleMapper.java index 2194c4b64..249f47942 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/mapper/EmisTransPlanRuleMapper.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/mapper/EmisTransPlanRuleMapper.java @@ -173,4 +173,11 @@ public interface EmisTransPlanRuleMapper { * @return */ List checkTransPlanRule(EmisTransPlanRule emisTransPlanRule); + + /** + * + * @param emisTransPlanRule + * @return + */ + List selectEmisTransPlanRuleListWithoutName(EmisTransPlanRule emisTransPlanRule); } \ No newline at end of file diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java index d3f955d64..4b44abbd6 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/EmisBaseService.java @@ -4204,46 +4204,66 @@ public class EmisBaseService { List cyclePath = visitPath.subList(cycleStartIndex, visitPath.size()); cyclePath.add(source.getId()); - // 构建详细的错误信息 - StringBuilder errorMsg = new StringBuilder(); - errorMsg.append("检测到循环引用!循环路径:"); - for (int i = 0; i < cyclePath.size(); i++) { - if (i > 0) { - errorMsg.append(" -> "); - } - errorMsg.append("节点ID:").append(cyclePath.get(i)); - } - errorMsg.append("\n循环引用发生在节点ID:").append(source.getId()); - errorMsg.append(",该节点在路径中重复出现"); + // 构建详细的错误信息 + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("检测到循环引用!循环路径:"); + for (int i = 0; i < cyclePath.size(); i++) { + if (i > 0) { + errorMsg.append(" -> "); + } + Long id = cyclePath.get(i); + EmisTransPlanRule node = findNodeById(id); + if (node != null) { + errorMsg.append(node.getLineName()).append("-") + .append(node.getProductTypeName()).append("-") + .append(node.getStartSiteName()).append("-") + .append(node.getNextSiteName()).append("-") + .append(node.getSupplierName()); + } else { + errorMsg.append("节点ID:").append(id); + } + } + errorMsg.append("\n循环引用发生在节点,详细信息见上"); + throw new RuntimeException(errorMsg.toString()); + } + } - throw new RuntimeException(errorMsg.toString()); - } - } + // 添加到已访问集合和路径中 + visitedIds.add(source.getId()); + visitPath.add(source.getId()); - // 添加到已访问集合和路径中 - visitedIds.add(source.getId()); - visitPath.add(source.getId()); + EmisTransPlanRule copy = new EmisTransPlanRule(); + BeanUtils.copyProperties(source, copy); - EmisTransPlanRule copy = new EmisTransPlanRule(); - BeanUtils.copyProperties(source, copy); + // 为每个新节点生成唯一treeId + copy.setTreeId(com.xdadan.erp.common.utils.idgen.UUID.fastUUID().toString()); - // 为每个新节点生成唯一treeId - copy.setTreeId(com.xdadan.erp.common.utils.idgen.UUID.fastUUID().toString()); + // 递归复制子节点 + if (source.getChildren() != null) { + List copiedChildren = new ArrayList<>(); + for (EmisTransPlanRule child : source.getChildren()) { + copiedChildren.add(deepCopyNode(child, visitedIds, visitPath)); + } + copy.setChildren(copiedChildren); + } - // 递归复制子节点 - if (source.getChildren() != null) { - List copiedChildren = new ArrayList<>(); - for (EmisTransPlanRule child : source.getChildren()) { - copiedChildren.add(deepCopyNode(child, visitedIds, visitPath)); - } - copy.setChildren(copiedChildren); - } + // 从路径中移除当前节点(回溯) + visitPath.remove(visitPath.size() - 1); - // 从路径中移除当前节点(回溯) - visitPath.remove(visitPath.size() - 1); + return copy; + } - return copy; - } + // 新增方法:根据ID查找节点 + private EmisTransPlanRule findNodeById(Long id) { + // 由于deepCopyNode递归时无法直接访问所有节点,这里可通过全量查询实现,实际生产建议优化 + List allRules = emisTransPlanRuleMapper.selectEmisTransPlanRuleList(new EmisTransPlanRule()); + for (EmisTransPlanRule rule : allRules) { + if (rule.getId() != null && rule.getId().equals(id)) { + return rule; + } + } + return null; + } /** * 检查月结客户首次签约日期,首次签约之前开的不允许修改 diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/IEmisTransPlanRuleService.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/IEmisTransPlanRuleService.java index 374ba6c06..d2436be51 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/IEmisTransPlanRuleService.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/IEmisTransPlanRuleService.java @@ -74,4 +74,12 @@ public interface IEmisTransPlanRuleService { * @return 结果 */ int draftSubmitRule(EmisTransPlanRule emisTransPlanRule) throws EmisBizError; + + /** + * 校验供应商线路规划树结构中的循环引用 + * + * @param emisTransPlanRule 查询条件 + * @return 循环引用提示信息列表 + */ + List checkTree(EmisTransPlanRule emisTransPlanRule); } \ No newline at end of file diff --git a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java index fa8a0fdbb..73ebe794c 100644 --- a/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java +++ b/emis-biz/src/main/java/com/xdadan/erp/emis/service/impl/EmisTransPlanRuleServiceImpl.java @@ -82,6 +82,12 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm checkDate(emisTransPlanRule); // 检查重复 checkDuplicate(emisTransPlanRule); + // 校验循环引用 + EmisTransPlanRule checkRule = new EmisTransPlanRule(); + checkRule.setProductType(emisTransPlanRule.getProductType()); + List allRules = emisTransPlanRuleMapper.selectEmisTransPlanRuleListWithoutName(checkRule); + allRules.add(emisTransPlanRule); + buildTree(allRules, emisTransPlanRule); return emisTransPlanRuleMapper.insertEmisTransPlanRule(emisTransPlanRule); } @@ -150,13 +156,19 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm // 检查重复 checkDuplicate(emisTransPlanRule); - + // 校验循环引用 + EmisTransPlanRule checkRule = new EmisTransPlanRule(); + checkRule.setProductType(emisTransPlanRule.getProductType()); + List allRules = emisTransPlanRuleMapper.selectEmisTransPlanRuleListWithoutName(checkRule); + allRules.add(emisTransPlanRule); + buildTree(allRules, emisTransPlanRule); return emisTransPlanRuleMapper.insertEmisTransPlanRule(emisTransPlanRule); } private void nameToCode(EmisTransPlanRule emisTransPlanRule) throws EmisBizError { // 线路 - List> lineInfo = emisTransPlanRuleMapper.selectLineInfoByNames(Collections.singletonList(emisTransPlanRule.getLineName())); + List> lineInfo = emisTransPlanRuleMapper + .selectLineInfoByNames(Collections.singletonList(emisTransPlanRule.getLineName())); if (lineInfo == null || lineInfo.isEmpty()) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "未找到对应的线路:" + emisTransPlanRule.getLineName()); } @@ -164,9 +176,11 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm // 产品类型(支持多个,逗号分隔) String[] productTypeNames = emisTransPlanRule.getProductTypeName().split(","); - List> productTypeInfo = emisTransPlanRuleMapper.selectProductTypeInfoByNamesAndLineCode(Arrays.asList(productTypeNames),lineInfo.get(0).get("code")); + List> productTypeInfo = emisTransPlanRuleMapper + .selectProductTypeInfoByNamesAndLineCode(Arrays.asList(productTypeNames), lineInfo.get(0).get("code")); if (productTypeInfo == null || productTypeInfo.isEmpty()) { - throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "未找到对应的产品类型:" + emisTransPlanRule.getProductTypeName()); + throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, + "未找到对应的产品类型:" + emisTransPlanRule.getProductTypeName()); } if (productTypeInfo.size() != productTypeNames.length) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "部分产品类型未找到:" + emisTransPlanRule.getProductTypeName()); @@ -177,14 +191,16 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm emisTransPlanRule.setProductType(productTypeCodes); // 起始网点 - List> startSiteInfo = emisTransPlanRuleMapper.selectSiteInfoByNames(Collections.singletonList(emisTransPlanRule.getStartSiteName())); + List> startSiteInfo = emisTransPlanRuleMapper + .selectSiteInfoByNames(Collections.singletonList(emisTransPlanRule.getStartSiteName())); if (startSiteInfo == null || startSiteInfo.isEmpty()) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "未找到对应的起始网点:" + emisTransPlanRule.getStartSiteName()); } emisTransPlanRule.setStartSiteCode(startSiteInfo.get(0).get("code")); // 下一网点 - List> nextSiteInfo = emisTransPlanRuleMapper.selectSiteInfoByNames(Collections.singletonList(emisTransPlanRule.getNextSiteName())); + List> nextSiteInfo = emisTransPlanRuleMapper + .selectSiteInfoByNames(Collections.singletonList(emisTransPlanRule.getNextSiteName())); if (nextSiteInfo == null || nextSiteInfo.isEmpty()) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "未找到对应的下一网点:" + emisTransPlanRule.getNextSiteName()); } @@ -192,7 +208,8 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm // 供应商(支持多个,逗号分隔) String[] supplierNames = emisTransPlanRule.getSupplierName().split(","); - List> supplierInfo = emisTransPlanRuleMapper.selectSupplierInfoByNames(Arrays.asList(supplierNames)); + List> supplierInfo = emisTransPlanRuleMapper + .selectSupplierInfoByNames(Arrays.asList(supplierNames)); if (supplierInfo == null || supplierInfo.isEmpty()) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "未找到对应的供应商:" + emisTransPlanRule.getSupplierName()); } @@ -293,7 +310,6 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "产品名称不能为空"); } - if (StringUtil.isBlank(emisTransPlanRule.getStartSiteName())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "起始网点名称不能为空"); } @@ -303,7 +319,7 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm if (StringUtil.isBlank(emisTransPlanRule.getSupplierName())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "供应商名称不能为空"); } - if(emisTransPlanRule.getStartSiteName().equals(emisTransPlanRule.getNextSiteName())){ + if (emisTransPlanRule.getStartSiteName().equals(emisTransPlanRule.getNextSiteName())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "同一产品类型下起始网点、下一网点不能相同"); } } @@ -327,7 +343,7 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm if (StringUtil.isBlank(emisTransPlanRule.getSupplierCode())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "供应商名称不能为空"); } - if(emisTransPlanRule.getStartSiteCode().equals(emisTransPlanRule.getNextSiteCode())){ + if (emisTransPlanRule.getStartSiteCode().equals(emisTransPlanRule.getNextSiteCode())) { throw new EmisBizError(EmisBizErrorType.PARAM_ERROR, "同一产品类型下起始网点、下一网点不能相同"); } } @@ -346,4 +362,388 @@ public class EmisTransPlanRuleServiceImpl extends EmisBaseService implements IEm } } + // public List buildTree(List allRules, + // EmisTransPlanRule emisTransPlanRule) { + // // 按lineCode分组 + // Map> groupByLine = allRules.stream() + // .collect(Collectors.groupingBy(EmisTransPlanRule::getLineCode)); + // + // List result = new ArrayList<>(); + // + // // 处理每个lineCode分组 + // for (Map.Entry> lineEntry : + // groupByLine.entrySet()) { + // List lineRules = lineEntry.getValue(); + // if (lineRules.isEmpty()) { + // continue; + // } + // + // // + // 按productType分组(支持productType和productTypeName为逗号分隔的多个类型,且一一对应,且每个rule只保留一个type和typeName) + // Map> groupByProductType = new HashMap<>(); + // for (EmisTransPlanRule rule : lineRules) { + // if (rule.getProductType() != null) { + // String[] types = rule.getProductType().split(","); + // String[] typeNames = rule.getProductTypeName() != null ? + // rule.getProductTypeName().split(",") + // : new String[0]; + // for (int i = 0; i < types.length; i++) { + // String type = types[i].trim(); + // if (!type.isEmpty()) { + // EmisTransPlanRule newRule = new EmisTransPlanRule(); + // BeanUtils.copyProperties(rule, newRule); + // newRule.setProductType(type); + // if (i < typeNames.length) { + // newRule.setProductTypeName(typeNames[i].trim()); + // } else { + // newRule.setProductTypeName(null); + // } + // groupByProductType.computeIfAbsent(type, k -> new + // ArrayList<>()).add(newRule); + // } + // } + // } + // } + // // + // 分组内去重(lineCode、productType、startSiteCode、nextSiteCode、supplierCode、startDate、endDate都相同只保留一条) + // for (Map.Entry> entry : + // groupByProductType.entrySet()) { + // List rules = entry.getValue(); + // Set uniqueKeys = new HashSet<>(); + // List deduped = new ArrayList<>(); + // for (EmisTransPlanRule rule : rules) { + // String key = (rule.getLineCode() == null ? "" : rule.getLineCode()) + "|" + // + (rule.getProductType() == null ? "" : rule.getProductType()) + "|" + // + (rule.getStartSiteCode() == null ? "" : rule.getStartSiteCode()) + "|" + // + (rule.getNextSiteCode() == null ? "" : rule.getNextSiteCode()) + "|" + // + (rule.getSupplierCode() == null ? "" : rule.getSupplierCode()) + "|" + // + (rule.getStartDate() == null ? "" : rule.getStartDate().toString()) + "|" + // + (rule.getEndDate() == null ? "" : rule.getEndDate().toString()); + // if (uniqueKeys.add(key)) { + // deduped.add(rule); + // } + // } + // entry.setValue(deduped); + // } + // + // Map> tempMap = new HashMap<>(); + // if (StringUtil.isNotBlank(emisTransPlanRule.getProductType())) { + // Iterator>> iterator = + // groupByProductType.entrySet() + // .iterator(); + // while (iterator.hasNext()) { + // Map.Entry> entry = iterator.next(); + // if (emisTransPlanRule.getProductType().equals(entry.getKey())) { + // tempMap.put(entry.getKey(), entry.getValue()); + // groupByProductType = tempMap; + // break; + // } + // } + // } + // + // // 处理每个productType分组 + // for (Map.Entry> productEntry : + // groupByProductType.entrySet()) { + // List productRules = productEntry.getValue(); + // + // // 构建站点关系树 + // Map> nodeMap = new HashMap<>(); + // for (EmisTransPlanRule rule : productRules) { + // String key = rule.getNextSiteCode() != null ? rule.getNextSiteCode() : + // rule.getStartSiteCode(); + // if (key != null) { + // nodeMap.computeIfAbsent(key, k -> new ArrayList<>()).add(rule); + // } + // } + // + // // 按startSiteCode分组,找出所有可能的父节点 + // Map> parentGroups = productRules.stream() + // .filter(rule -> rule.getStartSiteCode() != null) + // .collect(Collectors.groupingBy(EmisTransPlanRule::getStartSiteCode)); + // + // // 设置父子关系 + // for (Map.Entry> parentGroup : + // parentGroups.entrySet()) { + // String startSiteCode = parentGroup.getKey(); + // List children = parentGroup.getValue(); + // + // // 获取所有可能的父节点 + // List parents = nodeMap.get(startSiteCode); + // if (parents != null) { + // for (EmisTransPlanRule parent : parents) { + // if (parent.getChildren() == null) { + // parent.setChildren(new ArrayList<>()); + // } + // + // // 添加所有子节点到父节点的children列表中 + // for (EmisTransPlanRule child : children) { + // if (!parent.getChildren().contains(child)) { + // parent.getChildren().add(child); + // } + // } + // } + // } + // } + // + // // 找出顶级节点(没有父节点的节点) + // List topLevelNodes = productRules.stream() + // .filter(rule -> { + // // 检查该节点是否作为其他节点的子节点 + // boolean isChild = productRules.stream() + // .anyMatch(potential -> potential.getChildren() != null + // && potential.getChildren().contains(rule)); + // return !isChild; + // }) + // .collect(Collectors.toList()); + // + // // 将顶级节点直接添加到结果中(进行深度复制) + // for (EmisTransPlanRule node : topLevelNodes) { + // result.add(deepCopyNode(node)); + // } + // } + // } + // + // // 递归设置父ID和祖先节点ID(以treeId为准) + // for (EmisTransPlanRule root : result) { + // setParentAndAncestors(root, null, ""); + // } + // + // return result; + // } + // + // /** + // * 递归设置父ID和祖先节点ID + // * + // * @param node 当前节点 + // * @param parentId 父节点ID + // * @param ancestors 祖先节点ID字符串 + // */ + // private void setParentAndAncestors(EmisTransPlanRule node, String parentId, + // String ancestors) { + // if (node == null) { + // return; + // } + // // 设置父ID + // node.setParentId(parentId); + // + // // 设置祖先节点ID + // node.setAncestors(ancestors); + // // 处理子节点,子节点的祖先包含当前节点和当前节点的所有祖先 + // if (node.getChildren() != null && !node.getChildren().isEmpty()) { + // String childAncestors = ancestors.isEmpty() ? node.getTreeId() : ancestors + + // "," + node.getTreeId(); + // for (EmisTransPlanRule child : node.getChildren()) { + // setParentAndAncestors(child, node.getTreeId(), childAncestors); + // } + // } + // } + // + // /** + // * 深度复制节点及其子节点 + // * + // * @param source 源节点 + // * @return 复制后的新节点 + // */ + // private EmisTransPlanRule deepCopyNode(EmisTransPlanRule source) { + // return deepCopyNode(source, new HashSet<>(), new ArrayList<>()); + // } + // + // /** + // * 深度复制节点及其子节点(带循环引用检测) + // * + // * @param source 源节点 + // * @param visitedIds 已访问的节点ID集合 + // * @param visitPath 访问路径,用于记录循环引用的路径 + // * @return 复制后的新节点 + // */ + // private EmisTransPlanRule deepCopyNode(EmisTransPlanRule source, Set + // visitedIds, List visitPath) { + // if (source == null) { + // return null; + // } + // + // // 检查循环引用 + // if (visitedIds.contains(source.getId())) { + // // 找到循环引用的起始位置 + // int cycleStartIndex = visitPath.indexOf(source.getId()); + // if (cycleStartIndex != -1) { + // // 构建循环路径 + // List cyclePath = visitPath.subList(cycleStartIndex, visitPath.size()); + // cyclePath.add(source.getId()); + // + // // 构建详细的错误信息 + // StringBuilder errorMsg = new StringBuilder(); + // errorMsg.append("检测到循环引用!循环路径:"); + // for (int i = 0; i < cyclePath.size(); i++) { + // if (i > 0) { + // errorMsg.append(" -> "); + // } + // Long id = cyclePath.get(i); + // EmisTransPlanRule node = findNodeById(id); + // if (node != null) { + // errorMsg.append(node.getLineName()).append("-") + // .append(node.getProductTypeName()).append("-") + // .append(node.getStartSiteName()).append("-") + // .append(node.getNextSiteName()).append("-") + // .append(node.getSupplierName()); + // } else { + // errorMsg.append("节点ID:").append(id); + // } + // } + // errorMsg.append("\n循环引用发生在节点,详细信息见上"); + // throw new RuntimeException(errorMsg.toString()); + // } + // } + // + // // 添加到已访问集合和路径中 + // visitedIds.add(source.getId()); + // visitPath.add(source.getId()); + // + // EmisTransPlanRule copy = new EmisTransPlanRule(); + // BeanUtils.copyProperties(source, copy); + // + // // 为每个新节点生成唯一treeId + // copy.setTreeId(com.xdadan.erp.common.utils.idgen.UUID.fastUUID().toString()); + // + // // 递归复制子节点 + // if (source.getChildren() != null) { + // List copiedChildren = new ArrayList<>(); + // for (EmisTransPlanRule child : source.getChildren()) { + // copiedChildren.add(deepCopyNode(child, visitedIds, visitPath)); + // } + // copy.setChildren(copiedChildren); + // } + // + // // 从路径中移除当前节点(回溯) + // visitPath.remove(visitPath.size() - 1); + // + // return copy; + // } + // + // // 新增方法:根据ID查找节点 + // private EmisTransPlanRule findNodeById(Long id) { + // // 由于deepCopyNode递归时无法直接访问所有节点,这里可通过全量查询实现,实际生产建议优化 + // List allRules = + // emisTransPlanRuleMapper.selectEmisTransPlanRuleList(new EmisTransPlanRule()); + // for (EmisTransPlanRule rule : allRules) { + // if (rule.getId() != null && rule.getId().equals(id)) { + // return rule; + // } + // } + // return null; + // } + + @Override + public List checkTree(EmisTransPlanRule emisTransPlanRule) { + List allRules = emisTransPlanRuleMapper.selectEmisTransPlanRuleList(emisTransPlanRule); + return buildTreeForCheck(allRules); + } + + /** + * 仅校验循环引用并收集所有循环引用提示 + * + * @param allRules 所有规则 + * @return 循环引用提示信息列表 + */ + public List buildTreeForCheck(List allRules) { + List cycleMessages = new ArrayList<>(); + // 按lineCode+productType分组 + Map> groupMap = new HashMap<>(); + for (EmisTransPlanRule rule : allRules) { + String[] types = rule.getProductType() != null ? rule.getProductType().split(",") : new String[0]; + String[] typeNames = rule.getProductTypeName() != null ? rule.getProductTypeName().split(",") + : new String[0]; + for (int i = 0; i < types.length; i++) { + String type = types[i].trim(); + if (!type.isEmpty()) { + EmisTransPlanRule newRule = new EmisTransPlanRule(); + BeanUtils.copyProperties(rule, newRule); + newRule.setProductType(type); + if (i < typeNames.length) { + newRule.setProductTypeName(typeNames[i].trim()); + } else { + newRule.setProductTypeName(null); + } + String key = (newRule.getLineCode() == null ? "" : newRule.getLineCode()) + "|" + type; + groupMap.computeIfAbsent(key, k -> new ArrayList<>()).add(newRule); + } + } + } + // 每组独立校验 + for (List group : groupMap.values()) { + // 构建邻接表:startSiteCode -> List + Map> adjMap = new HashMap<>(); + for (EmisTransPlanRule rule : group) { + if (rule.getStartSiteCode() != null) { + adjMap.computeIfAbsent(rule.getStartSiteCode(), k -> new ArrayList<>()).add(rule); + } + } + // 全局visited,避免重复DFS + Set globalVisited = new HashSet<>(); + for (EmisTransPlanRule rule : group) { + if (rule.getId() != null && !globalVisited.contains(rule.getId())) { + dfsCheckCycle(rule, adjMap, new HashSet<>(), new ArrayList<>(), cycleMessages, globalVisited); + } + } + } + return cycleMessages; + } + + /** + * DFS检测循环引用并收集路径,避免死循环 + */ + private void dfsCheckCycle( + EmisTransPlanRule node, + Map> adjMap, + Set pathSet, + List path, + List cycleMessages, + Set globalVisited) { + if (node.getId() == null) + return; + if (pathSet.contains(node.getId())) { + // 检测到环,收集路径 + int idx = -1; + for (int i = 0; i < path.size(); i++) { + if (path.get(i).getId().equals(node.getId())) { + idx = i; + break; + } + } + if (idx != -1) { + List cyclePath = new ArrayList<>(path.subList(idx, path.size())); + cyclePath.add(node); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < cyclePath.size(); i++) { + if (i > 0) + sb.append(" -> "); + EmisTransPlanRule n = cyclePath.get(i); + sb.append( + (n.getLineName() == null ? "" : n.getLineName()) + "-" + + (n.getProductTypeName() == null ? "" : n.getProductTypeName()) + "-" + + (n.getStartSiteName() == null ? "" : n.getStartSiteName()) + "-" + + (n.getNextSiteName() == null ? "" : n.getNextSiteName()) + "-" + + (n.getSupplierName() == null ? "" : n.getSupplierName())); + } + cycleMessages.add("检测到循环引用: " + sb.toString()); + } + return; + } + if (globalVisited.contains(node.getId())) + return; // 已经全局遍历过,跳过 + pathSet.add(node.getId()); + path.add(node); + if (node.getNextSiteCode() != null) { + List nextList = adjMap.get(node.getNextSiteCode()); + if (nextList != null) { + for (EmisTransPlanRule next : nextList) { + dfsCheckCycle(next, adjMap, pathSet, path, cycleMessages, globalVisited); + } + } + } + path.remove(path.size() - 1); + pathSet.remove(node.getId()); + globalVisited.add(node.getId()); + } + } \ No newline at end of file diff --git a/emis-biz/src/main/resources/mapper/EmisTransPlanRuleMapper.xml b/emis-biz/src/main/resources/mapper/EmisTransPlanRuleMapper.xml index 937a75d42..7ceda2966 100644 --- a/emis-biz/src/main/resources/mapper/EmisTransPlanRuleMapper.xml +++ b/emis-biz/src/main/resources/mapper/EmisTransPlanRuleMapper.xml @@ -61,6 +61,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by id desc + +