Browse Source

运行部分修正

lisonglin 2 weeks ago
parent
commit
b0a9c6fc1f
17 changed files with 645 additions and 277 deletions
  1. 36 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeMonitorThreshold.java
  2. 8 31
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  3. 31 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsTransformerBase.java
  4. 7 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorCurrent.java
  5. 7 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorVoltage.java
  6. 43 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/SeOverloadAlarmListReq.java
  7. 43 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/SeVoltageAlarmListReq.java
  8. 12 15
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDashboardVO.java
  9. 17 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsTransformerBaseMapper.java
  10. 45 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/scheduler/SeVoltageCleanupScheduler.java
  11. 5 15
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeAlarmService.java
  12. 81 67
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java
  13. 41 9
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeConsumerVoltageServiceImpl.java
  14. 18 18
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  15. 145 73
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  16. 61 9
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java
  17. 45 37
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/impl/StaticIndicatorResultServiceImpl.java

+ 36 - 1
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeMonitorThreshold.java

@@ -50,4 +50,39 @@ public final class SeMonitorThreshold {
     public static final BigDecimal FEEDER_BASE_KV = new BigDecimal("10");
     public static final BigDecimal FEEDER_BASE_KV = new BigDecimal("10");
     public static final BigDecimal MVTRANS_BASE_KV = new BigDecimal("0.22");
     public static final BigDecimal MVTRANS_BASE_KV = new BigDecimal("0.22");
     public static final BigDecimal CONSUMER_BASE_V = new BigDecimal("220");
     public static final BigDecimal CONSUMER_BASE_V = new BigDecimal("220");
-}
+
+    /**
+     * 解析用户电压等级字符串,返回基准电压(V)
+     * <p>
+     * 码值示例:交流220V、交流380V、0.4kV、10kV、220V、直流220V
+     * <br>
+     * 规则:取连续数字(含小数点),"kV"则×1000,否则按V;
+     * 解析失败退 CONSUMER_BASE_V(220)
+     */
+    public static BigDecimal parseConsumerBaseVoltage(String voltageLevel) {
+        if (voltageLevel == null || voltageLevel.isEmpty()) return CONSUMER_BASE_V;
+        try {
+            // 提取第一段连续数字(含小数点)
+            StringBuilder sb = new StringBuilder();
+            boolean found = false;
+            for (char c : voltageLevel.toCharArray()) {
+                if (Character.isDigit(c) || c == '.') {
+                    sb.append(c);
+                    found = true;
+                } else if (found) {
+                    break; // 数字结束
+                }
+            }
+            if (!found || sb.length() == 0) return CONSUMER_BASE_V;
+            BigDecimal val = new BigDecimal(sb.toString());
+            // 判断是否是 kV
+            boolean isKv = voltageLevel.toUpperCase().contains("KV");
+            if (isKv) {
+                return val.multiply(new BigDecimal("1000"));
+            }
+            return val;
+        } catch (Exception e) {
+            return CONSUMER_BASE_V;
+        }
+    }
+}

+ 8 - 31
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java

@@ -5,6 +5,8 @@ import com.hdkj.fhzg.optimization.request.MaintOrgRequest;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.lt.bf.adapter.SiStateEstimationClient;
 import com.hdkj.lt.bf.adapter.SiStateEstimationClient;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
+import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
+import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
 import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
@@ -120,49 +122,24 @@ public class IndicatorController extends BaseController {
 
 
     /**
     /**
      * 异常告警-线路重过载列表
      * 异常告警-线路重过载列表
-     *
-     * @param params {id, type(2/3/4), feederName(可选), startDate(可选), endDate(可选), orderBy(asc/desc,可选)}
      */
      */
     @PostMapping("/alarm/overload")
     @PostMapping("/alarm/overload")
-    public ApiResponse<PageResult<SeAlarmListVO>> alarmOverload(@RequestBody Map<String, String> params) {
-        String id = params.get("id");
-        String type = params.get("type");
-        String feederName = params.get("feederName");
-        String startDate = params.get("startDate");
-        String endDate = params.get("endDate");
-        String orderBy = params.get("orderBy");
-        String pageStr = params.get("page");
-        String pageSizeStr = params.get("pageSize");
-        if (id == null || type == null) {
+    public ApiResponse<PageResult<SeAlarmListVO>> alarmOverload(@RequestBody SeOverloadAlarmListReq req) {
+        if (req.getId() == null || req.getType() == null) {
             return ApiResponse.fail("参数缺失");
             return ApiResponse.fail("参数缺失");
         }
         }
-        Integer page = pageStr != null ? Integer.parseInt(pageStr) : 1;
-        Integer pageSize = pageSizeStr != null ? Integer.parseInt(pageSizeStr) : 20;
-        return ApiResponse.success(seAlarmService.queryOverloadAlarmList(id, Integer.parseInt(type), feederName, startDate, endDate, orderBy, page, pageSize));
+        return ApiResponse.success(seAlarmService.queryOverloadAlarmList(req));
     }
     }
 
 
     /**
     /**
      * 异常告警-电压越限列表
      * 异常告警-电压越限列表
-     *
-     * @param params {id, type(2/3/4), feederName(可选), startDate(可选), endDate(可选), orderBy(asc/desc,可选), alarmLevel(feeder/mvtrans)}
      */
      */
     @PostMapping("/alarm/voltage")
     @PostMapping("/alarm/voltage")
-    public ApiResponse<PageResult<SeAlarmListVO>> alarmVoltage(@RequestBody Map<String, String> params) {
-        String id = params.get("id");
-        String type = params.get("type");
-        String feederName = params.get("feederName");
-        String startDate = params.get("startDate");
-        String endDate = params.get("endDate");
-        String orderBy = params.get("orderBy");
-        String pageStr = params.get("page");
-        String pageSizeStr = params.get("pageSize");
-        String alarmLevel = params.get("alarmLevel");
-        if (id == null || type == null) {
+    public ApiResponse<PageResult<SeAlarmListVO>> alarmVoltage(@RequestBody SeVoltageAlarmListReq req) {
+        if (req.getId() == null || req.getType() == null) {
             return ApiResponse.fail("参数缺失");
             return ApiResponse.fail("参数缺失");
         }
         }
-        Integer page = pageStr != null ? Integer.parseInt(pageStr) : 1;
-        Integer pageSize = pageSizeStr != null ? Integer.parseInt(pageSizeStr) : 20;
-        return ApiResponse.success(seAlarmService.queryVoltageAlarmList(id, Integer.parseInt(type), feederName, startDate, endDate, orderBy, page, pageSize, alarmLevel));
+        return ApiResponse.success(seAlarmService.queryVoltageAlarmList(req));
     }
     }
 
 
     /**
     /**

+ 31 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsTransformerBase.java

@@ -0,0 +1,31 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 配电设备_配电变压器台账表
+ * @TableName dwd_shb_ds_transformer_base
+ */
+@TableName(value = "dwd_shb_ds_transformer_base")
+@Data
+public class DwdShbDsTransformerBase implements Serializable {
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 资源ID
+     */
+    @TableId
+    private String psrId;
+
+    /**
+     * 设备名称
+     */
+    private String name;
+}

+ 7 - 1
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorCurrent.java

@@ -1,6 +1,8 @@
 package com.hdkj.lt.bf.entity;
 package com.hdkj.lt.bf.entity;
 
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
@@ -22,12 +24,16 @@ public class FhzgSeMonitorCurrent implements Serializable {
     private Long id;
     private Long id;
     private String feederId;
     private String feederId;
     private String feederName;
     private String feederName;
+    /** 必须用 IGNORED,否则 updateById 跳过 null → DB 残留旧值 → 状态机误判 */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private LocalDateTime firstOverTime;
     private LocalDateTime firstOverTime;
     private BigDecimal lastLoadRate;
     private BigDecimal lastLoadRate;
     private Integer lastStatus;
     private Integer lastStatus;
     private LocalDateTime lastSnapTime;
     private LocalDateTime lastSnapTime;
     private Integer alarmTriggered;
     private Integer alarmTriggered;
+    /** 必须用 IGNORED,同上(null 表示无活跃事件) */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Long alarmEventId;
     private Long alarmEventId;
     private LocalDateTime createTime;
     private LocalDateTime createTime;
     private LocalDateTime updateTime;
     private LocalDateTime updateTime;
-}
+}

+ 7 - 1
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorVoltage.java

@@ -1,6 +1,8 @@
 package com.hdkj.lt.bf.entity;
 package com.hdkj.lt.bf.entity;
 
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
@@ -28,12 +30,16 @@ public class FhzgSeMonitorVoltage implements Serializable {
     private String countyId;
     private String countyId;
     private String mvtransId;
     private String mvtransId;
     private String consumerId;
     private String consumerId;
+    /** 必须用 IGNORED,否则 updateById 跳过 null → DB 残留旧值 → 状态机误判 */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private LocalDateTime firstOverTime;
     private LocalDateTime firstOverTime;
     private BigDecimal lastVoltageRatio;
     private BigDecimal lastVoltageRatio;
     private Integer lastVoltageStatus;
     private Integer lastVoltageStatus;
     private LocalDateTime lastSnapTime;
     private LocalDateTime lastSnapTime;
     private Integer alarmTriggered;
     private Integer alarmTriggered;
+    /** 必须用 IGNORED,同上(null 表示无活跃事件) */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Long alarmEventId;
     private Long alarmEventId;
     private LocalDateTime createTime;
     private LocalDateTime createTime;
     private LocalDateTime updateTime;
     private LocalDateTime updateTime;
-}
+}

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

@@ -0,0 +1,43 @@
+package com.hdkj.lt.bf.entity.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 重过载告警列表请求参数
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@Data
+@ApiModel(description = "重过载告警列表请求参数")
+public class SeOverloadAlarmListReq {
+
+    @ApiModelProperty(value = "节点ID", required = true)
+    private String id;
+
+    @ApiModelProperty(value = "节点类型(2=区县 3=变电站 4=馈线)", required = true)
+    private Integer type;
+
+    @ApiModelProperty(value = "馈线名称(模糊)")
+    private String feederName;
+
+    @ApiModelProperty(value = "起始日期 yyyy-MM-dd")
+    private String startDate;
+
+    @ApiModelProperty(value = "截止日期 yyyy-MM-dd")
+    private String endDate;
+
+    @ApiModelProperty(value = "时间排序 asc/desc")
+    private String orderByTime;
+
+    @ApiModelProperty(value = "负载率排序 asc/desc,优先级高于时间排序")
+    private String orderByLoadRate;
+
+    @ApiModelProperty(value = "页码")
+    private Integer page;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize;
+}

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

@@ -0,0 +1,43 @@
+package com.hdkj.lt.bf.entity.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 电压越限告警列表请求参数
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@Data
+@ApiModel(description = "电压越限告警列表请求参数")
+public class SeVoltageAlarmListReq {
+
+    @ApiModelProperty(value = "节点ID", required = true)
+    private String id;
+
+    @ApiModelProperty(value = "节点类型(2=区县 3=变电站 4=馈线)", required = true)
+    private Integer type;
+
+    @ApiModelProperty(value = "馈线名称(模糊)")
+    private String feederName;
+
+    @ApiModelProperty(value = "起始日期 yyyy-MM-dd")
+    private String startDate;
+
+    @ApiModelProperty(value = "截止日期 yyyy-MM-dd")
+    private String endDate;
+
+    @ApiModelProperty(value = "时间排序 asc/desc")
+    private String orderByTime;
+
+    @ApiModelProperty(value = "告警级别 feeder/mvtrans")
+    private String alarmLevel;
+
+    @ApiModelProperty(value = "页码")
+    private Integer page;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize;
+}

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

@@ -89,7 +89,10 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "联络点过多线路数")
     @ApiModelProperty(value = "联络点过多线路数")
     private Integer excessiveTiePointFeederCount;
     private Integer excessiveTiePointFeederCount;
 
 
-    @ApiModelProperty(value = "无联络/单辐射线路数")
+    @ApiModelProperty(value = "无联络线路数")
+    private Integer noConnectionFeederCount;
+
+    @ApiModelProperty(value = "单辐射线路数")
     private Integer singleRadialFeederCount;
     private Integer singleRadialFeederCount;
 
 
     @ApiModelProperty(value = "存在大分支馈线数")
     @ApiModelProperty(value = "存在大分支馈线数")
@@ -118,13 +121,13 @@ public class StaticGridDashboardVO implements Serializable {
     // 四、供电能力
     // 四、供电能力
     // ============================================================
     // ============================================================
 
 
-    @ApiModelProperty(value = "区域最大负载率(%)(预留,来自运行态指标)")
+    @ApiModelProperty(value = "区域最大负载率(%),从容量日汇总表统计")
     private BigDecimal regionalMaxLoadRate;
     private BigDecimal regionalMaxLoadRate;
 
 
-    @ApiModelProperty(value = "区域平均负载率(%)(预留,来自运行态指标)")
+    @ApiModelProperty(value = "区域平均负载率(%),从容量日汇总表统计")
     private BigDecimal regionalAvgLoadRate;
     private BigDecimal regionalAvgLoadRate;
 
 
-    @ApiModelProperty(value = "线路过载条数(预留,来自运行态指标)")
+    @ApiModelProperty(value = "线路重过载条数,从容量日汇总表统计重载+过载")
     private Integer lineOverloadCount;
     private Integer lineOverloadCount;
 
 
     @ApiModelProperty(value = "主干线截面不匹配线路数")
     @ApiModelProperty(value = "主干线截面不匹配线路数")
@@ -136,12 +139,6 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "负荷组容量异常数")
     @ApiModelProperty(value = "负荷组容量异常数")
     private Integer loadGroupCapacityAnomalyCount;
     private Integer loadGroupCapacityAnomalyCount;
 
 
-    @ApiModelProperty(value = "综合线损率(%)(预留,来自运行态指标)")
-    private BigDecimal comprehensiveLossRate;
-
-    @ApiModelProperty(value = "功率因数合格率(%)(预留,来自运行态指标)")
-    private BigDecimal powerFactorPassRate;
-
     // ============================================================
     // ============================================================
     // 问题详情列表
     // 问题详情列表
     // ============================================================
     // ============================================================
@@ -182,10 +179,10 @@ public class StaticGridDashboardVO implements Serializable {
         @ApiModelProperty(value = "实际值")
         @ApiModelProperty(value = "实际值")
         private String actualValue;
         private String actualValue;
 
 
-        @ApiModelProperty(value = "问题描述")
-        private String description;
+        @ApiModelProperty(value = "问题原因")
+        private String reason;
 
 
-        @ApiModelProperty(value = "整改建议")
-        private String suggestion;
+        @ApiModelProperty(value = "关联设备ID列表")
+        private List<String> deviceIds;
     }
     }
-}
+}

+ 17 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsTransformerBaseMapper.java

@@ -0,0 +1,17 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.DwdShbDsTransformerBase;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 配电变压器台账 Mapper
+ * <p>
+ * 用于状估处理时查配变名称。直接用 BaseMapper + LambdaQueryWrapper.in() 按需查询。
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@Mapper
+public interface DwdShbDsTransformerBaseMapper extends BaseMapper<DwdShbDsTransformerBase> {
+}

+ 45 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/scheduler/SeVoltageCleanupScheduler.java

@@ -0,0 +1,45 @@
+package com.hdkj.lt.bf.scheduler;
+
+import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * 电压事件兜底清扫
+ * <p>
+ * 关闭超过24小时仍未恢复的活跃事件,防止因断档/数据缺失导致的事件失联。
+ * 正常恢复由断面处理时的状态机走,此任务仅做兜底。
+ *
+ * @author lsl
+ * @since 2026-07-27
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SeVoltageCleanupScheduler {
+
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+
+    /**
+     * 每天凌晨3点执行
+     */
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void cleanupStaleVoltageEvents() {
+        LocalDateTime threshold = LocalDateTime.now().minusHours(24);
+        FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+        update.setStatus(2);
+        update.setRecoverTime(LocalDateTime.now());
+        int updated = voltageEventMapper.update(update,
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                        .eq(FhzgSeVoltageEvent::getStatus, 1)
+                        .lt(FhzgSeVoltageEvent::getTriggerTime, threshold));
+        if (updated > 0) {
+            log.info("[电压事件清扫] 关闭 {} 条超过24h未恢复的电压事件(triggerTime < {})", updated, threshold);
+        }
+    }
+}

+ 5 - 15
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeAlarmService.java

@@ -1,5 +1,7 @@
 package com.hdkj.lt.bf.service;
 package com.hdkj.lt.bf.service;
 
 
+import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
+import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
 import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
@@ -23,25 +25,13 @@ public interface SeAlarmService {
 
 
     /**
     /**
      * 分页查询指定日期范围重过载告警列表
      * 分页查询指定日期范围重过载告警列表
-     *
-     * @param startDate 查询起始日期 yyyy-MM-dd,为空则当天
-     * @param endDate   查询截止日期 yyyy-MM-dd,为空则同 startDate
-     * @param orderDir 排序方向 asc/desc,为空则 desc
      */
      */
-    PageResult<SeAlarmListVO> queryOverloadAlarmList(String id, Integer type, String feederName,
-                                                      String startDate, String endDate, String orderDir,
-                                                      Integer page, Integer pageSize);
+    PageResult<SeAlarmListVO> queryOverloadAlarmList(SeOverloadAlarmListReq req);
 
 
     /**
     /**
      * 分页查询指定日期范围电压越限告警列表
      * 分页查询指定日期范围电压越限告警列表
-     *
-     * @param startDate 查询起始日期 yyyy-MM-dd,为空则当天
-     * @param endDate   查询截止日期 yyyy-MM-dd,为空则同 startDate
-     * @param orderDir 排序方向 asc/desc,为空则 desc
      */
      */
-    PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
-                                                     String startDate, String endDate, String orderDir,
-                                                     Integer page, Integer pageSize, String alarmLevel);
+    PageResult<SeAlarmListVO> queryVoltageAlarmList(SeVoltageAlarmListReq req);
 
 
     /**
     /**
      * 查询运行异常信息详情
      * 查询运行异常信息详情
@@ -55,4 +45,4 @@ public interface SeAlarmService {
      * @param type 节点类型 2=区县 3=变电站 4=馈线(type=4时返回自身)
      * @param type 节点类型 2=区县 3=变电站 4=馈线(type=4时返回自身)
      */
      */
     List<FeederSelectVO> queryFeederList(String id, Integer type);
     List<FeederSelectVO> queryFeederList(String id, Integer type);
-}
+}

+ 81 - 67
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java

@@ -1,11 +1,14 @@
 package com.hdkj.lt.bf.service.impl;
 package com.hdkj.lt.bf.service.impl;
 
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
+import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.entity.vo.*;
 import com.hdkj.lt.bf.entity.vo.*;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
@@ -58,30 +61,31 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     // ============================================================
     // ============================================================
 
 
     @Override
     @Override
-    public PageResult<SeAlarmListVO> queryOverloadAlarmList(String id, Integer type, String feederName,
-                                                            String startDate, String endDate, String orderDir,
-                                                            Integer page, Integer pageSize) {
-        if (page == null || page < 1) page = 1;
-        if (pageSize == null || pageSize < 1) pageSize = 20;
-
-        LocalDate start = parseDate(startDate);
-        LocalDate end = parseDate(endDate);
+    public PageResult<SeAlarmListVO> queryOverloadAlarmList(SeOverloadAlarmListReq req) {
+        if (req.getPage() == null || req.getPage() < 1) req.setPage(1);
+        if (req.getPageSize() == null || req.getPageSize() < 1) req.setPageSize(20);
+
+        LocalDate start = parseDate(req.getStartDate());
+        LocalDate end = parseDate(req.getEndDate());
         if (end == null || end.isBefore(start)) end = start;
         if (end == null || end.isBefore(start)) end = start;
         LocalDateTime rangeStart = start.atStartOfDay();
         LocalDateTime rangeStart = start.atStartOfDay();
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
 
 
-        Page<FhzgSeCurrentEvent> mpPage = new Page<>(page, pageSize);
+        Page<FhzgSeCurrentEvent> mpPage = new Page<>(req.getPage(), req.getPageSize());
 
 
         LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
         LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, rangeStart)
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, rangeStart)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, rangeEnd)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, rangeEnd)
                 .in(FhzgSeCurrentEvent::getAlarmType, OVERLOAD_TYPES);
                 .in(FhzgSeCurrentEvent::getAlarmType, OVERLOAD_TYPES);
 
 
-        applyCurrentFilter(wrapper, id, type);
-        if (StringUtils.isNotBlank(feederName)) {
-            wrapper.like(FhzgSeCurrentEvent::getFeederName, feederName);
+        applyCurrentFilter(wrapper, req.getId(), req.getType());
+        if (StringUtils.isNotBlank(req.getFeederName())) {
+            wrapper.like(FhzgSeCurrentEvent::getFeederName, req.getFeederName());
         }
         }
-        applyOrderBy(wrapper, orderDir, FhzgSeCurrentEvent::getFirstOverTime);
+
+        // 负载率排序优先级高于时间排序
+        applyOrderBy(wrapper, req.getOrderByLoadRate(), FhzgSeCurrentEvent::getMaxLoadRate);
+        applyOrderBy(wrapper, req.getOrderByTime(), FhzgSeCurrentEvent::getFirstOverTime);
 
 
         Page<FhzgSeCurrentEvent> result = currentEventMapper.selectPage(mpPage, wrapper);
         Page<FhzgSeCurrentEvent> result = currentEventMapper.selectPage(mpPage, wrapper);
         List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toOverloadVO).collect(Collectors.toList());
         List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toOverloadVO).collect(Collectors.toList());
@@ -89,9 +93,9 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     }
     }
 
 
     private SeAlarmListVO toOverloadVO(FhzgSeCurrentEvent e) {
     private SeAlarmListVO toOverloadVO(FhzgSeCurrentEvent e) {
-        String displayValue = e.getLastLoadRate() != null
-                ? e.getLastLoadRate().setScale(0, java.math.RoundingMode.HALF_UP).toPlainString()
-                : "80";
+        String displayValue = e.getMaxLoadRate() != null
+                ? e.getMaxLoadRate().setScale(0, java.math.RoundingMode.HALF_UP).toPlainString()
+                : null;
         return SeAlarmListVO.builder()
         return SeAlarmListVO.builder()
                 .alarmEventId(e.getId())
                 .alarmEventId(e.getId())
                 .feederName(e.getFeederName())
                 .feederName(e.getFeederName())
@@ -108,29 +112,27 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     // ============================================================
     // ============================================================
 
 
     @Override
     @Override
-    public PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
-                                                           String startDate, String endDate, String orderDir,
-                                                           Integer page, Integer pageSize, String alarmLevel) {
-        if (page == null || page < 1) page = 1;
-        if (pageSize == null || pageSize < 1) pageSize = 20;
-
-        LocalDate start = parseDate(startDate);
-        LocalDate end = parseDate(endDate);
+    public PageResult<SeAlarmListVO> queryVoltageAlarmList(SeVoltageAlarmListReq req) {
+        if (req.getPage() == null || req.getPage() < 1) req.setPage(1);
+        if (req.getPageSize() == null || req.getPageSize() < 1) req.setPageSize(20);
+
+        LocalDate start = parseDate(req.getStartDate());
+        LocalDate end = parseDate(req.getEndDate());
         if (end == null || end.isBefore(start)) end = start;
         if (end == null || end.isBefore(start)) end = start;
         LocalDateTime rangeStart = start.atStartOfDay();
         LocalDateTime rangeStart = start.atStartOfDay();
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
 
 
-        Page<FhzgSeVoltageEvent> mpPage = new Page<>(page, pageSize);
+        Page<FhzgSeVoltageEvent> mpPage = new Page<>(req.getPage(), req.getPageSize());
         LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
         LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
                 .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
                 .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
                 .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
                 .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
-                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder".equals(alarmLevel) ? "feeder" : "mvtrans");
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder".equals(req.getAlarmLevel()) ? "feeder" : "mvtrans");
 
 
-        applyVoltageFilter(wrapper, id, type);
-        if (StringUtils.isNotBlank(feederName)) {
-            wrapper.like(FhzgSeVoltageEvent::getFeederName, feederName);
+        applyVoltageFilter(wrapper, req.getId(), req.getType());
+        if (StringUtils.isNotBlank(req.getFeederName())) {
+            wrapper.like(FhzgSeVoltageEvent::getFeederName, req.getFeederName());
         }
         }
-        applyOrderBy(wrapper, orderDir, FhzgSeVoltageEvent::getFirstOverTime);
+        applyOrderBy(wrapper, req.getOrderByTime(), FhzgSeVoltageEvent::getFirstOverTime);
 
 
         Page<FhzgSeVoltageEvent> result = voltageEventMapper.selectPage(mpPage, wrapper);
         Page<FhzgSeVoltageEvent> result = voltageEventMapper.selectPage(mpPage, wrapper);
         List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toVoltageVO).collect(Collectors.toList());
         List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toVoltageVO).collect(Collectors.toList());
@@ -258,20 +260,18 @@ public class SeAlarmServiceImpl implements SeAlarmService {
 
 
         // 查询事件窗口内该配变的低压侧电压 max/min
         // 查询事件窗口内该配变的低压侧电压 max/min
         BigDecimal maxVoltage = null, minVoltage = null;
         BigDecimal maxVoltage = null, minVoltage = null;
-        if (event.getFirstOverTime() != null && event.getDeviceId() != null) {
-            List<FhzgSeSnapshotDetail> snaps = snapshotDetailMapper.selectList(
-                    new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
-                            .eq(FhzgSeSnapshotDetail::getDeviceId, event.getDeviceId())
-                            .eq(FhzgSeSnapshotDetail::getDeviceType, "mvtrans")
-                            .isNotNull(FhzgSeSnapshotDetail::getLvVoltageA)
-                            .ge(FhzgSeSnapshotDetail::getSnapshotTime, event.getFirstOverTime())
-                            .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime));
-            for (FhzgSeSnapshotDetail s : snaps) {
-                BigDecimal v = s.getLvVoltageA();
-                if (v == null) continue;
-                if (maxVoltage == null || v.compareTo(maxVoltage) > 0) maxVoltage = v;
-                if (minVoltage == null || v.compareTo(minVoltage) < 0) minVoltage = v;
-            }
+        List<FhzgSeSnapshotDetail> mvSnaps = snapshotDetailMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getDeviceId, event.getDeviceId())
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "mvtrans")
+                        .isNotNull(FhzgSeSnapshotDetail::getLvVoltageA)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, event.getFirstOverTime())
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime));
+        for (FhzgSeSnapshotDetail s : mvSnaps) {
+            BigDecimal v = s.getLvVoltageA();
+            if (v == null) continue;
+            if (maxVoltage == null || v.compareTo(maxVoltage) > 0) maxVoltage = v;
+            if (minVoltage == null || v.compareTo(minVoltage) < 0) minVoltage = v;
         }
         }
 
 
         return SeAlarmDetailVO.builder()
         return SeAlarmDetailVO.builder()
@@ -288,17 +288,12 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .build();
                 .build();
     }
     }
 
 
-    private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
-        LocalDateTime endTime = event.getRecoverTime();
-        if (endTime == null && event.getStatus() == 1) {
-            FhzgSeMonitorCurrent state = monitorCurrentMapper.selectOne(
-                    new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
-                            .eq(FhzgSeMonitorCurrent::getFeederId, event.getFeederId())
-                            .last("LIMIT 1"));
-            if (state != null) endTime = state.getLastSnapTime();
-        }
-        if (endTime == null) endTime = event.getTriggerTime();
+    // ============================================================
+    // 4. 重过载事件详情
+    // ============================================================
 
 
+    private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
+        String abnormalIndicator = resolveOverloadDisplay(event.getAlarmType());
         return SeAlarmDetailVO.builder()
         return SeAlarmDetailVO.builder()
                 .alarmEventId(event.getId())
                 .alarmEventId(event.getId())
                 .alarmType(event.getAlarmType())
                 .alarmType(event.getAlarmType())
@@ -306,27 +301,41 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .countyName(event.getCountyName())
                 .countyName(event.getCountyName())
                 .feederName(event.getFeederName())
                 .feederName(event.getFeederName())
                 .alarmStartTime(event.getFirstOverTime())
                 .alarmStartTime(event.getFirstOverTime())
-                .alarmEndTime(endTime)
-                .maxLoadRate(event.getMaxLoadRate())
+                .alarmEndTime(event.getRecoverTime())
+                .maxLoadRate(
+                        event.getMaxLoadRate() != null
+                                ? event.getMaxLoadRate().setScale(2, java.math.RoundingMode.HALF_UP)
+                                : null)
+                .abnormalIndicator(abnormalIndicator)
                 .reconStatus(event.getReconStatus())
                 .reconStatus(event.getReconStatus())
                 .build();
                 .build();
     }
     }
 
 
     /**
     /**
-     * 查 feeder 事件发生在窗口 [firstOverTime, endTime] 内的 mvtrans 事件数
+     * 解析重过载展示标签
      */
      */
-    private int[] countMvtransInWindow(String feederId, LocalDateTime firstOverTime, LocalDateTime endTime) {
-        if (firstOverTime == null) return new int[]{0, 0};
-        LocalDateTime rangeEnd = endTime != null ? endTime : LocalDateTime.now();
-        List<FhzgSeVoltageEvent> list = voltageEventMapper.selectList(
+    private String resolveOverloadDisplay(String alarmType) {
+        if ("current_overload".equals(alarmType)) return "线路过载";
+        if ("current_heavy".equals(alarmType)) return "线路重载";
+        return alarmType;
+    }
+
+    // ============================================================
+    // 辅助:窗口内配变统计
+    // ============================================================
+
+    /**
+     * 统计指定馈线在窗口内的配变越限数
+     */
+    private int[] countMvtransInWindow(String feederId, LocalDateTime start, LocalDateTime end) {
+        List<FhzgSeVoltageEvent> mvEvents = voltageEventMapper.selectList(
                 new LambdaQueryWrapper<FhzgSeVoltageEvent>()
                 new LambdaQueryWrapper<FhzgSeVoltageEvent>()
                         .eq(FhzgSeVoltageEvent::getFeederId, feederId)
                         .eq(FhzgSeVoltageEvent::getFeederId, feederId)
                         .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
                         .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
-                        .eq(FhzgSeVoltageEvent::getStatus, 1)
-                        .ge(FhzgSeVoltageEvent::getFirstOverTime, firstOverTime)
-                        .le(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd));
+                        .ge(FhzgSeVoltageEvent::getFirstOverTime, start)
+                        .lt(FhzgSeVoltageEvent::getFirstOverTime, end));
         int up = 0, down = 0;
         int up = 0, down = 0;
-        for (FhzgSeVoltageEvent e : list) {
+        for (FhzgSeVoltageEvent e : mvEvents) {
             if (e.getAlarmType() != null && e.getAlarmType().contains("over")) up++;
             if (e.getAlarmType() != null && e.getAlarmType().contains("over")) up++;
             else if (e.getAlarmType() != null && e.getAlarmType().contains("under")) down++;
             else if (e.getAlarmType() != null && e.getAlarmType().contains("under")) down++;
         }
         }
@@ -404,8 +413,13 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         }
         }
     }
     }
 
 
+    /**
+     * 追加排序条件,orderDir 为空时不追加任何排序
+     * 多个字段排序时按调用顺序决定优先级,先调用的优先
+     */
     private <T> void applyOrderBy(LambdaQueryWrapper<T> wrapper, String orderDir,
     private <T> void applyOrderBy(LambdaQueryWrapper<T> wrapper, String orderDir,
-                                  com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> column) {
+                                   SFunction<T, ?> column) {
+        if (StringUtils.isBlank(orderDir)) return;
         if ("asc".equalsIgnoreCase(orderDir)) {
         if ("asc".equalsIgnoreCase(orderDir)) {
             wrapper.orderByAsc(column);
             wrapper.orderByAsc(column);
         } else {
         } else {
@@ -414,7 +428,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     }
     }
 
 
     private void collectFeeders(com.hdkj.fhzg.optimization.response.FeederGroupTreeNode node,
     private void collectFeeders(com.hdkj.fhzg.optimization.response.FeederGroupTreeNode node,
-                                java.util.List<FeederSelectVO> result) {
+                                 java.util.List<FeederSelectVO> result) {
         if (node.getType() == 4) {
         if (node.getType() == 4) {
             result.add(new FeederSelectVO(node.getId(), node.getName()));
             result.add(new FeederSelectVO(node.getId(), node.getName()));
         }
         }

+ 41 - 9
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeConsumerVoltageServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.lt.bf.adapter.SiMeasurementClient;
 import com.hdkj.lt.bf.adapter.SiMeasurementClient;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
+import com.hdkj.lt.bf.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.entity.FhzgSeConsumerRelation;
 import com.hdkj.lt.bf.entity.FhzgSeConsumerRelation;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
@@ -90,11 +91,13 @@ public class SeConsumerVoltageServiceImpl {
             FhzgSeConsumerRelation rel = consNoMap.get(consNo);
             FhzgSeConsumerRelation rel = consNoMap.get(consNo);
             if (rel == null || measValue == null) continue;
             if (rel == null || measValue == null) continue;
 
 
-            BigDecimal voltageRatio = measValue.divide(SeMonitorThreshold.CONSUMER_BASE_V, 4, RoundingMode.HALF_UP);
-            int voltageStatus = calcConsumerVoltageStatus(voltageRatio, rel.getVoltageLevel());
+            BigDecimal baseV = SeMonitorThreshold.parseConsumerBaseVoltage(rel.getVoltageLevel());
+            BigDecimal voltageRatio = measValue.divide(baseV, 4, RoundingMode.HALF_UP);
+            int voltageStatus = calcConsumerVoltageStatus(voltageRatio);
 
 
             FhzgSeSnapshotDetail detail = FhzgSeSnapshotDetail.builder()
             FhzgSeSnapshotDetail detail = FhzgSeSnapshotDetail.builder()
                     .snapshotTime(snapTime).deviceId(consNo).deviceType("consumer")
                     .snapshotTime(snapTime).deviceId(consNo).deviceType("consumer")
+                    .consumerName(rel.getCustomerName())
                     .feederId(rel.getFeederId()).subsId(rel.getSubsId()).countyId(rel.getCountyId())
                     .feederId(rel.getFeederId()).subsId(rel.getSubsId()).countyId(rel.getCountyId())
                     .mvtransId(mvtransId).consumerId(consNo)
                     .mvtransId(mvtransId).consumerId(consNo)
                     .lvVoltageA(measValue).voltageRatio(voltageRatio).voltageStatus(voltageStatus)
                     .lvVoltageA(measValue).voltageRatio(voltageRatio).voltageStatus(voltageStatus)
@@ -103,7 +106,7 @@ public class SeConsumerVoltageServiceImpl {
 
 
             upsertConsumerVoltageMonitor(consNo, "consumer",
             upsertConsumerVoltageMonitor(consNo, "consumer",
                     rel.getFeederId(), rel.getSubsId(), rel.getCountyId(), mvtransId, consNo,
                     rel.getFeederId(), rel.getSubsId(), rel.getCountyId(), mvtransId, consNo,
-                    voltageRatio, voltageStatus, snapTime);
+                    voltageRatio, voltageStatus, snapTime, rel);
         }
         }
         log.info("配变{}用户电压处理完成 共{}户,成功{}户", mvtransId, consNos.size(), voltageMap.size());
         log.info("配变{}用户电压处理完成 共{}户,成功{}户", mvtransId, consNos.size(), voltageMap.size());
     }
     }
@@ -111,7 +114,8 @@ public class SeConsumerVoltageServiceImpl {
     private void upsertConsumerVoltageMonitor(String deviceId, String deviceType,
     private void upsertConsumerVoltageMonitor(String deviceId, String deviceType,
                                                String feederId, String subsId, String countyId,
                                                String feederId, String subsId, String countyId,
                                                String mvtransId, String consumerId,
                                                String mvtransId, String consumerId,
-                                               BigDecimal ratio, int status, LocalDateTime snapTime) {
+                                               BigDecimal ratio, int status, LocalDateTime snapTime,
+                                               FhzgSeConsumerRelation rel) {
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
@@ -133,6 +137,15 @@ public class SeConsumerVoltageServiceImpl {
             boolean intervalExceeded = lastSnap != null
             boolean intervalExceeded = lastSnap != null
                     && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES;
                     && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES;
             if (intervalExceeded) {
             if (intervalExceeded) {
+                // 断档间隙:先关掉还在活跃的事件,再清空状态机引用
+                if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeVoltageEvent close = new FhzgSeVoltageEvent();
+                    close.setId(state.getAlarmEventId());
+                    close.setStatus(2);
+                    close.setRecoverTime(snapTime);
+                    close.setLastValue(ratio);
+                    voltageEventMapper.updateById(close);
+                }
                 state.setFirstOverTime(null);
                 state.setFirstOverTime(null);
                 state.setAlarmTriggered(0);
                 state.setAlarmTriggered(0);
                 state.setAlarmEventId(null);
                 state.setAlarmEventId(null);
@@ -145,10 +158,12 @@ public class SeConsumerVoltageServiceImpl {
 
 
                     String alarmType = resolveConsumerAlarmType(status);
                     String alarmType = resolveConsumerAlarmType(status);
                     BigDecimal threshold = resolveConsumerThreshold(status);
                     BigDecimal threshold = resolveConsumerThreshold(status);
+                    String countyName = lookupCountyName(countyId);
 
 
                     FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
                     FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                            .deviceId(deviceId).deviceType("consumer").alarmLevel("consumer").alarmType(alarmType)
-                            .feederId(feederId).subsId(subsId).countyId(countyId)
+                            .deviceId(deviceId).deviceName(rel != null ? rel.getCustomerName() : null)
+                            .deviceType("consumer").alarmLevel("consumer").alarmType(alarmType)
+                            .feederId(feederId).subsId(subsId).countyId(countyId).countyName(countyName)
                             .mvtransId(mvtransId).consumerId(consumerId)
                             .mvtransId(mvtransId).consumerId(consumerId)
                             .threshold(threshold)
                             .threshold(threshold)
                             .firstOverTime(state.getFirstOverTime())
                             .firstOverTime(state.getFirstOverTime())
@@ -179,8 +194,12 @@ public class SeConsumerVoltageServiceImpl {
         }
         }
     }
     }
 
 
-    private int calcConsumerVoltageStatus(BigDecimal ratio, String voltageLevel) {
-        if (ratio == null) return 0;
+    /**
+     * 计算用户电压越限状态
+     * @return 0正常 1一般越上 2严重越上 3一般越下 4严重越下
+     */
+    private int calcConsumerVoltageStatus(BigDecimal ratio) {
+        if (ratio == null || ratio.signum() <= 0) return 0;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_SEVERE_V_OVER) > 0) return 2;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_SEVERE_V_OVER) > 0) return 2;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_GENERAL_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_GENERAL_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_SEVERE_V_UNDER) < 0) return 4;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_SEVERE_V_UNDER) < 0) return 4;
@@ -188,6 +207,19 @@ public class SeConsumerVoltageServiceImpl {
         return 0;
         return 0;
     }
     }
 
 
+    /**
+     * 查区县名称
+     */
+    private String lookupCountyName(String countyId) {
+        if (countyId == null) return null;
+        for (Map.Entry<String, String[]> entry : SeSnapshotConstants.COUNTIES) {
+            if (countyId.equals(entry.getKey())) {
+                return entry.getValue()[1];
+            }
+        }
+        return null;
+    }
+
     private String resolveConsumerAlarmType(int status) {
     private String resolveConsumerAlarmType(int status) {
         switch (status) {
         switch (status) {
             case 1: return "voltage_over_general";
             case 1: return "voltage_over_general";
@@ -207,4 +239,4 @@ public class SeConsumerVoltageServiceImpl {
             default: return null;
             default: return null;
         }
         }
     }
     }
-}
+}

+ 18 - 18
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java

@@ -153,7 +153,7 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
      * 返回 int[2]: [0]=重载事件数, [1]=过载事件数
      * 返回 int[2]: [0]=重载事件数, [1]=过载事件数
      */
      */
     private int[] countCurrentEvents(String id, Integer type,
     private int[] countCurrentEvents(String id, Integer type,
-                                     LocalDateTime start, LocalDateTime end) {
+                                      LocalDateTime start, LocalDateTime end) {
         LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
         LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, start)
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, start)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, end)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, end)
@@ -209,23 +209,23 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
         // 一条 SQL 聚合 16个count字段 + total_over_limit_hours
         // 一条 SQL 聚合 16个count字段 + total_over_limit_hours
         QueryWrapper<FhzgDwmVoltageOverLimitDaily> qw = new QueryWrapper<>();
         QueryWrapper<FhzgDwmVoltageOverLimitDaily> qw = new QueryWrapper<>();
         qw.select(
         qw.select(
-                        "COALESCE(SUM(over_upper_severe_tg_count),0) AS over_upper_severe_tg_count",
-                        "COALESCE(SUM(over_upper_severe_cust_count),0) AS over_upper_severe_cust_count",
-                        "COALESCE(SUM(over_upper_severe_feeder_count),0) AS over_upper_severe_feeder_count",
-                        "COALESCE(SUM(over_upper_severe_generatrix_count),0) AS over_upper_severe_generatrix_count",
-                        "COALESCE(SUM(over_upper_mild_tg_count),0) AS over_upper_mild_tg_count",
-                        "COALESCE(SUM(over_upper_mild_cust_count),0) AS over_upper_mild_cust_count",
-                        "COALESCE(SUM(over_upper_mild_feeder_count),0) AS over_upper_mild_feeder_count",
-                        "COALESCE(SUM(over_upper_mild_generatrix_count),0) AS over_upper_mild_generatrix_count",
-                        "COALESCE(SUM(over_lower_severe_tg_count),0) AS over_lower_severe_tg_count",
-                        "COALESCE(SUM(over_lower_severe_cust_count),0) AS over_lower_severe_cust_count",
-                        "COALESCE(SUM(over_lower_severe_feeder_count),0) AS over_lower_severe_feeder_count",
-                        "COALESCE(SUM(over_lower_severe_generatrix_count),0) AS over_lower_severe_generatrix_count",
-                        "COALESCE(SUM(over_lower_mild_tg_count),0) AS over_lower_mild_tg_count",
-                        "COALESCE(SUM(over_lower_mild_cust_count),0) AS over_lower_mild_cust_count",
-                        "COALESCE(SUM(over_lower_mild_feeder_count),0) AS over_lower_mild_feeder_count",
-                        "COALESCE(SUM(over_lower_mild_generatrix_count),0) AS over_lower_mild_generatrix_count",
-                        "COALESCE(SUM(total_over_limit_hours),0) AS total_over_limit_hours")
+                "COALESCE(SUM(over_upper_severe_tg_count),0) AS over_upper_severe_tg_count",
+                "COALESCE(SUM(over_upper_severe_cust_count),0) AS over_upper_severe_cust_count",
+                "COALESCE(SUM(over_upper_severe_feeder_count),0) AS over_upper_severe_feeder_count",
+                "COALESCE(SUM(over_upper_severe_generatrix_count),0) AS over_upper_severe_generatrix_count",
+                "COALESCE(SUM(over_upper_mild_tg_count),0) AS over_upper_mild_tg_count",
+                "COALESCE(SUM(over_upper_mild_cust_count),0) AS over_upper_mild_cust_count",
+                "COALESCE(SUM(over_upper_mild_feeder_count),0) AS over_upper_mild_feeder_count",
+                "COALESCE(SUM(over_upper_mild_generatrix_count),0) AS over_upper_mild_generatrix_count",
+                "COALESCE(SUM(over_lower_severe_tg_count),0) AS over_lower_severe_tg_count",
+                "COALESCE(SUM(over_lower_severe_cust_count),0) AS over_lower_severe_cust_count",
+                "COALESCE(SUM(over_lower_severe_feeder_count),0) AS over_lower_severe_feeder_count",
+                "COALESCE(SUM(over_lower_severe_generatrix_count),0) AS over_lower_severe_generatrix_count",
+                "COALESCE(SUM(over_lower_mild_tg_count),0) AS over_lower_mild_tg_count",
+                "COALESCE(SUM(over_lower_mild_cust_count),0) AS over_lower_mild_cust_count",
+                "COALESCE(SUM(over_lower_mild_feeder_count),0) AS over_lower_mild_feeder_count",
+                "COALESCE(SUM(over_lower_mild_generatrix_count),0) AS over_lower_mild_generatrix_count",
+                "COALESCE(SUM(total_over_limit_hours),0) AS total_over_limit_hours")
                 .eq("dim_level", dimLevel)
                 .eq("dim_level", dimLevel)
                 .ge("over_date", startDate.format(DATE_FMT))
                 .ge("over_date", startDate.format(DATE_FMT))
                 .le("over_date", endDate.format(DATE_FMT));
                 .le("over_date", endDate.format(DATE_FMT));

+ 145 - 73
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
+import com.hdkj.lt.bf.entity.DwdShbDsTransformerBase;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
@@ -19,6 +20,7 @@ import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.DwdShbDsTransformerBaseMapper;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
 import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
@@ -44,6 +46,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
     private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeVoltageEventMapper voltageEventMapper;
     private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final DwdShbDsTransformerBaseMapper transformerBaseMapper;
     private final FeederGroupApplication feederGroupApplication;
     private final FeederGroupApplication feederGroupApplication;
     private final ApplicationEventPublisher eventPublisher;
     private final ApplicationEventPublisher eventPublisher;
 
 
@@ -87,13 +90,21 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
 
 
         // === 2. 配变 ===
         // === 2. 配变 ===
         JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
         JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
-        if (mvtrans != null) {
+        Map<String, String> transformerNameMap = new HashMap<>();
+        if (mvtrans != null && !mvtrans.isEmpty()) {
+            // 先收集当前断面所有配变 ID,再一次性查名称(避免全表加载)
+            Set<String> mvtransIds = new HashSet<>();
+            for (int i = 0; i < mvtrans.size(); i++) {
+                String id = mvtrans.getJSONObject(i).getString("psrId");
+                if (id != null) mvtransIds.add(id);
+            }
+            transformerNameMap = buildTransformerMapping(mvtransIds);
             for (int i = 0; i < mvtrans.size(); i++) {
             for (int i = 0; i < mvtrans.size(); i++) {
                 JSONObject m = mvtrans.getJSONObject(i);
                 JSONObject m = mvtrans.getJSONObject(i);
                 FhzgSeSnapshotDetail d = parseMvtrans(m, snapTime, feederMap);
                 FhzgSeSnapshotDetail d = parseMvtrans(m, snapTime, feederMap);
                 if (d != null) {
                 if (d != null) {
                     batch.add(d);
                     batch.add(d);
-                    processMvtransMonitor(m, d, snapTime, feederMap);
+                    processMvtransMonitor(m, d, snapTime, feederMap, transformerNameMap);
                 }
                 }
             }
             }
         }
         }
@@ -143,7 +154,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
     private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
-                                      LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
         if (detail.getCurrentStatus() == null) return;
         if (detail.getCurrentStatus() == null) return;
         String feederId = detail.getFeederId();
         String feederId = detail.getFeederId();
         String[] loc = feederMap.get(feederId);
         String[] loc = feederMap.get(feederId);
@@ -168,6 +179,15 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         } else {
         } else {
             LocalDateTime lastSnap = state.getLastSnapTime();
             LocalDateTime lastSnap = state.getLastSnapTime();
             if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
             if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
+                // 断档间隙:先关掉还在活跃的事件,再清空状态机引用
+                if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeCurrentEvent close = new FhzgSeCurrentEvent();
+                    close.setId(state.getAlarmEventId());
+                    close.setStatus(2);
+                    close.setRecoverTime(snapTime);
+                    close.setLastLoadRate(loadRate);
+                    currentEventMapper.updateById(close);
+                }
                 state.setFirstOverTime(null);
                 state.setFirstOverTime(null);
                 state.setAlarmTriggered(0);
                 state.setAlarmTriggered(0);
                 state.setAlarmEventId(null);
                 state.setAlarmEventId(null);
@@ -200,13 +220,33 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                             event.getId(), feederId, feederName, countyId, subsId,
                             event.getId(), feederId, feederName, countyId, subsId,
                             alarmType, maxLrTime, state.getFirstOverTime()));
                             alarmType, maxLrTime, state.getFirstOverTime()));
                 }
                 }
+                // 活跃事件:断面负载率比记录值高则刷新 maxLoadRate
+                if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeCurrentEvent cur = currentEventMapper.selectById(state.getAlarmEventId());
+                    if (cur != null) {
+                        BigDecimal curMax = cur.getMaxLoadRate();
+                        if (loadRate != null && (curMax == null || loadRate.compareTo(curMax) > 0)) {
+                            FhzgSeCurrentEvent upd = new FhzgSeCurrentEvent();
+                            upd.setId(state.getAlarmEventId());
+                            upd.setMaxLoadRate(loadRate);
+                            currentEventMapper.updateById(upd);
+                        }
+                    }
+                }
             } else {
             } else {
                 if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
                 if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
-                    FhzgSeCurrentEvent update = new FhzgSeCurrentEvent();
-                    update.setId(state.getAlarmEventId());
-                    update.setStatus(2); update.setRecoverTime(snapTime);
-                    update.setLastLoadRate(loadRate); update.setMaxLoadRate(maxLoadRate);
-                    currentEventMapper.updateById(update);
+                    FhzgSeCurrentEvent cur = currentEventMapper.selectById(state.getAlarmEventId());
+                    if (cur != null) {
+                        BigDecimal trueMax = cur.getMaxLoadRate();
+                        if (trueMax == null || (maxLoadRate != null && maxLoadRate.compareTo(trueMax) > 0)) {
+                            trueMax = maxLoadRate;
+                        }
+                        FhzgSeCurrentEvent update = new FhzgSeCurrentEvent();
+                        update.setId(state.getAlarmEventId());
+                        update.setStatus(2); update.setRecoverTime(snapTime);
+                        update.setLastLoadRate(loadRate); update.setMaxLoadRate(trueMax);
+                        currentEventMapper.updateById(update);
+                    }
                 }
                 }
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
             }
             }
@@ -216,11 +256,12 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
     private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
-                                      LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
         if (detail.getVoltageStatus() == null) return;
         if (detail.getVoltageStatus() == null) return;
         String[] loc = feederMap.get(detail.getFeederId());
         String[] loc = feederMap.get(detail.getFeederId());
-        checkVoltageMonitor(detail.getFeederId(), "feeder", detail.getVoltageRatio(), detail.getVoltageStatus(),
-                snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
+        String feederName = loc != null && loc.length > 2 ? loc[2] : null;
+        checkVoltageMonitor(detail.getFeederId(), "feeder", feederName, detail.getVoltageRatio(), detail.getVoltageStatus(),
+                snapTime, detail.getFeederId(), feederName,
                 loc != null ? loc[0] : "", loc != null ? loc[1] : "", null, null);
                 loc != null ? loc[0] : "", loc != null ? loc[1] : "", null, null);
     }
     }
 
 
@@ -247,14 +288,17 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private void processMvtransMonitor(JSONObject m, FhzgSeSnapshotDetail detail,
     private void processMvtransMonitor(JSONObject m, FhzgSeSnapshotDetail detail,
-                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                        LocalDateTime snapTime, Map<String, String[]> feederMap,
+                                        Map<String, String> transformerNameMap) {
         if (detail.getVoltageStatus() == null) return;
         if (detail.getVoltageStatus() == null) return;
         String mvtransId = detail.getDeviceId();
         String mvtransId = detail.getDeviceId();
         String[] loc = feederMap.get(detail.getFeederId());
         String[] loc = feederMap.get(detail.getFeederId());
         int status = detail.getVoltageStatus();
         int status = detail.getVoltageStatus();
         if (status == 0) recoverUserAlarms(mvtransId, snapTime);
         if (status == 0) recoverUserAlarms(mvtransId, snapTime);
 
 
-        checkVoltageMonitor(mvtransId, "mvtrans", detail.getVoltageRatio(), status,
+        String transformerName = transformerNameMap != null
+                ? transformerNameMap.getOrDefault(mvtransId, mvtransId) : mvtransId;
+        checkVoltageMonitor(mvtransId, "mvtrans", transformerName, detail.getVoltageRatio(), status,
                 snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
                 snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
                 loc != null ? loc[0] : "", loc != null ? loc[1] : "", mvtransId, null);
                 loc != null ? loc[0] : "", loc != null ? loc[1] : "", mvtransId, null);
 
 
@@ -270,10 +314,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     // 电压状态机
     // 电压状态机
     // ============================================================
     // ============================================================
 
 
-    private void checkVoltageMonitor(String deviceId, String deviceType,
-                                     BigDecimal voltageRatio, int voltageStatus,
-                                     LocalDateTime snapTime, String feederId, String feederName,
-                                     String subsId, String countyId, String mvtransId, String consumerId) {
+    private void checkVoltageMonitor(String deviceId, String deviceType, String deviceName,
+                                      BigDecimal voltageRatio, int voltageStatus,
+                                      LocalDateTime snapTime, String feederId, String feederName,
+                                      String subsId, String countyId, String mvtransId, String consumerId) {
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
@@ -293,6 +337,13 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         } else {
         } else {
             LocalDateTime lastSnap = state.getLastSnapTime();
             LocalDateTime lastSnap = state.getLastSnapTime();
             if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
             if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
+                // 断档间隙:先关掉还在活跃的事件,再清空状态机引用
+                if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeVoltageEvent close = new FhzgSeVoltageEvent();
+                    close.setId(state.getAlarmEventId());
+                    close.setStatus(2); close.setRecoverTime(snapTime); close.setLastValue(voltageRatio);
+                    voltageEventMapper.updateById(close);
+                }
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
             }
             }
             if (isOverLimit) {
             if (isOverLimit) {
@@ -300,62 +351,60 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                 if (state.getFirstOverTime() != null
                 if (state.getFirstOverTime() != null
                         && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
                         && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
                         && state.getAlarmTriggered() == 0) {
                         && state.getAlarmTriggered() == 0) {
-                    String countyName = lookupCountyName(countyId, null);
-
-                    if ("feeder".equals(deviceType)) {
-                        // === 馈线自身越限 → 创建 feeder 级独立事件 ===
-                        String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
-                        BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
-                        BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
-                        int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
-                        FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                .deviceId(deviceId).deviceName(feederName)
-                                .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
-                                .feederId(feederId).feederName(feederName)
-                                .subsId(subsId).countyId(countyId).countyName(countyName)
-                                .threshold(threshold)
-                                .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
-                                .triggerValue(voltageRatio)
-                                .voltageRangeMax(vr[0]).voltageRangeMin(vr[1])
-                                .overUpperMvtrans(mc[0]).overLowerMvtrans(mc[1])
-                                .lastValue(voltageRatio).status(1).build();
-                        voltageEventMapper.insert(event);
-                        state.setAlarmTriggered(1); state.setAlarmEventId(event.getId());
-                        // 馈线越下限 → 异步触发重构算法
-                        if ("feeder".equals(deviceType) && "voltage_under".equals(alarmType)) {
-                            eventPublisher.publishEvent(new ReconTriggerEvent(
-                                    event.getId(), feederId, feederName, countyId, subsId,
-                                    alarmType, snapTime, state.getFirstOverTime()));
+                            String countyName = lookupCountyName(countyId, null);
+
+                            if ("feeder".equals(deviceType)) {
+                                // === 馈线自身越限 → 创建 feeder 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
+                                int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(deviceName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio)
+                                        .voltageRangeMax(vr[0]).voltageRangeMin(vr[1])
+                                        .overUpperMvtrans(mc[0]).overLowerMvtrans(mc[1])
+                                        .lastValue(voltageRatio).status(1).build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1); state.setAlarmEventId(event.getId());
+                                // 馈线越下限 → 异步触发重构算法
+                                if ("feeder".equals(deviceType) && "voltage_under".equals(alarmType)) {
+                                    eventPublisher.publishEvent(new ReconTriggerEvent(
+                                            event.getId(), feederId, feederName, countyId, subsId,
+                                            alarmType, snapTime, state.getFirstOverTime()));
+                                }
+                            } else if ("mvtrans".equals(deviceType)) {
+                                // === 台区越限 → 创建 mvtrans 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(deviceName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .mvtransId(mvtransId)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio).lastValue(voltageRatio).status(1)
+                                        .build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1);
+                                state.setAlarmEventId(event.getId());
+                            }
+                            // consumer 级别暂走旧逻辑(如有可后续优化)
                         }
                         }
-                    } else if ("mvtrans".equals(deviceType)) {
-                        // === 台区越限 → 创建 mvtrans 级独立事件 ===
-                        String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
-                        BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
-                        FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                .deviceId(deviceId).deviceName(feederName)
-                                .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
-                                .feederId(feederId).feederName(feederName)
-                                .subsId(subsId).countyId(countyId).countyName(countyName)
-                                .mvtransId(mvtransId)
-                                .threshold(threshold)
-                                .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
-                                .triggerValue(voltageRatio).lastValue(voltageRatio).status(1)
-                                .build();
-                        voltageEventMapper.insert(event);
-                        state.setAlarmTriggered(1);
-                        state.setAlarmEventId(event.getId());
-                    }
-                    // consumer 级别暂走旧逻辑(如有可后续优化)
-                }
             } else {
             } else {
+                // 设备恢复正常(任何 deviceType 都走):关自己的事件
                 if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
                 if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
-                    // 只有 feeder 自身恢复才标聚合事件 status=2;mvtrans 恢复不动聚合事件
-                    if ("feeder".equals(deviceType)) {
-                        FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
-                        update.setId(state.getAlarmEventId());
-                        update.setStatus(2); update.setRecoverTime(snapTime); update.setLastValue(voltageRatio);
-                        voltageEventMapper.updateById(update);
-                    }
+                    FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+                    update.setId(state.getAlarmEventId());
+                    update.setStatus(2); update.setRecoverTime(snapTime); update.setLastValue(voltageRatio);
+                    voltageEventMapper.updateById(update);
                 }
                 }
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
             }
             }
@@ -434,14 +483,14 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private int calcFeederVoltageStatus(BigDecimal ratio) {
     private int calcFeederVoltageStatus(BigDecimal ratio) {
-        if (ratio == null) return 0;
+        if (ratio == null || ratio.signum() <= 0) return 0;
         if (ratio.compareTo(SeMonitorThreshold.FEEDER_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.FEEDER_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.FEEDER_V_UNDER) < 0) return 2;
         if (ratio.compareTo(SeMonitorThreshold.FEEDER_V_UNDER) < 0) return 2;
         return 0;
         return 0;
     }
     }
 
 
     private int calcMvtransVoltageStatus(BigDecimal ratio) {
     private int calcMvtransVoltageStatus(BigDecimal ratio) {
-        if (ratio == null) return 0;
+        if (ratio == null || ratio.signum() <= 0) return 0;
         if (ratio.compareTo(SeMonitorThreshold.MVTRANS_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.MVTRANS_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.MVTRANS_V_UNDER) < 0) return 2;
         if (ratio.compareTo(SeMonitorThreshold.MVTRANS_V_UNDER) < 0) return 2;
         return 0;
         return 0;
@@ -547,8 +596,31 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         return map;
         return map;
     }
     }
 
 
+    /**
+     * 按配变 ID 集合批量查名称(只查当前断面出现的,不加载全表)
+     * 查不到的设备以 ID 代替
+     */
+    private Map<String, String> buildTransformerMapping(Collection<String> ids) {
+        if (ids == null || ids.isEmpty()) return Collections.emptyMap();
+        Map<String, String> map = new HashMap<>();
+        try {
+            List<DwdShbDsTransformerBase> rows = transformerBaseMapper.selectList(
+                    new LambdaQueryWrapper<DwdShbDsTransformerBase>()
+                            .in(DwdShbDsTransformerBase::getPsrId, ids)
+                            .isNotNull(DwdShbDsTransformerBase::getName));
+            for (DwdShbDsTransformerBase row : rows) {
+                map.put(row.getPsrId(), row.getName());
+            }
+            log.info("配变名称映射加载完成: 配变{}台, 命中{}条, 未命中{}条",
+                    ids.size(), map.size(), ids.size() - map.size());
+        } catch (Exception e) {
+            log.warn("加载配变名称映射失败", e);
+        }
+        return map;
+    }
+
     private void buildFeederMappingRecursive(List<FeederGroupTreeNode> nodes, Map<String, String[]> map,
     private void buildFeederMappingRecursive(List<FeederGroupTreeNode> nodes, Map<String, String[]> map,
-                                             String parentSubsId, String parentCountyId) {
+                                              String parentSubsId, String parentCountyId) {
         if (nodes == null) return;
         if (nodes == null) return;
         for (FeederGroupTreeNode node : nodes) {
         for (FeederGroupTreeNode node : nodes) {
             String subsId = parentSubsId, countyId = parentCountyId;
             String subsId = parentSubsId, countyId = parentCountyId;

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

@@ -5,12 +5,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
+import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
+import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
 import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
 import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
+import com.alibaba.fastjson.JSON;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -50,6 +53,7 @@ public class StaticGridDashboardServiceImpl
     private final StaticIndicatorMetricMapper metricMapper;
     private final StaticIndicatorMetricMapper metricMapper;
     private final SjztPwwyyqxtPrmPdrOutageMvDsService outageDurationService;
     private final SjztPwwyyqxtPrmPdrOutageMvDsService outageDurationService;
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
+    private final StaticProblemDetailMapper problemDetailMapper;
 
 
     // ============================================================
     // ============================================================
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
@@ -144,6 +148,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
+        vo.setProblemDetails(buildProblemDetails(result.getId()));
         return vo;
         return vo;
     }
     }
 
 
@@ -174,6 +179,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, substationId, null, ym);
         fillCapacityMetrics(vo, null, substationId, null, ym);
+        vo.setProblemDetails(buildProblemDetails(resultId));
         return vo;
         return vo;
     }
     }
 
 
@@ -204,6 +210,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, null, feederId, ym);
         fillCapacityMetrics(vo, null, null, feederId, ym);
+        vo.setProblemDetails(buildProblemDetails(resultId));
         return vo;
         return vo;
     }
     }
 
 
@@ -318,6 +325,9 @@ public class StaticGridDashboardServiceImpl
                 .sameBusInterconnectionIssueCount(toInteger(metricMap.get("same_bus_interconnection_issue_count")))
                 .sameBusInterconnectionIssueCount(toInteger(metricMap.get("same_bus_interconnection_issue_count")))
                 .excessiveTiePointFeederCount(toInteger(metricMap.get("excessive_tie_point_feeder_count")))
                 .excessiveTiePointFeederCount(toInteger(metricMap.get("excessive_tie_point_feeder_count")))
                 .singleRadialFeederCount(toInteger(metricMap.get("single_radial_feeder_count")))
                 .singleRadialFeederCount(toInteger(metricMap.get("single_radial_feeder_count")))
+                .noConnectionFeederCount(calcNoConnectionFeederCount(
+                        metricMap.get("feeder_total_count"),
+                        metricMap.get("tied_feeder_total_count")))
                 .largeBranchFeederCount(toInteger(metricMap.get("large_branch_issue_feeder_count")))
                 .largeBranchFeederCount(toInteger(metricMap.get("large_branch_issue_feeder_count")))
                 .unreasonableSegmentCount(toInteger(metricMap.get("unreasonable_segment_count_feeder_count")))
                 .unreasonableSegmentCount(toInteger(metricMap.get("unreasonable_segment_count_feeder_count")))
                 .mainTrunkSegmentDenseCount(toInteger(metricMap.get("main_trunk_lantern_issue_feeder_count")))
                 .mainTrunkSegmentDenseCount(toInteger(metricMap.get("main_trunk_lantern_issue_feeder_count")))
@@ -332,8 +342,6 @@ public class StaticGridDashboardServiceImpl
                 .mainTrunkSectionMismatchCount(toInteger(metricMap.get("main_trunk_bottleneck_feeder_count")))
                 .mainTrunkSectionMismatchCount(toInteger(metricMap.get("main_trunk_bottleneck_feeder_count")))
                 .connectedCapacityExceedFeederCount(toInteger(metricMap.get("connected_capacity_exceed_feeder_count")))
                 .connectedCapacityExceedFeederCount(toInteger(metricMap.get("connected_capacity_exceed_feeder_count")))
                 .loadGroupCapacityAnomalyCount(toInteger(metricMap.get("load_group_capacity_anomaly_count")))
                 .loadGroupCapacityAnomalyCount(toInteger(metricMap.get("load_group_capacity_anomaly_count")))
-                .comprehensiveLossRate(null)
-                .powerFactorPassRate(null)
                 .build();
                 .build();
     }
     }
 
 
@@ -433,6 +441,48 @@ public class StaticGridDashboardServiceImpl
         }
         }
     }
     }
 
 
+    /**
+     * 无联络线路数 = 馈线总条数 - 有联络馈线总条数
+     */
+    private Integer calcNoConnectionFeederCount(String feederTotalStr, String tiedFeederTotalStr) {
+        Integer total = toInteger(feederTotalStr);
+        Integer tied = toInteger(tiedFeederTotalStr);
+        if (total == null || tied == null) return null;
+        return total - tied;
+    }
+
+    /**
+     * 查询问题详情列表
+     */
+    private List<StaticGridDashboardVO.ProblemDetailItem> buildProblemDetails(Long resultId) {
+        if (resultId == null) return Collections.emptyList();
+        LambdaQueryWrapper<StaticProblemDetail> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StaticProblemDetail::getResultId, resultId);
+        List<StaticProblemDetail> details = problemDetailMapper.selectList(wrapper);
+        if (details == null || details.isEmpty()) return Collections.emptyList();
+        return details.stream().map(d -> {
+            List<String> deviceIds = null;
+            if (StringUtils.isNotBlank(d.getObjectId())) {
+                try {
+                    deviceIds = JSON.parseArray(d.getObjectId(), String.class);
+                } catch (Exception e) {
+                    log.warn("解析deviceIds失败 objectId={}", d.getObjectId());
+                }
+            }
+            return StaticGridDashboardVO.ProblemDetailItem.builder()
+                    .problemId(d.getProblemId())
+                    .feederId(d.getFeederId())
+                    .categoryName(d.getCategoryName())
+                    .metricName(d.getMetricName())
+                    .severity(d.getSeverity())
+                    .objectType(d.getObjectType())
+                    .actualValue(d.getActualValue())
+                    .reason(d.getDescription())
+                    .deviceIds(deviceIds)
+                    .build();
+        }).collect(Collectors.toList());
+    }
+
     private BigDecimal toBigDecimal(String value) {
     private BigDecimal toBigDecimal(String value) {
         if (StringUtils.isBlank(value)) return null;
         if (StringUtils.isBlank(value)) return null;
         try {
         try {
@@ -504,16 +554,18 @@ public class StaticGridDashboardServiceImpl
             vo.setRegionalAvgLoadRate(BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
             vo.setRegionalAvgLoadRate(BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
         }
         }
 
 
-        // 过载线路数
+        // 线路重过载条数(重载 ≥80% + 过载 ≥100%)
         if (feederId != null) {
         if (feederId != null) {
-            // 单馈线:该月有任意过载记录即 1
-            boolean hasOverload = list.stream()
-                    .anyMatch(d -> d.getOverloadCount() != null && d.getOverloadCount() > 0);
-            vo.setLineOverloadCount(hasOverload ? 1 : 0);
+            // 单馈线:该月有任意重载或过载记录即 1
+            boolean hasHeavyOrOverload = list.stream()
+                    .anyMatch(d -> (d.getHeavyCount() != null && d.getHeavyCount() > 0)
+                            || (d.getOverloadCount() != null && d.getOverloadCount() > 0));
+            vo.setLineOverloadCount(hasHeavyOrOverload ? 1 : 0);
         } else {
         } else {
-            // 区县/变电站:去重有过载记录的馈线数
+            // 区县/变电站:去重有重载或过载记录的馈线数
             long count = list.stream()
             long count = list.stream()
-                    .filter(d -> d.getOverloadCount() != null && d.getOverloadCount() > 0)
+                    .filter(d -> (d.getHeavyCount() != null && d.getHeavyCount() > 0)
+                            || (d.getOverloadCount() != null && d.getOverloadCount() > 0))
                     .map(FhzgSeCapacityDaily::getFeederId)
                     .map(FhzgSeCapacityDaily::getFeederId)
                     .distinct()
                     .distinct()
                     .count();
                     .count();

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

@@ -117,19 +117,23 @@ public class StaticIndicatorResultServiceImpl
         save(mainRecord);
         save(mainRecord);
         Long resultId = mainRecord.getId();
         Long resultId = mainRecord.getId();
 
 
-        // 4. 写指标明细 — county 级
-        List<StaticIndicatorMetric> metricList = new ArrayList<StaticIndicatorMetric>();
+        // 4-5. 写指标明细 + 问题详情
+        List<StaticIndicatorMetric> metricList = new ArrayList<>();
+        List<StaticProblemDetail> problemDetailList = new ArrayList<>();
+
+        // county 级
         if (countyResult != null) {
         if (countyResult != null) {
             JSONArray categories = countyResult.getJSONArray("categories");
             JSONArray categories = countyResult.getJSONArray("categories");
             if (categories != null) {
             if (categories != null) {
                 for (int i = 0; i < categories.size(); i++) {
                 for (int i = 0; i < categories.size(); i++) {
                     JSONObject category = categories.getJSONObject(i);
                     JSONObject category = categories.getJSONObject(i);
-                    parseCategoryMetrics(category, resultId, "county", null, null, null, null, null, null, metricList);
+                    parseCategoryMetrics(category, resultId, "county", null, null,
+                            null, null, null, null, metricList, problemDetailList);
                 }
                 }
             }
             }
         }
         }
 
 
-        // 5. 写指标明细 — feeder 级
+        // feeder 级
         if (calcResult != null) {
         if (calcResult != null) {
             JSONArray feederResults = calcResult.getJSONArray("feeder_results");
             JSONArray feederResults = calcResult.getJSONArray("feeder_results");
             if (feederResults != null) {
             if (feederResults != null) {
@@ -148,49 +152,24 @@ public class StaticIndicatorResultServiceImpl
                             parseCategoryMetrics(category, resultId, "feeder", feederId,
                             parseCategoryMetrics(category, resultId, "feeder", feederId,
                                     feederName, sourceSubstationId, sourceSubstationName,
                                     feederName, sourceSubstationId, sourceSubstationName,
                                     sourceBusId, supplyAreaType,
                                     sourceBusId, supplyAreaType,
-                                    metricList);
+                                    metricList, problemDetailList);
                         }
                         }
                     }
                     }
                 }
                 }
             }
             }
         }
         }
+
+        // 批量插入指标明细
         for (StaticIndicatorMetric m : metricList) {
         for (StaticIndicatorMetric m : metricList) {
             metricMapper.insert(m);
             metricMapper.insert(m);
         }
         }
         log.info("静态网架指标明细入库完成 resultId={}, metricCount={}", resultId, metricList.size());
         log.info("静态网架指标明细入库完成 resultId={}, metricCount={}", resultId, metricList.size());
 
 
-        // 6. 写问题详情
-        if (calcResult != null) {
-            JSONArray problemDetails = calcResult.getJSONArray("problem_details");
-            if (problemDetails != null) {
-                int problemCount = 0;
-                for (int i = 0; i < problemDetails.size(); i++) {
-                    JSONObject p = problemDetails.getJSONObject(i);
-                    JSONObject actualValues = p.getJSONObject("actual_values");
-                    String actualValue = actualValues != null
-                            ? JSON.toJSONString(actualValues.get("value")) : null;
-
-                    StaticProblemDetail detail = StaticProblemDetail.builder()
-                            .resultId(resultId)
-                            .problemId(p.getString("problem_id"))
-                            .feederId(p.getString("feeder_id"))
-                            .categoryCode(p.getString("category_code"))
-                            .categoryName(p.getString("category_name"))
-                            .metricCode(p.getString("metric_code"))
-                            .metricName(p.getString("metric_name"))
-                            .severity(p.getString("severity"))
-                            .objectType(p.getString("object_type"))
-                            .objectId(p.getString("object_id"))
-                            .actualValue(actualValue)
-                            .description(p.getString("description"))
-                            .suggestion(p.getString("suggestion"))
-                            .build();
-                    problemDetailMapper.insert(detail);
-                    problemCount++;
-                }
-                log.info("静态网架问题详情入库完成 resultId={}, problemCount={}", resultId, problemCount);
-            }
+        // 批量插入问题详情(从每个指标的 details[] 内嵌解析)
+        for (StaticProblemDetail d : problemDetailList) {
+            problemDetailMapper.insert(d);
         }
         }
+        log.info("静态网架问题详情入库完成 resultId={}, problemCount={}", resultId, problemDetailList.size());
 
 
         log.info("静态网架评估入库完成 batchId={}, countyId={}, resultId={}", batchId, countyId, resultId);
         log.info("静态网架评估入库完成 batchId={}, countyId={}, resultId={}", batchId, countyId, resultId);
         return resultId;
         return resultId;
@@ -241,7 +220,8 @@ public class StaticIndicatorResultServiceImpl
                                        String feederId, String feederName,
                                        String feederId, String feederName,
                                        String sourceSubstationId, String sourceSubstationName,
                                        String sourceSubstationId, String sourceSubstationName,
                                        String sourceBusId, String supplyAreaType,
                                        String sourceBusId, String supplyAreaType,
-                                       List<StaticIndicatorMetric> metricList) {
+                                       List<StaticIndicatorMetric> metricList,
+                                       List<StaticProblemDetail> problemDetailList) {
         String categoryCode = category.getString("category_code");
         String categoryCode = category.getString("category_code");
         String categoryName = category.getString("category_name");
         String categoryName = category.getString("category_name");
         JSONArray metrics = category.getJSONArray("metrics");
         JSONArray metrics = category.getJSONArray("metrics");
@@ -274,6 +254,34 @@ public class StaticIndicatorResultServiceImpl
                     .isProblem(m.getBoolean("is_problem") != null && m.getBoolean("is_problem") ? 1 : 0)
                     .isProblem(m.getBoolean("is_problem") != null && m.getBoolean("is_problem") ? 1 : 0)
                     .build();
                     .build();
             metricList.add(metric);
             metricList.add(metric);
+
+            // 提取该指标下的 details[] → t_static_problem_detail
+            JSONArray details = m.getJSONArray("details");
+            if (details != null && !details.isEmpty()) {
+                for (int j = 0; j < details.size(); j++) {
+                    JSONObject d = details.getJSONObject(j);
+                    JSONArray deviceIds = d.getJSONArray("device_ids");
+                    String deviceIdsStr = deviceIds != null && !deviceIds.isEmpty()
+                            ? JSON.toJSONString(deviceIds) : null;
+
+                    StaticProblemDetail detail = StaticProblemDetail.builder()
+                            .resultId(resultId)
+                            .problemId(level + "_" + m.getString("metric_code") + "_" + j)
+                            .feederId(feederId)
+                            .categoryCode(categoryCode)
+                            .categoryName(categoryName)
+                            .metricCode(m.getString("metric_code"))
+                            .metricName(m.getString("metric_name"))
+                            .severity(m.getString("severity"))
+                            .objectType("device")
+                            .objectId(deviceIdsStr)
+                            .actualValue(valueStr)
+                            .description(d.getString("reason"))
+                            .suggestion(d.getString("reason_code"))
+                            .build();
+                    problemDetailList.add(detail);
+                }
+            }
         }
         }
     }
     }
 }
 }