liuaini пре 1 недеља
родитељ
комит
867569b975

+ 194 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/constants/StaticGridMetricTypeEnum.java

@@ -0,0 +1,194 @@
+package com.hdkj.lt.bf.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 静态网架指标详情 metricType 枚举
+ * <p>
+ * 入参 metricType(接口文档「三、指标类型字典」取值) -> 主表/明细表中的 metric_code 映射。
+ * 每个枚举项承载该指标的:主指标 metric_code(用于筛选问题馈线 + 作为 reason/deviceIds 的来源)、
+ * 筛选条件、是否展示问题详情、以及 details 各字段的取数规约(FieldSpec)。
+ * <p>
+ * 取数规约遵循「表优先,表里没有的字段才取 raw_response」:
+ * <ul>
+ *   <li>{@link DetailSource#METRIC_VALUE} / {@link DetailSource#METRIC_UNIT}:从 t_static_indicator_metric 取对应 metric_code 的 value/unit</li>
+ *   <li>{@link DetailSource#REASON}:从 t_static_problem_detail.description 取(按主指标 metric_code 关联)</li>
+ *   <li>{@link DetailSource#RAW_LEAF} / {@link DetailSource#RAW_BRANCH_FIRST} / {@link DetailSource#RAW_LOAD_GROUPS}:
+ *       表结构中没有的字段,从 t_static_indicator_result.raw_response 中主指标 details[0] 取</li>
+ * </ul>
+ *
+ * @author liuaini
+ * @since 2026-08-06
+ */
+@Getter
+public enum StaticGridMetricTypeEnum {
+
+    /** 1. 线路 N-1 通过率:取「线路N-1是否通过」value=false 的线路;estimated_load_mva 等保留三位小数 */
+    N1_FAIL("n1Fail", "线路N-1通过率", "n_minus_1_passed", FilterType.VALUE_FALSE, true, Arrays.asList(
+            new FieldSpec("estimatedLoadMva", DetailSource.RAW_LEAF, "estimated_load_mva", 3),
+            new FieldSpec("requiredBackupWithMarginMva", DetailSource.RAW_LEAF, "required_backup_with_margin_mva", 3),
+            new FieldSpec("availableTargetSpareMva", DetailSource.RAW_LEAF, "available_target_spare_mva", 3),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 2. 站间无联络线路数:取「是否存在站间联络」value=false 的线路;不展示问题详情 */
+    NO_CONNECTION("noConnection", "站间无联络线路数", "has_inter_substation_tie", FilterType.VALUE_FALSE, false, Collections.emptyList()),
+
+    /** 3. 馈线同母线联络:取「是否存在同母线联络问题」value=true 的线路;只展示 reason */
+    SAME_BUS_INTERCONNECTION("sameBusInterconnection", "馈线同母线联络", "has_same_bus_interconnection_issue", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 4. 联络点过多线路数:取「联络点是否过多」value=true 的线路;tie_point_count 取指标表「联络点数量」 */
+    EXCESSIVE_TIE_POINT("excessiveTiePoint", "联络点过多线路数", "has_excessive_tie_points", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("tiePointCount", DetailSource.METRIC_VALUE, "tie_point_count", -1),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 5. 无联络/单辐射线路数:取「是否为单辐射馈线」value=true 的线路;不展示问题详情 */
+    SINGLE_RADIAL("singleRadial", "无联络/单辐射线路数", "is_single_radial_feeder", FilterType.VALUE_TRUE, false, Collections.emptyList()),
+
+    /** 6. 存在大分支馈线数:取「是否存在大分支问题」value=true 的线路;取 details[0].branches[0] 的分支信息 */
+    LARGE_BRANCH("largeBranch", "存在大分支馈线数", "has_large_branch_issue", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("branchId", DetailSource.RAW_BRANCH_FIRST, "branch_id", -1),
+            new FieldSpec("deviceCount", DetailSource.RAW_BRANCH_FIRST, "device_count", -1),
+            new FieldSpec("transformerCount", DetailSource.RAW_BRANCH_FIRST, "transformer_count", -1)
+    )),
+
+    /** 7. 分段数不合理线路数:取「分段数是否不合理」value=true;value 取指标表「主干线分段数」 */
+    UNREASONABLE_SEGMENT("unreasonableSegment", "分段数不合理线路数", "has_unreasonable_segment_count", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("value", DetailSource.METRIC_VALUE, "segment_count", -1),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 8. 主干线分段配变密集数:取「是否存在主干挂灯笼问题」value=true;value 取指标表「主干线分段数」 */
+    DENSE_SEGMENT("denseSegment", "主干线分段配变密集数", "has_main_trunk_lantern_issue", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("value", DetailSource.METRIC_VALUE, "segment_count", -1),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 9. 供电半径过长:取「供电半径是否超标」value=true;supply_radius_km 取指标表「供电半径」 */
+    SUPPLY_RADIUS_EXCEED("supplyRadiusExceed", "供电半径过长", "is_supply_radius_exceeded", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("supplyRadiusKm", DetailSource.METRIC_VALUE, "supply_radius_km", -1),
+            new FieldSpec("unit", DetailSource.METRIC_UNIT, "supply_radius_km", -1),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 10. 超长馈线:取「是否为超长馈线」value=true;value/unit 取指标表「馈线总长度」 */
+    EXTRA_LONG_FEEDER("extraLongFeeder", "超长馈线", "is_extra_long_feeder", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("value", DetailSource.METRIC_VALUE, "total_length_km", -1),
+            new FieldSpec("unit", DetailSource.METRIC_UNIT, "total_length_km", -1)
+    )),
+
+    /** 11. 主干线截面不匹配:取「是否存在主干卡脖子」value=true;估算负载/额定容量/负载率取 raw_response details */
+    TRUNK_MISMATCH("trunkMismatch", "主干线截面不匹配", "has_main_trunk_bottleneck", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("estimatedLoadMva", DetailSource.RAW_LEAF, "estimated_load_mva", -1),
+            new FieldSpec("thermalCapacityMva", DetailSource.RAW_LEAF, "thermal_capacity_mva", -1),
+            new FieldSpec("loadRatePercent", DetailSource.RAW_LEAF, "load_rate_percent", -1),
+            new FieldSpec("reason", DetailSource.REASON, null, -1)
+    )),
+
+    /** 12. 线路装接容量超标准:取「装接容量是否超标准」value=true;容量/台数取指标表对应指标 */
+    CAPACITY_EXCEED("capacityExceed", "线路装接容量超标准", "is_connected_capacity_exceeded", FilterType.VALUE_TRUE, true, Arrays.asList(
+            new FieldSpec("capacityMva", DetailSource.METRIC_VALUE, "connected_transformer_capacity_mva", -1),
+            new FieldSpec("capacityMvaUnit", DetailSource.METRIC_UNIT, "connected_transformer_capacity_mva", -1),
+            new FieldSpec("transformerCount", DetailSource.METRIC_VALUE, "connected_transformer_count", -1),
+            new FieldSpec("transformerCountUnit", DetailSource.METRIC_UNIT, "connected_transformer_count", -1)
+    )),
+
+    /** 13. 负荷组容量异常:取「负荷组容量异常数」is_problem=true;value 取该指标值,load_groups 取 raw_response details */
+    LOAD_GROUP_ANOMALY("loadGroupAnomaly", "负荷组容量异常", "load_group_capacity_anomaly_count", FilterType.IS_PROBLEM_TRUE, true, Arrays.asList(
+            new FieldSpec("value", DetailSource.METRIC_VALUE, "load_group_capacity_anomaly_count", -1),
+            new FieldSpec("unit", DetailSource.METRIC_UNIT, "load_group_capacity_anomaly_count", -1),
+            new FieldSpec("loadGroups", DetailSource.RAW_LOAD_GROUPS, null, -1)
+    ));
+
+    /** 接口入参 metricType 取值(如 n1Fail) */
+    private final String metricType;
+    /** 指标中文名(对应 metric_name 的语义,仅作展示) */
+    private final String displayName;
+    /** 主指标 metric_code:用于筛选问题馈线,并作为 reason/deviceIds 的关联键 */
+    private final String primaryMetricCode;
+    /** 馈线筛选条件 */
+    private final FilterType filterType;
+    /** 是否展示问题详情(noConnection/singleRadial 为 false -> details 固定为 {}) */
+    private final boolean showDetails;
+    /** details 字段取数规约 */
+    private final List<FieldSpec> detailFields;
+
+    StaticGridMetricTypeEnum(String metricType, String displayName, String primaryMetricCode,
+                             FilterType filterType, boolean showDetails, List<FieldSpec> detailFields) {
+        this.metricType = metricType;
+        this.displayName = displayName;
+        this.primaryMetricCode = primaryMetricCode;
+        this.filterType = filterType;
+        this.showDetails = showDetails;
+        this.detailFields = detailFields;
+    }
+
+    /**
+     * 根据 metricType 取枚举项,未匹配返回 null
+     */
+    public static StaticGridMetricTypeEnum fromMetricType(String metricType) {
+        if (metricType == null) {
+            return null;
+        }
+        for (StaticGridMetricTypeEnum e : values()) {
+            if (e.metricType.equals(metricType)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 馈线筛选条件类型
+     */
+    public enum FilterType {
+        /** value = "true" */
+        VALUE_TRUE,
+        /** value = "false" */
+        VALUE_FALSE,
+        /** is_problem = 1 */
+        IS_PROBLEM_TRUE
+    }
+
+    /**
+     * details 字段取数来源
+     */
+    public enum DetailSource {
+        /** 指标表 value(ref = metric_code) */
+        METRIC_VALUE,
+        /** 指标表 unit(ref = metric_code) */
+        METRIC_UNIT,
+        /** 问题详情表 description(主指标) */
+        REASON,
+        /** raw_response 主指标 details[0] 叶子字段(ref = json key) */
+        RAW_LEAF,
+        /** raw_response 主指标 details[0].branches[0] 字段(ref = branch 内 key) */
+        RAW_BRANCH_FIRST,
+        /** raw_response 主指标 details[0].load_groups 数组 */
+        RAW_LOAD_GROUPS
+    }
+
+    /**
+     * details 单字段取数规约
+     */
+    @Getter
+    @AllArgsConstructor
+    public static class FieldSpec {
+        /** 输出字段名 */
+        private final String outputKey;
+        /** 取数来源 */
+        private final DetailSource source;
+        /** metric_code 或 raw_response json key(REASON/RAW_LOAD_GROUPS 时为 null) */
+        private final String ref;
+        /** 数值保留小数位:-1 不处理;>=0 按 HALF_UP 保留 n 位(仅对 RAW_LEAF 数值生效) */
+        private final int scale;
+    }
+}

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

@@ -1,8 +1,11 @@
 package com.hdkj.lt.bf.controller.optimization;
 
 import com.hdkj.hussar.ApiResponse;
+import com.hdkj.lt.bf.entity.dto.StaticGridDetailQueryDTO;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
+import com.hdkj.lt.bf.entity.vo.StaticGridDetailResultVO;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
+import com.hdkj.lt.bf.service.staticgrid.StaticGridDetailService;
 import com.hdkj.lt.core.mvc.BaseController;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -27,6 +30,7 @@ import java.util.Map;
 public class StaticGridController extends BaseController {
 
     private final StaticGridDashboardService staticGridDashboardService;
+    private final StaticGridDetailService staticGridDetailService;
 
     /**
      * 查询静态网架评估仪表盘
@@ -46,4 +50,24 @@ public class StaticGridController extends BaseController {
         StaticGridDashboardVO vo = staticGridDashboardService.queryLatestDashboard(id, type);
         return ApiResponse.success(vo);
     }
+
+    /**
+     * 查询静态网架指标详情
+     * <p>
+     * 入参:
+     * <ul>
+     *   <li>type - 统计维度:2 区县、3 变电站</li>
+     *   <li>id - 区县下取 countyId(即 maintOrgId),变电站下取 sourceSubstationId</li>
+     *   <li>metricType - 指标类型(n1Fail/noConnection/sameBusInterconnection/...)</li>
+     * </ul>
+     * 固定返回 13 个指标类型 key,仅入参 metricType 对应数组填充问题馈线列表,其余为空数组。
+     *
+     * @param query 查询入参
+     * @return 指标类型 -> 馈线详情列表
+     */
+    @PostMapping("/detail")
+    public ApiResponse<StaticGridDetailResultVO> detail(@RequestBody StaticGridDetailQueryDTO query) {
+        StaticGridDetailResultVO data = staticGridDetailService.queryDetail(query);
+        return ApiResponse.success(data);
+    }
 }

+ 29 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/StaticGridDetailQueryDTO.java

@@ -0,0 +1,29 @@
+package com.hdkj.lt.bf.entity.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 静态网架指标详情 请求 DTO
+ *
+ * @author liuaini
+ * @since 2026-08-06
+ */
+@Data
+@ApiModel(description = "静态网架指标详情请求参数")
+public class StaticGridDetailQueryDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "统计维度:2=区县;3=变电站", required = true)
+    private Integer type;
+
+    @ApiModelProperty(value = "资源ID:type=2 传区县ID(county_id);type=3 传变电站ID(source_substation_id)", required = true)
+    private String id;
+
+    @ApiModelProperty(value = "指标类型,取值见指标类型字典(如 n1Fail)", required = true)
+    private String metricType;
+}

+ 1 - 19
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDetailItemVO.java

@@ -18,7 +18,7 @@ import java.util.Map;
  * data 中每个 metricType 数组的元素结构:{ feederId, feederName, details, fileList, deviceIds }。
  * details 为 Map(各指标字段不同;不展示详情的指标为 {})。
  *
- * @author lsl
+ * @author liuaini
  * @since 2026-08-06
  */
 @Data
@@ -39,24 +39,6 @@ public class StaticGridDetailItemVO implements Serializable {
     @ApiModelProperty(value = "问题详情,各指标字段不同;不展示详情的指标为 {}")
     private Map<String, Object> details;
 
-    @ApiModelProperty(value = "文件列表(保留字段,暂返回空数组)")
-    private List<FileType> fileList;
-
     @ApiModelProperty(value = "问题设备ID列表")
     private List<String> deviceIds;
-
-    @Data
-    @Accessors(chain = true)
-    public static class FileType {
-
-        private Long fileId;
-        /**
-         * 1-单线图
-         * 2-开关段图
-         * 3-负荷组图
-         * 4-一级联络图
-         * 5-接线组图
-         */
-        private String fileType;
-    }
 }

+ 69 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDetailResultVO.java

@@ -0,0 +1,69 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 静态网架指标详情返回结果。
+ * <p>原以 {@code Map<String, List<StaticGridDetailItemVO>>} 返回(key 为 metricType),
+ * 现固化为实体类,字段名与 metricType 取值一一对应,JSON 序列化后的 key 与原 Map 完全一致。</p>
+ * <p>任一请求只命中一个 metricType:命中的字段返回问题馈线列表,其余字段固定为空数组。</p>
+ *
+ * @author liuaini
+ * @since 2026-08-06
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(value = "StaticGridDetailResultVO", description = "静态网架指标详情返回结果")
+public class StaticGridDetailResultVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "线路N-1通过率 问题馈线列表")
+    private List<StaticGridDetailItemVO> n1Fail;
+
+    @ApiModelProperty(value = "站间无联络线路数 问题馈线列表")
+    private List<StaticGridDetailItemVO> noConnection;
+
+    @ApiModelProperty(value = "馈线同母线联络 问题馈线列表")
+    private List<StaticGridDetailItemVO> sameBusInterconnection;
+
+    @ApiModelProperty(value = "联络点过多线路数 问题馈线列表")
+    private List<StaticGridDetailItemVO> excessiveTiePoint;
+
+    @ApiModelProperty(value = "无联络/单辐射线路数 问题馈线列表")
+    private List<StaticGridDetailItemVO> singleRadial;
+
+    @ApiModelProperty(value = "存在大分支馈线数 问题馈线列表")
+    private List<StaticGridDetailItemVO> largeBranch;
+
+    @ApiModelProperty(value = "分段数不合理线路数 问题馈线列表")
+    private List<StaticGridDetailItemVO> unreasonableSegment;
+
+    @ApiModelProperty(value = "主干线分段配变密集数 问题馈线列表")
+    private List<StaticGridDetailItemVO> denseSegment;
+
+    @ApiModelProperty(value = "供电半径过长 问题馈线列表")
+    private List<StaticGridDetailItemVO> supplyRadiusExceed;
+
+    @ApiModelProperty(value = "超长馈线 问题馈线列表")
+    private List<StaticGridDetailItemVO> extraLongFeeder;
+
+    @ApiModelProperty(value = "主干线截面不匹配 问题馈线列表")
+    private List<StaticGridDetailItemVO> trunkMismatch;
+
+    @ApiModelProperty(value = "线路装接容量超标准 问题馈线列表")
+    private List<StaticGridDetailItemVO> capacityExceed;
+
+    @ApiModelProperty(value = "负荷组容量异常 问题馈线列表")
+    private List<StaticGridDetailItemVO> loadGroupAnomaly;
+}

+ 26 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/StaticGridDetailService.java

@@ -0,0 +1,26 @@
+package com.hdkj.lt.bf.service.staticgrid;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.hdkj.lt.bf.entity.dto.StaticGridDetailQueryDTO;
+import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
+import com.hdkj.lt.bf.entity.vo.StaticGridDetailResultVO;
+
+/**
+ * 静态网架指标详情服务
+ * <p>
+ * 按区县(type=2)或变电站(type=3)维度,查询指定指标(metricType)下的问题馈线明细。
+ * 出参固定返回全部 13 个指标字段,仅入参 metricType 对应的字段返回实际数据,其余为空数组。
+ *
+ * @author liuaini
+ * @since 2026-08-06
+ */
+public interface StaticGridDetailService extends IService<StaticIndicatorResult> {
+
+    /**
+     * 查询静态网架指标详情
+     *
+     * @param query type/id/metricType
+     * @return 13 个指标字段的 VO(仅 metricType 对应字段有数据,其余为空数组)
+     */
+    StaticGridDetailResultVO queryDetail(StaticGridDetailQueryDTO query);
+}

+ 559 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDetailServiceImpl.java

@@ -0,0 +1,559 @@
+package com.hdkj.lt.bf.service.staticgrid.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.hdkj.lt.bf.constants.OptimizationConstants;
+import com.hdkj.lt.bf.constants.StaticGridMetricTypeEnum;
+import com.hdkj.lt.bf.constants.StaticGridMetricTypeEnum.DetailSource;
+import com.hdkj.lt.bf.constants.StaticGridMetricTypeEnum.FieldSpec;
+import com.hdkj.lt.bf.entity.dto.StaticGridDetailQueryDTO;
+import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
+import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
+import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
+import com.hdkj.lt.bf.entity.vo.StaticGridDetailItemVO;
+import com.hdkj.lt.bf.entity.vo.StaticGridDetailResultVO;
+import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
+import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
+import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
+import com.hdkj.lt.bf.service.staticgrid.StaticGridDetailService;
+import com.hdkj.lt.utils.cache.RedisRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.YearMonth;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 静态网架指标详情服务实现
+ * 馈线列表来自 t_static_indicator_metric(level=feeder,按主指标 metric_code + 筛选条件)。
+ * details 取数遵循「表优先」:reason←t_static_problem_detail.description;value/unit←指标表;
+ * 表里没有的结构化字段←t_static_indicator_result.raw_response 主指标 details[0]。
+ * deviceIds←t_static_problem_detail.object_id(object_type=device),缺失则回退 raw_response details[0].device_ids。
+ *
+ * @author liuaini
+ * @since 2026-08-06
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StaticGridDetailServiceImpl
+        extends ServiceImpl<StaticIndicatorResultMapper, StaticIndicatorResult>
+        implements StaticGridDetailService {
+
+    private final StaticIndicatorMetricMapper metricMapper;
+    private final StaticProblemDetailMapper problemDetailMapper;
+    private final RedisRepository redisRepository;
+
+    @Override
+    public StaticGridDetailResultVO queryDetail(StaticGridDetailQueryDTO query) {
+        if (query == null || query.getType() == null
+                || StringUtils.isBlank(query.getId())
+                || StringUtils.isBlank(query.getMetricType())) {
+            log.warn("静态网架指标详情参数缺失: {}", query);
+            return emptyResult();
+        }
+
+        int type = query.getType();
+        if (type != 2 && type != 3) {
+            log.warn("不支持的统计维度 type={}", type);
+            return emptyResult();
+        }
+
+        StaticGridMetricTypeEnum metricEnum = StaticGridMetricTypeEnum.fromMetricType(query.getMetricType());
+        if (metricEnum == null) {
+            log.warn("不支持的 metricType={}", query.getMetricType());
+            return emptyResult();
+        }
+
+        YearMonth ym = YearMonth.now();
+//        String cacheKey = OptimizationConstants.CACHE_KEY_GRID_DETAIL_PREFIX
+//                + type + ":" + query.getId() + ":" + query.getMetricType() + ":" + ym;
+//        Object cached = redisRepository.get(cacheKey);
+//        if (cached instanceof StaticGridDetailResultVO) {
+//            return (StaticGridDetailResultVO) cached;
+//        }
+
+        //        redisRepository.setExpire(cacheKey, data,
+//                OptimizationConstants.CACHE_TTL_GRID_DETAIL_MINUTES, TimeUnit.MINUTES);
+        return doQuery(type, query.getId(), metricEnum, ym);
+    }
+
+    private StaticGridDetailResultVO doQuery(int type, String id,StaticGridMetricTypeEnum e, YearMonth ym) {
+        // 1. 定位当月最新批次结果
+        Long chosenResultId;
+        String rawResponse;
+        if (type == 2) {
+            StaticIndicatorResult result = getLatestByCountyId(id, ym);
+            if (result == null) {
+                log.warn("区县当月无静态网架数据 countyId={}, month={}", id, ym);
+                return emptyResult();
+            }
+            chosenResultId = result.getId();
+            rawResponse = result.getRawResponse();
+        } else {
+            List<StaticIndicatorResult> monthResults = listByMonth(ym);
+            if (monthResults.isEmpty()) {
+                log.warn("当月无静态网架数据 month={}", ym);
+                return emptyResult();
+            }
+            Map<Long, StaticIndicatorResult> resultById = monthResults.stream()
+                    .collect(Collectors.toMap(StaticIndicatorResult::getId, r -> r, (a, b) -> a));
+            LambdaQueryWrapper<StaticIndicatorMetric> subWrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                    .eq(StaticIndicatorMetric::getLevel, "feeder")
+                    .eq(StaticIndicatorMetric::getSourceSubstationId, id)
+                    .eq(StaticIndicatorMetric::getMetricCode, e.getPrimaryMetricCode())
+                    .in(StaticIndicatorMetric::getResultId, resultById.keySet());
+            List<StaticIndicatorMetric> subMetrics = metricMapper.selectList(subWrapper);
+            if (subMetrics.isEmpty()) {
+                log.warn("变电站当月无 feeder 级指标 substationId={}", id);
+                return emptyResult();
+            }
+            chosenResultId = subMetrics.stream()
+                    .map(StaticIndicatorMetric::getResultId)
+                    .distinct()
+                    .max(Comparator.comparing(rid -> resultById.get(rid).getCreateTime()))
+                    .orElse(null);
+            StaticIndicatorResult result = resultById.get(chosenResultId);
+            rawResponse = result.getRawResponse();
+        }
+
+        // 2. 取问题馈线列表(主指标 + 筛选条件)
+        LambdaQueryWrapper<StaticIndicatorMetric> feederWrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                .eq(StaticIndicatorMetric::getLevel, "feeder")
+                .eq(StaticIndicatorMetric::getResultId, chosenResultId)
+                .eq(StaticIndicatorMetric::getMetricCode, e.getPrimaryMetricCode())
+                .orderByAsc(StaticIndicatorMetric::getFeederName);
+        if (type == 3) {
+            feederWrapper.eq(StaticIndicatorMetric::getSourceSubstationId, id);
+        }
+        applyValueCondition(feederWrapper, e);
+        List<StaticIndicatorMetric> feederMetrics = metricMapper.selectList(feederWrapper);
+        if (feederMetrics.isEmpty()) {
+            return emptyResult();
+        }
+
+        List<String> feederIds = feederMetrics.stream()
+                .map(StaticIndicatorMetric::getFeederId)
+                .distinct()
+                .collect(Collectors.toList());
+
+        // 3. 批量取 value/unit 指标行
+        Map<String, Map<String, StaticIndicatorMetric>> valueMetricsByFeeder = loadValueMetrics(e, chosenResultId, feederIds);
+
+        // 4. 批量取问题详情(主指标,object_type=device)
+        Map<String, StaticProblemDetail> problemDetailByFeeder = loadProblemDetails(e, chosenResultId, feederIds);
+
+        // 5. 解析 raw_response,建立 feederId -> 主指标 details 数组
+        Map<String, JSONArray> rawDetailsByFeeder = parseRawDetails(rawResponse, e.getPrimaryMetricCode(), feederIds);
+
+        // 6. 组装
+        List<StaticGridDetailItemVO> items = new ArrayList<>(feederMetrics.size());
+        for (StaticIndicatorMetric m : feederMetrics) {
+            items.add(buildItem(m, e, valueMetricsByFeeder, problemDetailByFeeder, rawDetailsByFeeder));
+        }
+
+        return buildResult(e, items);
+    }
+
+    // ============================================================
+    // 主表 / 指标查询辅助
+    // ============================================================
+
+    /**
+     * type=2 区县:按 county_id + 月份取最新一条主表记录
+     */
+    private StaticIndicatorResult getLatestByCountyId(String countyId, YearMonth ym) {
+        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
+                .eq(StaticIndicatorResult::getCountyId, countyId)
+                .ge(StaticIndicatorResult::getCreateTime, monthStart(ym))
+                .le(StaticIndicatorResult::getCreateTime, monthEnd(ym))
+                .orderByDesc(StaticIndicatorResult::getCreateTime)
+                .last("LIMIT 1");
+        return getOne(wrapper);
+    }
+
+    /**
+     * 当月所有主表批次(type=3 用:先拿到月份范围内的结果再按变电站过滤)
+     */
+    private List<StaticIndicatorResult> listByMonth(YearMonth ym) {
+        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<StaticIndicatorResult>()
+                .ge(StaticIndicatorResult::getCreateTime, monthStart(ym))
+                .le(StaticIndicatorResult::getCreateTime, monthEnd(ym))
+                .orderByDesc(StaticIndicatorResult::getCreateTime);
+        return list(wrapper);
+    }
+
+    private void applyValueCondition(LambdaQueryWrapper<StaticIndicatorMetric> wrapper, StaticGridMetricTypeEnum e) {
+        switch (e.getFilterType()) {
+            case VALUE_TRUE:
+                wrapper.eq(StaticIndicatorMetric::getValue, "true");
+                break;
+            case VALUE_FALSE:
+                wrapper.eq(StaticIndicatorMetric::getValue, "false");
+                break;
+            case IS_PROBLEM_TRUE:
+                wrapper.eq(StaticIndicatorMetric::getIsProblem, 1);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * 批量查询 details 所需的 value/unit 指标行,按 feederId -> metricCode 索引
+     */
+    private Map<String, Map<String, StaticIndicatorMetric>> loadValueMetrics(StaticGridMetricTypeEnum e,
+                                                                             Long resultId, List<String> feederIds) {
+        Set<String> valueCodes = new LinkedHashSet<>();
+        for (FieldSpec fs : e.getDetailFields()) {
+            if ((fs.getSource() == DetailSource.METRIC_VALUE || fs.getSource() == DetailSource.METRIC_UNIT)
+                    && fs.getRef() != null) {
+                valueCodes.add(fs.getRef());
+            }
+        }
+        Map<String, Map<String, StaticIndicatorMetric>> result = new HashMap<>();
+        if (valueCodes.isEmpty()) {
+            return result;
+        }
+        LambdaQueryWrapper<StaticIndicatorMetric> wrapper = new LambdaQueryWrapper<StaticIndicatorMetric>()
+                .eq(StaticIndicatorMetric::getLevel, "feeder")
+                .eq(StaticIndicatorMetric::getResultId, resultId)
+                .in(StaticIndicatorMetric::getFeederId, feederIds)
+                .in(StaticIndicatorMetric::getMetricCode, valueCodes);
+        List<StaticIndicatorMetric> metrics = metricMapper.selectList(wrapper);
+        for (StaticIndicatorMetric m : metrics) {
+            result.computeIfAbsent(m.getFeederId(), k -> new HashMap<>()).put(m.getMetricCode(), m);
+        }
+        return result;
+    }
+
+    /**
+     * 批量查询问题详情(主指标 + object_type=device),按 feederId 索引
+     */
+    private Map<String, StaticProblemDetail> loadProblemDetails(StaticGridMetricTypeEnum e,
+                                                                Long resultId, List<String> feederIds) {
+        LambdaQueryWrapper<StaticProblemDetail> wrapper = new LambdaQueryWrapper<StaticProblemDetail>()
+                .eq(StaticProblemDetail::getResultId, resultId)
+                .in(StaticProblemDetail::getFeederId, feederIds)
+                .eq(StaticProblemDetail::getMetricCode, e.getPrimaryMetricCode())
+                .eq(StaticProblemDetail::getObjectType, "device");
+        List<StaticProblemDetail> details = problemDetailMapper.selectList(wrapper);
+        Map<String, StaticProblemDetail> result = new HashMap<>();
+        for (StaticProblemDetail d : details) {
+            // 同一 (result_id, feeder_id, metric_code) 至多一行,putIfAbsent 保险
+            result.putIfAbsent(d.getFeederId(), d);
+        }
+        return result;
+    }
+
+    // ============================================================
+    // raw_response 解析
+    // ============================================================
+
+    /**
+     * 解析 raw_response,建立 feederId -> 主指标 details(JSONArray) 索引(仅保留需要的 feeder)
+     */
+    private Map<String, JSONArray> parseRawDetails(String rawResponse, String primaryMetricCode, List<String> feederIds) {
+        Map<String, JSONArray> result = new HashMap<>();
+        if (StringUtils.isBlank(rawResponse)) {
+            return result;
+        }
+        Set<String> feederIdSet = new LinkedHashSet<>(feederIds);
+        try {
+            JSONObject root = JSON.parseObject(rawResponse);
+            JSONObject calc = root.getJSONObject("calculation_result");
+            if (calc == null) {
+                return result;
+            }
+            JSONArray feederResults = calc.getJSONArray("feeder_results");
+            if (feederResults == null) {
+                return result;
+            }
+            for (int i = 0; i < feederResults.size(); i++) {
+                JSONObject fr = feederResults.getJSONObject(i);
+                String fid = fr.getString("feeder_id");
+                if (!feederIdSet.contains(fid)) {
+                    continue;
+                }
+                JSONArray categories = fr.getJSONArray("categories");
+                if (categories == null) {
+                    continue;
+                }
+                for (int j = 0; j < categories.size(); j++) {
+                    JSONArray metrics = categories.getJSONObject(j).getJSONArray("metrics");
+                    if (metrics == null) {
+                        continue;
+                    }
+                    for (int k = 0; k < metrics.size(); k++) {
+                        JSONObject m = metrics.getJSONObject(k);
+                        if (primaryMetricCode.equals(m.getString("metric_code"))) {
+                            result.put(fid, m.getJSONArray("details"));
+                            break;
+                        }
+                    }
+                    if (result.containsKey(fid)) {
+                        break;
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.warn("解析 raw_response 失败 primaryMetricCode={}", primaryMetricCode, ex);
+        }
+        return result;
+    }
+
+    // ============================================================
+    // 组装
+    // ============================================================
+
+    private StaticGridDetailItemVO buildItem(StaticIndicatorMetric metric, StaticGridMetricTypeEnum e,
+                                             Map<String, Map<String, StaticIndicatorMetric>> valueMetricsByFeeder,
+                                             Map<String, StaticProblemDetail> problemDetailByFeeder,
+                                             Map<String, JSONArray> rawDetailsByFeeder) {
+        String fid = metric.getFeederId();
+        Map<String, StaticIndicatorMetric> valueMetrics = valueMetricsByFeeder.getOrDefault(fid, Collections.emptyMap());
+        StaticProblemDetail pd = problemDetailByFeeder.get(fid);
+        JSONArray details = rawDetailsByFeeder.get(fid);
+
+        Map<String, Object> detailsMap = new LinkedHashMap<>();
+        if (e.isShowDetails()) {
+            for (FieldSpec fs : e.getDetailFields()) {
+                detailsMap.put(fs.getOutputKey(), resolveField(fs, valueMetrics, pd, details));
+            }
+        }
+
+        return StaticGridDetailItemVO.builder()
+                .feederId(fid)
+                .feederName(metric.getFeederName())
+                .details(detailsMap)
+                .deviceIds(resolveDeviceIds(pd, details))
+                .build();
+    }
+
+    private Object resolveField(FieldSpec fs, Map<String, StaticIndicatorMetric> valueMetrics,
+                                StaticProblemDetail pd, JSONArray details) {
+        switch (fs.getSource()) {
+            case METRIC_VALUE:
+                StaticIndicatorMetric mv = valueMetrics.get(fs.getRef());
+                return mv != null ? toNumber(mv.getValue()) : null;
+            case METRIC_UNIT:
+                StaticIndicatorMetric mu = valueMetrics.get(fs.getRef());
+                return mu != null ? mu.getUnit() : null;
+            case REASON:
+                return pd != null ? pd.getDescription() : null;
+            case RAW_LEAF:
+                return roundRawLeaf(getDetailLeaf(details, fs.getRef()), fs.getScale());
+            case RAW_BRANCH_FIRST:
+                return getBranchFirst(details, fs.getRef());
+            case RAW_LOAD_GROUPS:
+                return getLoadGroups(details);
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * deviceIds:优先取问题详情表 object_id(object_type=device),缺失则回退 raw_response details[0].device_ids
+     */
+    private List<String> resolveDeviceIds(StaticProblemDetail pd, JSONArray details) {
+        if (pd != null && StringUtils.isNotBlank(pd.getObjectId())) {
+            try {
+                List<String> ids = JSON.parseArray(pd.getObjectId(), String.class);
+                if (ids != null && !ids.isEmpty()) {
+                    return ids;
+                }
+            } catch (Exception ex) {
+                log.warn("解析 deviceIds 失败 objectId={}", pd.getObjectId());
+            }
+        }
+        if (details != null && !details.isEmpty()) {
+            JSONObject d0 = details.getJSONObject(0);
+            if (d0 != null) {
+                Object ids = d0.get("device_ids");
+                if (ids instanceof JSONArray) {
+                    return ((JSONArray) ids).toJavaList(String.class);
+                }
+            }
+        }
+        return Collections.emptyList();
+    }
+
+    private Object getDetailLeaf(JSONArray details, String key) {
+        if (details == null || details.isEmpty()) {
+            return null;
+        }
+        JSONObject d0 = details.getJSONObject(0);
+        return d0 == null ? null : d0.get(key);
+    }
+
+    private Object getBranchFirst(JSONArray details, String key) {
+        if (details == null || details.isEmpty()) {
+            return null;
+        }
+        JSONObject d0 = details.getJSONObject(0);
+        if (d0 == null) {
+            return null;
+        }
+        JSONArray branches = d0.getJSONArray("branches");
+        if (branches == null || branches.isEmpty()) {
+            return null;
+        }
+        JSONObject b0 = branches.getJSONObject(0);
+        return b0 == null ? null : b0.get(key);
+    }
+
+    private List<Map<String, Object>> getLoadGroups(JSONArray details) {
+        if (details == null || details.isEmpty()) {
+            return Collections.emptyList();
+        }
+        JSONObject d0 = details.getJSONObject(0);
+        if (d0 == null) {
+            return Collections.emptyList();
+        }
+        JSONArray groups = d0.getJSONArray("load_groups");
+        if (groups == null || groups.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Map<String, Object>> list = new ArrayList<>(groups.size());
+        for (int i = 0; i < groups.size(); i++) {
+            JSONObject g = groups.getJSONObject(i);
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("branchId", g.get("branch_id"));
+            item.put("capacityMva", g.get("capacity_mva"));
+            item.put("transformerCount", g.get("transformer_count"));
+            list.add(item);
+        }
+        return list;
+    }
+
+    /**
+     * 指标表 value 字符串 -> 数值:整数返回 Integer,小数返回 Double
+     */
+    private Object toNumber(String valueStr) {
+        if (StringUtils.isBlank(valueStr)) {
+            return null;
+        }
+        try {
+            BigDecimal bd = new BigDecimal(valueStr);
+            if (bd.scale() <= 0) {
+                return bd.intValue();
+            }
+            return bd.doubleValue();
+        } catch (Exception ex) {
+            return valueStr;
+        }
+    }
+
+    /**
+     * raw_response 叶子数值:scale>=0 时按 HALF_UP 保留 n 位小数(如 N-1 的估算负载保留三位小数)
+     */
+    private Object roundRawLeaf(Object val, int scale) {
+        if (val == null) {
+            return null;
+        }
+        if (scale >= 0) {
+            try {
+                return new BigDecimal(val.toString()).setScale(scale, RoundingMode.HALF_UP).doubleValue();
+            } catch (Exception ex) {
+                return val;
+            }
+        }
+        return val;
+    }
+
+    // ============================================================
+    // 结果构建
+    // ============================================================
+
+    /**
+     * 固定 13 个指标字段,仅入参 metricType 对应字段填充数据,其余为空数组
+     */
+    private StaticGridDetailResultVO buildResult(StaticGridMetricTypeEnum e,
+                                                                     List<StaticGridDetailItemVO> items) {
+        StaticGridDetailResultVO vo = emptyResult();
+        switch (e) {
+            case N1_FAIL:
+                vo.setN1Fail(items);
+                break;
+            case NO_CONNECTION:
+                vo.setNoConnection(items);
+                break;
+            case SAME_BUS_INTERCONNECTION:
+                vo.setSameBusInterconnection(items);
+                break;
+            case EXCESSIVE_TIE_POINT:
+                vo.setExcessiveTiePoint(items);
+                break;
+            case SINGLE_RADIAL:
+                vo.setSingleRadial(items);
+                break;
+            case LARGE_BRANCH:
+                vo.setLargeBranch(items);
+                break;
+            case UNREASONABLE_SEGMENT:
+                vo.setUnreasonableSegment(items);
+                break;
+            case DENSE_SEGMENT:
+                vo.setDenseSegment(items);
+                break;
+            case SUPPLY_RADIUS_EXCEED:
+                vo.setSupplyRadiusExceed(items);
+                break;
+            case EXTRA_LONG_FEEDER:
+                vo.setExtraLongFeeder(items);
+                break;
+            case TRUNK_MISMATCH:
+                vo.setTrunkMismatch(items);
+                break;
+            case CAPACITY_EXCEED:
+                vo.setCapacityExceed(items);
+                break;
+            case LOAD_GROUP_ANOMALY:
+                vo.setLoadGroupAnomaly(items);
+                break;
+            default:
+                break;
+        }
+        return vo;
+    }
+
+    private StaticGridDetailResultVO emptyResult() {
+        return StaticGridDetailResultVO.builder()
+                .n1Fail(Collections.emptyList())
+                .noConnection(Collections.emptyList())
+                .sameBusInterconnection(Collections.emptyList())
+                .excessiveTiePoint(Collections.emptyList())
+                .singleRadial(Collections.emptyList())
+                .largeBranch(Collections.emptyList())
+                .unreasonableSegment(Collections.emptyList())
+                .denseSegment(Collections.emptyList())
+                .supplyRadiusExceed(Collections.emptyList())
+                .extraLongFeeder(Collections.emptyList())
+                .trunkMismatch(Collections.emptyList())
+                .capacityExceed(Collections.emptyList())
+                .loadGroupAnomaly(Collections.emptyList())
+                .build();
+    }
+
+    // ============================================================
+    // 工具
+    // ============================================================
+
+    private LocalDateTime monthStart(YearMonth ym) {
+        return ym.atDay(1).atStartOfDay();
+    }
+
+    private LocalDateTime monthEnd(YearMonth ym) {
+        return ym.atEndOfMonth().atTime(23, 59, 59);
+    }
+}