浏览代码

静态指标分层分级修改

lisonglin 3 周之前
父节点
当前提交
c1f34edd0e
共有 13 个文件被更改,包括 444 次插入147 次删除
  1. 4 4
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/StaticGridController.java
  2. 4 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/staticgrid/StaticIndicatorMetric.java
  3. 6 3
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/staticgrid/StaticIndicatorResult.java
  4. 6 6
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDashboardVO.java
  5. 1 8
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/StaticGridDashboardService.java
  6. 391 106
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java
  7. 3 3
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/algorithm/AlgorithmController.java
  8. 5 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/entity/staticgrid/StaticIndicatorMetric.java
  9. 7 2
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/entity/staticgrid/StaticIndicatorResult.java
  10. 3 3
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/AlgorithmService.java
  11. 2 2
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/impl/AlgorithmServiceImpl.java
  12. 2 2
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/StaticIndicatorResultService.java
  13. 10 7
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/impl/StaticIndicatorResultServiceImpl.java

+ 4 - 4
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/StaticGridController.java

@@ -41,10 +41,10 @@ public class StaticGridController extends BaseController {
      */
     @PostMapping("/dashboard")
     public ApiResponse<StaticGridDashboardVO> dashboard(@RequestBody Map<String, String> params) {
-        String batchId = params.get("batchId");
-        StaticGridDashboardVO vo = StringUtils.isBlank(batchId)
-                ? staticGridDashboardService.queryLatestDashboard()
-                : staticGridDashboardService.queryDashboardByBatchId(batchId);
+        String daytime = params.get("daytime");
+        String id = params.get("id");
+        String type = params.get("type");
+        StaticGridDashboardVO vo = staticGridDashboardService.queryLatestDashboard(daytime,id,type);
         return ApiResponse.success(vo);
     }
 }

+ 4 - 1
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/staticgrid/StaticIndicatorMetric.java

@@ -43,6 +43,9 @@ public class StaticIndicatorMetric implements Serializable {
     @ApiModelProperty(value = "馈线名称")
     private String feederName;
 
+    @ApiModelProperty(value = "源变电站ID")
+    private String sourceSubstationId;
+
     @ApiModelProperty(value = "源变电站名称")
     private String sourceSubstationName;
 
@@ -81,4 +84,4 @@ public class StaticIndicatorMetric implements Serializable {
 
     @ApiModelProperty(value = "是否为问题项")
     private Integer isProblem;
-}
+}

+ 6 - 3
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/staticgrid/StaticIndicatorResult.java

@@ -35,8 +35,11 @@ public class StaticIndicatorResult implements Serializable {
     @ApiModelProperty(value = "批次ID")
     private String batchId;
 
-    @ApiModelProperty(value = "区县ID")
-    private Integer countyId;
+    @ApiModelProperty(value = "区县ID(即运维单位ID maintOrgId)")
+    private String countyId;
+
+    @ApiModelProperty(value = "运维单位ID(跟countyId同值,冗余便于树节点直接关联)")
+    private String maintOrgId;
 
     @ApiModelProperty(value = "区县短标识")
     private String countySid;
@@ -74,4 +77,4 @@ public class StaticIndicatorResult implements Serializable {
     @ApiModelProperty(value = "创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime createTime;
-}
+}

+ 6 - 6
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDashboardVO.java

@@ -41,8 +41,8 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "批次ID")
     private String batchId;
 
-    @ApiModelProperty(value = "区县ID")
-    private Integer countyId;
+    @ApiModelProperty(value = "区县ID(即运维单位ID maintOrgId)")
+    private String countyId;
 
     @ApiModelProperty(value = "区县短标识")
     private String countySid;
@@ -57,7 +57,7 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "数据质量状态")
     private String dataQualityStatus;
 
-    @ApiModelProperty(value = "数据完整率(%)")
+    @ApiModelProperty(value = "数据完整率(%)\")")
     private BigDecimal dataQualityCompletenessRate;
 
     // ============================================================
@@ -98,7 +98,7 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "分段数不合理线路数")
     private Integer unreasonableSegmentCount;
 
-    @ApiModelProperty(value = "主干线分段配变密集数(预留,未在当前算法返回中)")
+    @ApiModelProperty(value = "主干线分段配变密集数")
     private Integer mainTrunkSegmentDenseCount;
 
     // ============================================================
@@ -127,7 +127,7 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "线路过载条数(预留,来自运行态指标)")
     private Integer lineOverloadCount;
 
-    @ApiModelProperty(value = "主干线截面不匹配线路数(预留,来源待定)")
+    @ApiModelProperty(value = "主干线截面不匹配线路数")
     private Integer mainTrunkSectionMismatchCount;
 
     @ApiModelProperty(value = "线路装接容量超标准线路数")
@@ -188,4 +188,4 @@ public class StaticGridDashboardVO implements Serializable {
         @ApiModelProperty(value = "整改建议")
         private String suggestion;
     }
-}
+}

+ 1 - 8
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/StaticGridDashboardService.java

@@ -17,13 +17,6 @@ public interface StaticGridDashboardService extends IService<StaticIndicatorResu
      *
      * @return 仪表盘 VO
      */
-    StaticGridDashboardVO queryLatestDashboard();
+    StaticGridDashboardVO queryLatestDashboard(String daytime, String id, String type);
 
-    /**
-     * 按 batchId 查询指定一次静态网架评估结果
-     *
-     * @param batchId 批次ID
-     * @return 仪表盘 VO
-     */
-    StaticGridDashboardVO queryDashboardByBatchId(String batchId);
 }

+ 391 - 106
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java

@@ -18,18 +18,25 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * 静态网架评估仪表盘服务实现
  * <p>
- * 从 t_static_indicator_result + t_static_indicator_metric + t_static_problem_detail 三张表
- * 聚合数据,构建前端仪表盘 VO。
- * <p>
- * 预留字段(运行态指标/外部台账)直接返回 null,前端渲染为"-"。
+ * 支持三种节点维度查询:
+ * <ul>
+ *   <li>type=2 区县 — 取 county 级指标</li>
+ *   <li>type=3 变电站 — 聚合该变电站下所有馈线的 feeder 级指标</li>
+ *   <li>type=4 馈线 — 取单条馈线的 feeder 级指标(聚合后 bool→0/1、rate→0%/100%)</li>
+ * </ul>
+ * 静态指标按月份过滤,前端传 daytime(如 2026-7-16),取该月最新一批数据。
+ * 停电时户数通过馈线组树递归收集馈线名列表后查询。
  *
  * @author lsl
  * @since 2026-07-19
@@ -46,73 +53,239 @@ public class StaticGridDashboardServiceImpl
     private final FeederGroupApplication feederGroupApplication;
     private final SjztPwwyyqxtPrmPdrOutageMvDsService pwwyyqxtPrmPdrOutageMvDsService;
 
+    // ============================================================
+    // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
+    // ============================================================
+
+    /** 布尔计数类:count(value="true") */
+    private static final Map<String, String> BOOL_COUNT_MAP = new LinkedHashMap<>();
+    static {
+        BOOL_COUNT_MAP.put("same_bus_interconnection_issue_count", "has_same_bus_interconnection_issue");
+        BOOL_COUNT_MAP.put("excessive_tie_point_feeder_count", "has_excessive_tie_points");
+        BOOL_COUNT_MAP.put("single_radial_feeder_count", "is_single_radial_feeder");
+        BOOL_COUNT_MAP.put("large_branch_issue_feeder_count", "has_large_branch_issue");
+        BOOL_COUNT_MAP.put("main_trunk_lantern_issue_feeder_count", "has_main_trunk_lantern_issue");
+        BOOL_COUNT_MAP.put("unreasonable_segment_count_feeder_count", "has_unreasonable_segment_count");
+        BOOL_COUNT_MAP.put("supply_radius_exceed_feeder_count", "is_supply_radius_exceeded");
+        BOOL_COUNT_MAP.put("extra_long_feeder_count", "is_extra_long_feeder");
+        BOOL_COUNT_MAP.put("main_trunk_bottleneck_feeder_count", "has_main_trunk_bottleneck");
+        BOOL_COUNT_MAP.put("connected_capacity_exceed_feeder_count", "is_connected_capacity_exceeded");
+    }
+
+    /** 布尔比率类:count(true)/total×100 */
+    private static final Map<String, String> BOOL_RATE_MAP = new LinkedHashMap<>();
+    static {
+        BOOL_RATE_MAP.put("wiring_standardization_rate", "is_standard_wiring");
+        BOOL_RATE_MAP.put("main_trunk_section_standardization_rate", "main_trunk_section_compliant");
+        BOOL_RATE_MAP.put("n_minus_1_pass_rate", "n_minus_1_passed");
+        BOOL_RATE_MAP.put("line_interconnection_rate", "has_tie");
+        BOOL_RATE_MAP.put("inter_substation_interconnection_rate", "has_inter_substation_tie");
+    }
+
+    /** 整数求和类:sum(value) */
+    private static final Map<String, String> INT_SUM_MAP = new LinkedHashMap<>();
+    static {
+        INT_SUM_MAP.put("feeder_head_tie_point_issue_count", "feeder_head_tie_point_issue_count");
+        INT_SUM_MAP.put("same_pole_tie_point_issue_count", "same_pole_tie_point_issue_count");
+        INT_SUM_MAP.put("load_group_capacity_anomaly_count", "load_group_capacity_anomaly_count");
+        INT_SUM_MAP.put("main_trunk_total_count", "main_trunk_count");
+        INT_SUM_MAP.put("section_compliant_main_trunk_count", "section_compliant_main_trunk_count");
+    }
+
+    // ============================================================
+    // 主入口
+    // ============================================================
+
     @Override
-    public StaticGridDashboardVO queryLatestDashboard() {
-        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
-                .orderByDesc(StaticIndicatorResult::getCreateTime)
-                .last("LIMIT 1");
-        StaticIndicatorResult result = getOne(wrapper);
+    public StaticGridDashboardVO queryLatestDashboard(String daytime, String id, String type) {
+        if (StringUtils.isBlank(id) || StringUtils.isBlank(type)) {
+            log.warn("参数缺失: id={}, type={}", id, type);
+            return null;
+        }
+
+        int nodeType;
+        try {
+            nodeType = Integer.parseInt(type);
+        } catch (NumberFormatException e) {
+            log.warn("type 参数非法: {}", type);
+            return null;
+        }
+
+        YearMonth ym = parseYearMonth(daytime);
+        if (nodeType == 2) {
+            return queryByCounty(id, ym);
+        } else if (nodeType == 3) {
+            return queryBySubstation(id, ym);
+        } else if (nodeType == 4) {
+            return queryByFeeder(id, ym);
+        }
+        log.warn("不支持的节点类型: {}", nodeType);
+        return null;
+    }
+
+    // ============================================================
+    // 三种维度的查询路径
+    // ============================================================
+
+    /**
+     * type=2 区县:主表按 maintOrgId + 月份查最新,取 county 级指标
+     */
+    private StaticGridDashboardVO queryByCounty(String maintOrgId, YearMonth ym) {
+        StaticIndicatorResult result = getLatestByMaintOrgId(maintOrgId, ym);
         if (result == null) {
-            log.warn("无静态网架评估数据");
+            log.warn("区县无静态网架数据 maintOrgId={}, month={}", maintOrgId, ym);
             return null;
         }
-        return buildDashboardVO(result);
+        // county 级指标
+        List<StaticIndicatorMetric> countyMetrics = getMetrics(result.getId(), "county", null, null);
+        Map<String, String> metricMap = metricsToMap(countyMetrics);
+        // 停电时户数
+        BigDecimal outage = handleOutageDurationHoursHouseholds(maintOrgId, 2);
+        return buildDashboardVO(result, metricMap, outage);
     }
 
-    @Override
-    public StaticGridDashboardVO queryDashboardByBatchId(String batchId) {
-        if (StringUtils.isBlank(batchId)) {
-            return queryLatestDashboard();
+    /**
+     * type=3 变电站:先查该月所有主表批次,再按 sourceSubstationId 过滤 feeder 级指标,聚合
+     */
+    private StaticGridDashboardVO queryBySubstation(String substationId, YearMonth ym) {
+        List<Long> resultIds = getResultIdsByMonth(ym);
+        if (resultIds.isEmpty()) {
+            log.warn("月份无静态网架数据 month={}", ym);
+            return null;
         }
-        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
-                .eq(StaticIndicatorResult::getBatchId, batchId)
-                .last("LIMIT 1");
-        StaticIndicatorResult result = getOne(wrapper);
-        if (result == null) {
-            log.warn("无静态网架评估数据 batchId={}", batchId);
+        LambdaQueryWrapper<StaticIndicatorMetric> wrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                .in(StaticIndicatorMetric::getResultId, resultIds)
+                .eq(StaticIndicatorMetric::getLevel, "feeder")
+                .eq(StaticIndicatorMetric::getSourceSubstationId, substationId);
+        List<StaticIndicatorMetric> feederMetrics = metricMapper.selectList(wrapper);
+        if (feederMetrics.isEmpty()) {
+            log.warn("变电站无 feeder 级指标 substationId={}", substationId);
             return null;
         }
-        return buildDashboardVO(result);
+        Long resultId = feederMetrics.get(0).getResultId();
+        StaticIndicatorResult result = getById(resultId);
+        Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
+        BigDecimal outage = handleOutageDurationHoursHouseholds(substationId, 3);
+        return buildDashboardVO(result, metricMap, outage);
     }
 
     /**
-     * 构建仪表盘 VO
+     * type=4 馈线:按 feederId 查 feeder 级指标,聚合(单条馈线 → bool 转 0/1,rate 转 0%/100%)
      */
-    private StaticGridDashboardVO buildDashboardVO(StaticIndicatorResult result) {
-        Long resultId = result.getId();
+    private StaticGridDashboardVO queryByFeeder(String feederId, YearMonth ym) {
+        List<Long> resultIds = getResultIdsByMonth(ym);
+        if (resultIds.isEmpty()) {
+            log.warn("月份无静态网架数据 month={}", ym);
+            return null;
+        }
+        LambdaQueryWrapper<StaticIndicatorMetric> wrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                .in(StaticIndicatorMetric::getResultId, resultIds)
+                .eq(StaticIndicatorMetric::getLevel, "feeder")
+                .eq(StaticIndicatorMetric::getFeederId, feederId);
+        List<StaticIndicatorMetric> feederMetrics = metricMapper.selectList(wrapper);
+        if (feederMetrics.isEmpty()) {
+            log.warn("馈线无 feeder 级指标 feederId={}", feederId);
+            return null;
+        }
+        Long resultId = feederMetrics.get(0).getResultId();
+        StaticIndicatorResult result = getById(resultId);
+        Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
+        BigDecimal outage = handleOutageDurationHoursHouseholds(feederId, 4);
+        return buildDashboardVO(result, metricMap, outage);
+    }
 
-        // 查 county 级指标,按 metricCode 索引
-        LambdaQueryWrapper<StaticIndicatorMetric> metricWrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
-                .eq(StaticIndicatorMetric::getResultId, resultId)
-                .eq(StaticIndicatorMetric::getLevel, "county");
-        List<StaticIndicatorMetric> countyMetrics = metricMapper.selectList(metricWrapper);
-        Map<String, String> countyMetricMap = new HashMap<>();
-        for (StaticIndicatorMetric m : countyMetrics) {
-            countyMetricMap.put(m.getMetricCode(), m.getValue());
-        }
-
-        // 查问题详情
-        /*LambdaQueryWrapper<StaticProblemDetail> problemWrapper = new LambdaQueryWrapper<StaticProblemDetail>()
-                .eq(StaticProblemDetail::getResultId, resultId)
-                .orderByAsc(StaticProblemDetail::getProblemId);
-        List<StaticProblemDetail> problems = problemDetailMapper.selectList(problemWrapper);
-        List<StaticGridDashboardVO.ProblemDetailItem> problemItems = new ArrayList<StaticGridDashboardVO.ProblemDetailItem>();
-        for (StaticProblemDetail p : problems) {
-            problemItems.add(StaticGridDashboardVO.ProblemDetailItem.builder()
-                    .problemId(p.getProblemId())
-                    .feederId(p.getFeederId())
-                    .categoryName(p.getCategoryName())
-                    .metricName(p.getMetricName())
-                    .severity(p.getSeverity())
-                    .objectType(p.getObjectType())
-                    .actualValue(p.getActualValue())
-                    .description(p.getDescription())
-                    .suggestion(p.getSuggestion())
-                    .build());
-        }*/
+    // ============================================================
+    // 聚合逻辑:feeder 级指标 → county 级格式指标 Map
+    // ============================================================
+
+    /**
+     * 将 feeder 级指标列表按 metric_code 聚合,
+     * 输出与 county 级指标同 key 的 Map(供 buildDashboardVO 使用)
+     */
+    private Map<String, String> aggregateFeederMetrics(List<StaticIndicatorMetric> feederMetrics) {
+        // 按 metric_code 分组
+        Map<String, List<StaticIndicatorMetric>> byCode = feederMetrics.stream()
+                .collect(Collectors.groupingBy(StaticIndicatorMetric::getMetricCode));
+
+        // 馈线总数(去重 feederId)
+        long totalFeeders = feederMetrics.stream()
+                .map(StaticIndicatorMetric::getFeederId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .count();
+
+        Map<String, String> result = new HashMap<>();
+        result.put("feeder_total_count", String.valueOf(totalFeeders));
+
+        // 布尔计数类:count(true)
+        for (Map.Entry<String, String> entry : BOOL_COUNT_MAP.entrySet()) {
+            long count = countTrue(byCode.get(entry.getValue()));
+            result.put(entry.getKey(), String.valueOf(count));
+        }
+
+        // 布尔比率类:count(true)/total×100
+        for (Map.Entry<String, String> entry : BOOL_RATE_MAP.entrySet()) {
+            long trueCount = countTrue(byCode.get(entry.getValue()));
+            if (totalFeeders > 0) {
+                BigDecimal rate = new BigDecimal(trueCount)
+                        .divide(new BigDecimal(totalFeeders), 6, RoundingMode.HALF_UP)
+                        .multiply(new BigDecimal("100"))
+                        .setScale(2, RoundingMode.HALF_UP);
+                result.put(entry.getKey(), rate.toPlainString());
+            }
+        }
+
+        // 整数求和类
+        for (Map.Entry<String, String> entry : INT_SUM_MAP.entrySet()) {
+            int sum = sumInt(byCode.get(entry.getValue()));
+            result.put(entry.getKey(), String.valueOf(sum));
+        }
+
+        // 特殊:tied_feeder_total_count = count(has_tie=true)
+        result.put("tied_feeder_total_count", String.valueOf(countTrue(byCode.get("has_tie"))));
+
+        // 特殊:standard_wiring_feeder_count = count(is_standard_wiring=true)
+        result.put("standard_wiring_feeder_count",
+                String.valueOf(countTrue(byCode.get("is_standard_wiring"))));
+
+        // 特殊:connected_capacity_anomaly_count = 超标准条数 + 负荷组异常数
+        int capacityAnomaly = Integer.parseInt(result.getOrDefault("connected_capacity_exceed_feeder_count", "0"))
+                + Integer.parseInt(result.getOrDefault("load_group_capacity_anomaly_count", "0"));
+        result.put("connected_capacity_anomaly_count", String.valueOf(capacityAnomaly));
 
+        return result;
+    }
+
+    private long countTrue(List<StaticIndicatorMetric> metrics) {
+        if (metrics == null) return 0;
+        return metrics.stream()
+                .filter(m -> "true".equalsIgnoreCase(m.getValue()))
+                .count();
+    }
+
+    private int sumInt(List<StaticIndicatorMetric> metrics) {
+        if (metrics == null) return 0;
+        return metrics.stream()
+                .filter(m -> StringUtils.isNotBlank(m.getValue()))
+                .mapToInt(m -> {
+                    try {
+                        return new BigDecimal(m.getValue()).intValue();
+                    } catch (NumberFormatException e) {
+                        return 0;
+                    }
+                })
+                .sum();
+    }
+
+    // ============================================================
+    // 构建 VO(type=2 和 type=3/4 共用)
+    // ============================================================
+
+    private StaticGridDashboardVO buildDashboardVO(StaticIndicatorResult result,
+                                                    Map<String, String> metricMap,
+                                                    BigDecimal outageDuration) {
         return StaticGridDashboardVO.builder()
-                .resultId(resultId)
+                .resultId(result.getId())
                 .batchId(result.getBatchId())
                 .countyId(result.getCountyId())
                 .countySid(result.getCountySid())
@@ -121,62 +294,142 @@ public class StaticGridDashboardServiceImpl
                 .dataQualityStatus(result.getDataQualityStatus())
                 .dataQualityCompletenessRate(result.getDataQualityCompletenessRate())
                 // 一、标准化网架
-                .wiringStandardizationRate(toBigDecimal(countyMetricMap.get("wiring_standardization_rate")))
-                .mainTrunkSectionStandardizationRate(toBigDecimal(countyMetricMap.get("main_trunk_section_standardization_rate")))
+                .wiringStandardizationRate(toBigDecimal(metricMap.get("wiring_standardization_rate")))
+                .mainTrunkSectionStandardizationRate(toBigDecimal(metricMap.get("main_trunk_section_standardization_rate")))
                 // 二、供电可靠性
-                .outageDurationHoursHouseholds(handleOutageDurationHoursHouseholds("BD69707EA19E4FED85C595AFB5D06720","BA6481B8F34143FDA7E1C35A76846911"))  // 暂时写死:洪湖7.1—现在的停电时户数
-                .lineNMinus1PassRate(toBigDecimal(countyMetricMap.get("n_minus_1_pass_rate")))
-                .interSubstationNoConnectionCount(toInteger(countyMetricMap.get("inter_substation_interconnection_rate")))
-                .sameBusInterconnectionIssueCount(toInteger(countyMetricMap.get("same_bus_interconnection_issue_count")))
-                .excessiveTiePointFeederCount(toInteger(countyMetricMap.get("excessive_tie_point_feeder_count")))
-                .singleRadialFeederCount(toInteger(countyMetricMap.get("single_radial_feeder_count")))
-                .largeBranchFeederCount(toInteger(countyMetricMap.get("large_branch_issue_feeder_count")))
-                .unreasonableSegmentCount(toInteger(countyMetricMap.get("unreasonable_segment_count_feeder_count")))
-                .mainTrunkSegmentDenseCount(toInteger(countyMetricMap.get("main_trunk_lantern_issue_feeder_count")))
+                .outageDurationHoursHouseholds(outageDuration)
+                .lineNMinus1PassRate(toBigDecimal(metricMap.get("n_minus_1_pass_rate")))
+                .interSubstationNoConnectionCount(calcInterSubstationNoConnectionCount(
+                        metricMap.get("feeder_total_count"),
+                        metricMap.get("inter_substation_interconnection_rate")))
+                .sameBusInterconnectionIssueCount(toInteger(metricMap.get("same_bus_interconnection_issue_count")))
+                .excessiveTiePointFeederCount(toInteger(metricMap.get("excessive_tie_point_feeder_count")))
+                .singleRadialFeederCount(toInteger(metricMap.get("single_radial_feeder_count")))
+                .largeBranchFeederCount(toInteger(metricMap.get("large_branch_issue_feeder_count")))
+                .unreasonableSegmentCount(toInteger(metricMap.get("unreasonable_segment_count_feeder_count")))
+                .mainTrunkSegmentDenseCount(toInteger(metricMap.get("main_trunk_lantern_issue_feeder_count")))
                 // 三、电压质量
                 .iecUserVoltageIndex(null)  // 预留
-                .supplyRadiusExceedFeederCount(toInteger(countyMetricMap.get("supply_radius_exceed_feeder_count")))
-                .extraLongFeederCount(toInteger(countyMetricMap.get("extra_long_feeder_count")))
+                .supplyRadiusExceedFeederCount(toInteger(metricMap.get("supply_radius_exceed_feeder_count")))
+                .extraLongFeederCount(toInteger(metricMap.get("extra_long_feeder_count")))
                 // 四、供电能力
-                .regionalMaxLoadRate(null)  // 预留:运行态指标
-                .regionalAvgLoadRate(null)  // 预留:运行态指标
-                .lineOverloadCount(null)  // 预留:运行态指标
-                .mainTrunkSectionMismatchCount(toInteger(countyMetricMap.get("main_trunk_bottleneck_feeder_count")))
-                .connectedCapacityExceedFeederCount(toInteger(countyMetricMap.get("connected_capacity_exceed_feeder_count")))
-                .loadGroupCapacityAnomalyCount(toInteger(countyMetricMap.get("load_group_capacity_anomaly_count")))
-                .comprehensiveLossRate(null)  // 预留:运行态指标
-                .powerFactorPassRate(null)  // 预留:运行态指标
-                // 问题详情
-                //.problemDetails(problemItems)
+                .regionalMaxLoadRate(null)
+                .regionalAvgLoadRate(null)
+                .lineOverloadCount(null)
+                .mainTrunkSectionMismatchCount(toInteger(metricMap.get("main_trunk_bottleneck_feeder_count")))
+                .connectedCapacityExceedFeederCount(toInteger(metricMap.get("connected_capacity_exceed_feeder_count")))
+                .loadGroupCapacityAnomalyCount(toInteger(metricMap.get("load_group_capacity_anomaly_count")))
+                .comprehensiveLossRate(null)
+                .powerFactorPassRate(null)
                 .build();
     }
 
-    //处理停电时户数
-    public BigDecimal handleOutageDurationHoursHouseholds(String cityId, String maintOrgId){
-        List<FeederGroupTreeNode> feederGroupTree = feederGroupApplication.getFeederGroupTree();
-        FeederGroupTreeNode type1 = findNodeRecursively(feederGroupTree,1,cityId);
-        if (type1 == null) {
-            return null;
+    // ============================================================
+    // 停电时户数 — 改造为 (id, type)
+    // ============================================================
+
+    /**
+     * 根据节点类型和ID,从馈线组树收集馈线名列表,查询停电时户数
+     *
+     * @param id   节点设备ID
+     * @param type 节点类型 2=区县 3=变电站 4=馈线
+     * @return 停电时户数
+     */
+    public BigDecimal handleOutageDurationHoursHouseholds(String id, int type) {
+        List<FeederGroupTreeNode> tree = feederGroupApplication.getFeederGroupTree();
+        List<String> feederNames = new ArrayList<>();
+
+        if (type == 4) {
+            // 馈线:直接找该节点取 name
+            FeederGroupTreeNode node = findNodeRecursively(tree, 4, id);
+            if (node != null) {
+                feederNames.add(node.getName());
+            }
+        } else {
+            // 区县/变电站:找该节点 → 递归收集子树所有 type=4 的馈线名
+            FeederGroupTreeNode node = findNodeRecursively(tree, type, id);
+            if (node != null && node.getChildren() != null) {
+                collectNodesByType(node.getChildren(), feederNames);
+            }
         }
-        FeederGroupTreeNode type2 = findNodeRecursively(feederGroupTree,2,maintOrgId);
-        if (type2 == null) {
+
+        if (feederNames.isEmpty()) {
+            log.warn("未找到馈线名列表 id={}, type={}", id, type);
             return null;
         }
-        List<String> type4Result = new ArrayList<>();
-        collectNodesByType(type2.getChildren(),type4Result);
-        return pwwyyqxtPrmPdrOutageMvDsService.selectStaticOutageDurationHoursHouseholds(type4Result);
+        return pwwyyqxtPrmPdrOutageMvDsService.selectStaticOutageDurationHoursHouseholds(feederNames);
     }
 
+    // ============================================================
+    // 主表查询辅助
+    // ============================================================
+
+    /**
+     * 区县维度:按 maintOrgId + 月份查最新一条主表记录
+     */
+    private StaticIndicatorResult getLatestByMaintOrgId(String maintOrgId, YearMonth ym) {
+        LocalDate monthStart = ym.atDay(1);
+        LocalDate monthEnd = ym.atEndOfMonth();
+        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
+                .eq(StaticIndicatorResult::getMaintOrgId, maintOrgId)
+                .ge(StaticIndicatorResult::getCreateTime, monthStart.atStartOfDay())
+                .le(StaticIndicatorResult::getCreateTime, monthEnd.atTime(23, 59, 59))
+                .orderByDesc(StaticIndicatorResult::getCreateTime)
+                .last("LIMIT 1");
+        return getOne(wrapper);
+    }
 
+    /**
+     * 获取该月所有主表批次ID(type=3/4 用——先拿到月份范围内的 result_id 再查 metric)
+     */
+    private List<Long> getResultIdsByMonth(YearMonth ym) {
+        LocalDate monthStart = ym.atDay(1);
+        LocalDate monthEnd = ym.atEndOfMonth();
+        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
+                .ge(StaticIndicatorResult::getCreateTime, monthStart.atStartOfDay())
+                .le(StaticIndicatorResult::getCreateTime, monthEnd.atTime(23, 59, 59));
+        List<StaticIndicatorResult> results = list(wrapper);
+        return results.stream()
+                .map(StaticIndicatorResult::getId)
+                .collect(Collectors.toList());
+    }
 
-    //递归树
-    private FeederGroupTreeNode findNodeRecursively(List<FeederGroupTreeNode> nodes,int nodeType, String targetId){
+    /**
+     * 查 county 级或 feeder 级指标
+     */
+    private List<StaticIndicatorMetric> getMetrics(Long resultId, String level,
+                                                    String feederId, String substationId) {
+        LambdaQueryWrapper<StaticIndicatorMetric> wrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                .eq(StaticIndicatorMetric::getResultId, resultId)
+                .eq(StaticIndicatorMetric::getLevel, level);
+        if (feederId != null) {
+            wrapper.eq(StaticIndicatorMetric::getFeederId, feederId);
+        }
+        if (substationId != null) {
+            wrapper.eq(StaticIndicatorMetric::getSourceSubstationId, substationId);
+        }
+        return metricMapper.selectList(wrapper);
+    }
+
+    private Map<String, String> metricsToMap(List<StaticIndicatorMetric> metrics) {
+        Map<String, String> map = new HashMap<>();
+        for (StaticIndicatorMetric m : metrics) {
+            map.put(m.getMetricCode(), m.getValue());
+        }
+        return map;
+    }
+
+    // ============================================================
+    // 树递归辅助
+    // ============================================================
+
+    private FeederGroupTreeNode findNodeRecursively(List<FeederGroupTreeNode> nodes, int nodeType, String targetId) {
         if (nodes == null || nodes.isEmpty()) return null;
         for (FeederGroupTreeNode node : nodes) {
-            if (node.getType() == nodeType && node.getId().equals(targetId)){
+            if (node.getType() == nodeType && targetId.equals(node.getId())) {
                 return node;
             }
-            if (node.getChildren() != null && !node.getChildren().isEmpty()){
+            if (node.getChildren() != null && !node.getChildren().isEmpty()) {
                 FeederGroupTreeNode found = findNodeRecursively(node.getChildren(), nodeType, targetId);
                 if (found != null) return found;
             }
@@ -184,22 +437,56 @@ public class StaticGridDashboardServiceImpl
         return null;
     }
 
-    //收集树线路节点
     private void collectNodesByType(List<FeederGroupTreeNode> nodes, List<String> collector) {
         if (nodes == null || nodes.isEmpty()) return;
         for (FeederGroupTreeNode node : nodes) {
             if (node.getType() == 4) {
                 collector.add(node.getName());
-            } else if (node.getChildren()!= null && !node.getChildren().isEmpty()) {
-                collectNodesByType(node.getChildren(),collector);
+            } else if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+                collectNodesByType(node.getChildren(), collector);
             }
         }
     }
 
-    private BigDecimal toBigDecimal(String value) {
-        if (StringUtils.isBlank(value)) {
+    // ============================================================
+    // 工具方法
+    // ============================================================
+
+    /**
+     * 解析 daytime(如 "2026-7-16")为 YearMonth
+     */
+    private YearMonth parseYearMonth(String daytime) {
+        if (StringUtils.isBlank(daytime)) {
+            return YearMonth.now();
+        }
+        try {
+            LocalDate date = LocalDate.parse(daytime, DateTimeFormatter.ofPattern("yyyy-M-d"));
+            return YearMonth.from(date);
+        } catch (DateTimeParseException e) {
+            log.warn("daytime 格式非法: {}, 使用当前月份", daytime);
+            return YearMonth.now();
+        }
+    }
+
+    /**
+     * 站间无联络线路数 = 馈线总数 × (1 - 站间联络率/100)
+     */
+    private Integer calcInterSubstationNoConnectionCount(String feederTotalCountStr, String interSubstationRateStr) {
+        Integer total = toInteger(feederTotalCountStr);
+        if (total == null || total == 0) return null;
+        if (StringUtils.isBlank(interSubstationRateStr)) return total;
+        try {
+            BigDecimal rate = new BigDecimal(interSubstationRateStr);
+            BigDecimal noConnRatio = BigDecimal.ONE.subtract(
+                    rate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
+            return new BigDecimal(total).multiply(noConnRatio).setScale(0, RoundingMode.HALF_UP).intValue();
+        } catch (NumberFormatException e) {
             return null;
         }
+    }
+
+    private BigDecimal toBigDecimal(String value) {
+        if (StringUtils.isBlank(value)) return null;
         try {
             return new BigDecimal(value);
         } catch (NumberFormatException e) {
@@ -208,9 +495,7 @@ public class StaticGridDashboardServiceImpl
     }
 
     private Integer toInteger(String value) {
-        if (StringUtils.isBlank(value)) {
-            return null;
-        }
+        if (StringUtils.isBlank(value)) return null;
         try {
             return Integer.valueOf(value);
         } catch (NumberFormatException e) {
@@ -221,4 +506,4 @@ public class StaticGridDashboardServiceImpl
             }
         }
     }
-}
+}

+ 3 - 3
services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/algorithm/AlgorithmController.java

@@ -37,14 +37,14 @@ public class AlgorithmController extends BaseController {
     /**
      * 查询静态网架评估结果并入库
      * <p>
-     * 入参:batchId(批次ID)、countyId(区县ID)
+     * 入参:batchId(批次ID)、countyId(区县ID,即运维单位ID maintOrgId
      *
      * @return 主表ID
      */
     @PostMapping(value = "/static-grid")
     public ApiResponse<Long> queryStaticGridIndicators(@RequestBody Map<String, Object> params) {
         String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
-        Integer countyId = params.get("countyId") != null ? Integer.valueOf(String.valueOf(params.get("countyId"))) : null;
+        String countyId = params.get("countyId") != null ? String.valueOf(params.get("countyId")) : null;
         return data(algorithmService.queryStaticGridIndicators(batchId, countyId));
     }
-}
+}

+ 5 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/entity/staticgrid/StaticIndicatorMetric.java

@@ -51,6 +51,11 @@ public class StaticIndicatorMetric implements Serializable {
      */
     private String feederName;
 
+    /**
+     * 源变电站ID(level=county 时为 null)
+     */
+    private String sourceSubstationId;
+
     /**
      * 源变电站名称(level=county 时为 null)
      */

+ 7 - 2
services/load-transfer-si/src/main/java/com/hdkj/lt/si/entity/staticgrid/StaticIndicatorResult.java

@@ -38,9 +38,14 @@ public class StaticIndicatorResult implements Serializable {
     private String batchId;
 
     /**
-     * 区县ID
+     * 区县ID(即运维单位ID maintOrgId)
      */
-    private Integer countyId;
+    private String countyId;
+
+    /**
+     * 运维单位ID(跟 countyId 同值,冗余便于树节点直接关联)
+     */
+    private String maintOrgId;
 
     /**
      * 区县短标识

+ 3 - 3
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/AlgorithmService.java

@@ -18,8 +18,8 @@ public interface AlgorithmService {
      * 查询静态网架评估结果并入库
      *
      * @param batchId  批次ID
-     * @param countyId 区县ID
+     * @param countyId 区县ID(即运维单位ID maintOrgId)
      * @return 主表ID
      */
-    Long queryStaticGridIndicators(String batchId, Integer countyId);
-}
+    Long queryStaticGridIndicators(String batchId, String countyId);
+}

+ 2 - 2
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/impl/AlgorithmServiceImpl.java

@@ -29,7 +29,7 @@ public class AlgorithmServiceImpl implements AlgorithmService {
     }
 
     @Override
-    public Long queryStaticGridIndicators(String batchId, Integer countyId) {
+    public Long queryStaticGridIndicators(String batchId, String countyId) {
         return staticIndicatorResultService.saveStaticGridResult(batchId, countyId);
     }
-}
+}

+ 2 - 2
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/StaticIndicatorResultService.java

@@ -15,8 +15,8 @@ public interface StaticIndicatorResultService extends IService<StaticIndicatorRe
      * 调用算法并解析入库
      *
      * @param batchId  批次ID
-     * @param countyId 区县ID
+     * @param countyId 区县ID(即运维单位ID maintOrgId)
      * @return 主表ID
      */
-    Long saveStaticGridResult(String batchId, Integer countyId);
+    Long saveStaticGridResult(String batchId, String countyId);
 }

+ 10 - 7
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/impl/StaticIndicatorResultServiceImpl.java

@@ -21,7 +21,6 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,7 +52,7 @@ public class StaticIndicatorResultServiceImpl
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long saveStaticGridResult(String batchId, Integer countyId) {
+    public Long saveStaticGridResult(String batchId, String countyId) {
         // 1. 调算法
         String rawResponse = algorithmRemoteCallService.queryStaticGridIndicators();
         if (StringUtils.isBlank(rawResponse)) {
@@ -88,6 +87,7 @@ public class StaticIndicatorResultServiceImpl
         StaticIndicatorResult mainRecord = StaticIndicatorResult.builder()
                 .batchId(batchId)
                 .countyId(countyId)
+                .maintOrgId(countyId)
                 .countySid(countySid)
                 .countyName(countyName)
                 .feederIds(feederIds)
@@ -111,7 +111,7 @@ public class StaticIndicatorResultServiceImpl
             if (categories != null) {
                 for (int i = 0; i < categories.size(); i++) {
                     JSONObject category = categories.getJSONObject(i);
-                    parseCategoryMetrics(category, resultId, "county", null, null, null, null, null, metricList);
+                    parseCategoryMetrics(category, resultId, "county", null, null, null, null, null, null, metricList);
                 }
             }
         }
@@ -124,6 +124,7 @@ public class StaticIndicatorResultServiceImpl
                     JSONObject feeder = feederResults.getJSONObject(i);
                     String feederId = feeder.getString("feeder_id");
                     String feederName = feeder.getString("feeder_name");
+                    String sourceSubstationId = feeder.getString("source_substation_id");
                     String sourceSubstationName = feeder.getString("source_substation_name");
                     String sourceBusId = feeder.getString("source_bus_id");
                     String supplyAreaType = feeder.getString("supply_area_type");
@@ -132,7 +133,8 @@ public class StaticIndicatorResultServiceImpl
                         for (int j = 0; j < feederCategories.size(); j++) {
                             JSONObject category = feederCategories.getJSONObject(j);
                             parseCategoryMetrics(category, resultId, "feeder", feederId,
-                                    feederName, sourceSubstationName, sourceBusId, supplyAreaType,
+                                    feederName, sourceSubstationId, sourceSubstationName,
+                                    sourceBusId, supplyAreaType,
                                     metricList);
                         }
                     }
@@ -186,8 +188,8 @@ public class StaticIndicatorResultServiceImpl
      */
     private void parseCategoryMetrics(JSONObject category, Long resultId, String level,
                                        String feederId, String feederName,
-                                       String sourceSubstationName, String sourceBusId,
-                                       String supplyAreaType,
+                                       String sourceSubstationId, String sourceSubstationName,
+                                       String sourceBusId, String supplyAreaType,
                                        List<StaticIndicatorMetric> metricList) {
         String categoryCode = category.getString("category_code");
         String categoryName = category.getString("category_name");
@@ -205,6 +207,7 @@ public class StaticIndicatorResultServiceImpl
                     .level(level)
                     .feederId(feederId)
                     .feederName(feederName)
+                    .sourceSubstationId(sourceSubstationId)
                     .sourceSubstationName(sourceSubstationName)
                     .sourceBusId(sourceBusId)
                     .supplyAreaType(supplyAreaType)
@@ -222,4 +225,4 @@ public class StaticIndicatorResultServiceImpl
             metricList.add(metric);
         }
     }
-}
+}