lisonglin 2 недель назад
Родитель
Сommit
699b440c89
19 измененных файлов с 742 добавлено и 280 удалено
  1. 35 0
      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. 6 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorCurrent.java
  5. 6 0
      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. 14 4
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeAlarmListVO.java
  9. 40 12
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeVoltageDashboardVO.java
  10. 11 14
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDashboardVO.java
  11. 17 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsTransformerBaseMapper.java
  12. 45 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/scheduler/SeVoltageCleanupScheduler.java
  13. 5 15
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeAlarmService.java
  14. 111 73
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java
  15. 41 9
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeConsumerVoltageServiceImpl.java
  16. 62 51
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  17. 118 25
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  18. 61 9
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java
  19. 45 37
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/impl/StaticIndicatorResultServiceImpl.java

+ 35 - 0
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;
+}

+ 6 - 0
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,11 +24,15 @@ 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;

+ 6 - 0
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,11 +30,15 @@ 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;
+}

+ 14 - 4
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeAlarmListVO.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.NoArgsConstructor;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 
 /**
 /**
  * 异常告警列表行 VO(线路重过载 / 电压越限共用)
  * 异常告警列表行 VO(线路重过载 / 电压越限共用)
@@ -27,21 +28,30 @@ public class SeAlarmListVO implements Serializable {
     @ApiModelProperty(value = "告警事件ID")
     @ApiModelProperty(value = "告警事件ID")
     private Long alarmEventId;
     private Long alarmEventId;
 
 
+    @ApiModelProperty(value = "馈线ID")
+    private String feederId;
+
     @ApiModelProperty(value = "线路名称")
     @ApiModelProperty(value = "线路名称")
     private String feederName;
     private String feederName;
 
 
-    @ApiModelProperty(value = "展示值: 负载率'102%' 或 电压越限类型'线路电压越下限'")
+    @ApiModelProperty(value = "展示值: 负载率'102' 或 电压方向'越上限/越下限'")
     private String alarmDisplayValue;
     private String alarmDisplayValue;
 
 
     @ApiModelProperty(value = "越限时间(firstOverTime)")
     @ApiModelProperty(value = "越限时间(firstOverTime)")
     private String alarmTime;
     private String alarmTime;
 
 
+    @ApiModelProperty(value = "告警持续时长 如'1.5h'")
+    private String alarmDuration;
+
     @ApiModelProperty(value = "告警类型: current_heavy/current_overload/voltage_over/voltage_under")
     @ApiModelProperty(value = "告警类型: current_heavy/current_overload/voltage_over/voltage_under")
     private String alarmType;
     private String alarmType;
 
 
-    @ApiModelProperty(value = "告警层级: feeder/mvtrans (前端用来决定颜色)")
+    @ApiModelProperty(value = "告警层级: feeder/mvtrans/consumer (前端用来决定颜色)")
     private String alarmLevel;
     private String alarmLevel;
 
 
-    @ApiModelProperty(value = "馈线ID")
-    private String feederId;
+    @ApiModelProperty(value = "设备名称(台区名/用户名),给台区列表展示")
+    private String deviceName;
+
+    @ApiModelProperty(value = "影响用户数(台区越限专用)")
+    private Integer affectedConsumerCount;
 }
 }

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

@@ -11,12 +11,11 @@ import lombok.NoArgsConstructor;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
-import java.util.List;
 
 
 /**
 /**
  * 电压质量指标 VO
  * 电压质量指标 VO
  * <p>
  * <p>
- * 今日:从 fhzg_se_alarm_event 查(馈线+台区,不分级别),展示当前越限数
+ * 今日:从 fhzg_se_voltage_event 查(馈线+台区+用户),分条/台/户数(去重) + 次数(事件总数)
  * 历史(当月/全年):从 ads_mrt_voltage_over_limit_daily_result 查(8维度×4级别=16字段)
  * 历史(当月/全年):从 ads_mrt_voltage_over_limit_daily_result 查(8维度×4级别=16字段)
  *
  *
  * @author lsl
  * @author lsl
@@ -32,20 +31,49 @@ public class SeVoltageDashboardVO implements Serializable {
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
     // ============================================================
     // ============================================================
-    // 今日(查 alarm_event,不分一般/严重)
+    // 今日统计(从 voltage_event 表实时查)
+    // 条数/台数/户数 = 去重的 deviceId
+    // 次数 = 事件总条数
     // ============================================================
     // ============================================================
 
 
-    @ApiModelProperty(value = "今日越上限台区数")
-    private Integer todayOverUpperAreaCount;
+    // --- 越上限:线路 ---
+    @ApiModelProperty(value = "越上限线路条数(去重)")
+    private Integer overUpperFeederCount;
+    @ApiModelProperty(value = "越上限线路事件次数")
+    private Integer overUpperFeederTimes;
 
 
-    @ApiModelProperty(value = "今日越下限台区数")
-    private Integer todayOverLowerAreaCount;
+    // --- 越上限:台区 ---
+    @ApiModelProperty(value = "越上限台区台数(去重)")
+    private Integer overUpperAreaCount;
+    @ApiModelProperty(value = "越上限台区事件次数")
+    private Integer overUpperAreaTimes;
 
 
-    @ApiModelProperty(value = "今日越上限馈线数")
-    private Integer todayOverUpperFeederCount;
+    // --- 越上限:用户 ---
+    @ApiModelProperty(value = "越上限用户户数(去重)")
+    private Integer overUpperConsumerCount;
+    @ApiModelProperty(value = "越上限用户事件次数")
+    private Integer overUpperConsumerTimes;
 
 
-    @ApiModelProperty(value = "今日越下限馈线数")
-    private Integer todayOverLowerFeederCount;
+    // --- 越下限:线路 ---
+    @ApiModelProperty(value = "越下限线路条数(去重)")
+    private Integer overLowerFeederCount;
+    @ApiModelProperty(value = "越下限线路事件次数")
+    private Integer overLowerFeederTimes;
+
+    // --- 越下限:台区 ---
+    @ApiModelProperty(value = "越下限台区台数(去重)")
+    private Integer overLowerAreaCount;
+    @ApiModelProperty(value = "越下限台区事件次数")
+    private Integer overLowerAreaTimes;
+
+    // --- 越下限:用户 ---
+    @ApiModelProperty(value = "越下限用户户数(去重)")
+    private Integer overLowerConsumerCount;
+    @ApiModelProperty(value = "越下限用户事件次数")
+    private Integer overLowerConsumerTimes;
+
+    @ApiModelProperty(value = "IEC 用户电压指数(待实现)")
+    private BigDecimal iecIndex;
 
 
     // ============================================================
     // ============================================================
     // 历史(当月/全年,查中台T-2 ads_mrt_voltage_over_limit_daily_result)
     // 历史(当月/全年,查中台T-2 ads_mrt_voltage_over_limit_daily_result)
@@ -98,4 +126,4 @@ public class SeVoltageDashboardVO implements Serializable {
     @ApiModelProperty(value = "越限总时长(小时)")
     @ApiModelProperty(value = "越限总时长(小时)")
     @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
     @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
     private BigDecimal totalOverLimitHours;
     private BigDecimal totalOverLimitHours;
-}
+}

+ 11 - 14
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);
-}
+}

+ 111 - 73
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;
@@ -21,6 +24,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatter;
@@ -58,30 +62,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,15 +94,16 @@ 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())
                 .feederId(e.getFeederId())
                 .feederId(e.getFeederId())
                 .alarmDisplayValue(displayValue)
                 .alarmDisplayValue(displayValue)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmDuration(calcDuration(e.getFirstOverTime(), e.getRecoverTime(), e.getStatus()))
                 .alarmType(e.getAlarmType())
                 .alarmType(e.getAlarmType())
                 .alarmLevel("feeder")
                 .alarmLevel("feeder")
                 .build();
                 .build();
@@ -108,29 +114,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());
@@ -138,20 +142,46 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     }
     }
 
 
     private SeAlarmListVO toVoltageVO(FhzgSeVoltageEvent e) {
     private SeAlarmListVO toVoltageVO(FhzgSeVoltageEvent e) {
-        String displayValue = resolveVoltageDisplay(e.getAlarmType(), e.getAlarmLevel());
+        String displayValue = resolveVoltageDirection(e.getAlarmType());
+        // 台区事件:查窗口内该配变下用户越限事件数(嵌套在配变窗口内的,不去重)
+        Integer consumerCount = null;
+        if ("mvtrans".equals(e.getAlarmLevel()) && e.getMvtransId() != null) {
+            LocalDateTime windowEnd = (e.getStatus() != null && e.getStatus() == 2 && e.getRecoverTime() != null)
+                    ? e.getRecoverTime() : LocalDateTime.now();
+            Long cnt = voltageEventMapper.selectCount(
+                    new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                            .eq(FhzgSeVoltageEvent::getAlarmLevel, "consumer")
+                            .eq(FhzgSeVoltageEvent::getMvtransId, e.getMvtransId())
+                            .ge(FhzgSeVoltageEvent::getFirstOverTime, e.getFirstOverTime())
+                            .lt(FhzgSeVoltageEvent::getFirstOverTime, windowEnd));
+            consumerCount = cnt != null ? cnt.intValue() : 0;
+        }
         return SeAlarmListVO.builder()
         return SeAlarmListVO.builder()
                 .alarmEventId(e.getId())
                 .alarmEventId(e.getId())
+                .deviceName(e.getDeviceName())
                 .feederName(e.getFeederName())
                 .feederName(e.getFeederName())
                 .feederId(e.getFeederId())
                 .feederId(e.getFeederId())
                 .alarmDisplayValue(displayValue)
                 .alarmDisplayValue(displayValue)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmDuration(calcDuration(e.getFirstOverTime(), e.getRecoverTime(), e.getStatus()))
                 .alarmType(e.getAlarmType())
                 .alarmType(e.getAlarmType())
                 .alarmLevel(e.getAlarmLevel())
                 .alarmLevel(e.getAlarmLevel())
+                .affectedConsumerCount(consumerCount)
                 .build();
                 .build();
     }
     }
 
 
     /**
     /**
-     * 展示标签:按 alarmType + alarmLevel 拼线路/台区电压越限
+     * 电压越限方向(列表用):只返回越上限/越下限,不带设备类型前缀
+     */
+    private String resolveVoltageDirection(String alarmType) {
+        if (alarmType == null) return "电压越限";
+        if (alarmType.contains("over")) return "越上限";
+        if (alarmType.contains("under")) return "越下限";
+        return "电压越限";
+    }
+
+    /**
+     * 展示标签(详情用):按 alarmType + alarmLevel 拼线路/台区电压越限
      */
      */
     private String resolveVoltageDisplay(String alarmType, String alarmLevel) {
     private String resolveVoltageDisplay(String alarmType, String alarmLevel) {
         boolean isOver = alarmType != null && alarmType.contains("over");
         boolean isOver = alarmType != null && alarmType.contains("over");
@@ -181,7 +211,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
             return buildOverloadDetail(currentEvent);
             return buildOverloadDetail(currentEvent);
         }
         }
 
 
-        // 电压事件(仅馈线级有详情,配变越限无详情)
+        // 电压事件
         FhzgSeVoltageEvent voltageEvent = voltageEventMapper.selectById(alarmEventId);
         FhzgSeVoltageEvent voltageEvent = voltageEventMapper.selectById(alarmEventId);
         if (voltageEvent != null && "feeder".equals(voltageEvent.getAlarmLevel())) {
         if (voltageEvent != null && "feeder".equals(voltageEvent.getAlarmLevel())) {
             return buildFeederDetail(voltageEvent);
             return buildFeederDetail(voltageEvent);
@@ -244,35 +274,12 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .build();
                 .build();
     }
     }
 
 
-//    /**
-//     * 单配变事件详情:展示该配变自身信息
-//     */
-//    private SeAlarmDetailVO buildMvtransDetail(FhzgSeVoltageEvent event) {
-//        LocalDateTime endTime = event.getRecoverTime() != null ? event.getRecoverTime()
-//                : (event.getTriggerTime() != null ? event.getTriggerTime() : event.getFirstOverTime());
-//        return SeAlarmDetailVO.builder()
-//                .alarmEventId(event.getId())
-//                .alarmType(event.getAlarmType())
-//                .alarmCategory("voltage")
-//                .countyName(event.getCountyName())
-//                .feederName(event.getFeederName())
-//                .alarmStartTime(event.getFirstOverTime())
-//                .alarmEndTime(endTime)
-//                .abnormalIndicator(resolveVoltageDisplay(event.getAlarmType(), event.getAlarmLevel()))
-//                .build();
-//    }
+    // ============================================================
+    // 4. 重过载事件详情
+    // ============================================================
 
 
     private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
     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();
-
+        String abnormalIndicator = resolveOverloadDisplay(event.getAlarmType());
         return SeAlarmDetailVO.builder()
         return SeAlarmDetailVO.builder()
                 .alarmEventId(event.getId())
                 .alarmEventId(event.getId())
                 .alarmType(event.getAlarmType())
                 .alarmType(event.getAlarmType())
@@ -280,27 +287,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++;
         }
         }
@@ -378,8 +399,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 {
@@ -387,6 +413,18 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         }
         }
     }
     }
 
 
+    /**
+     * 计算告警持续时长
+     * @return 如 "1.5h",已结束用 recoverTime,活跃用当前时间
+     */
+    private String calcDuration(LocalDateTime start, LocalDateTime end, Integer status) {
+        if (start == null) return null;
+        LocalDateTime actualEnd = (status != null && status == 2 && end != null) ? end : LocalDateTime.now();
+        long minutes = Duration.between(start, actualEnd).toMinutes();
+        if (minutes < 0) minutes = 0;
+        return String.format("%.1fh", minutes / 60.0);
+    }
+
     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) {
@@ -398,4 +436,4 @@ public class SeAlarmServiceImpl implements SeAlarmService {
             }
             }
         }
         }
     }
     }
-}
+}

+ 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;
         }
         }
     }
     }
-}
+}

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

@@ -100,52 +100,63 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     }
     }
 
 
     /**
     /**
-     * 今日电压:线路越限数从 feeder 事件 (alarm_level='feeder') 统计,
-     * 台区越限数从 mvtrans 事件 (alarm_level='mvtrans') 直接 COUNT。
-     * 按 firstOverTime 今日范围 + status=1(活跃)过滤。
+     * 今日电压:按 alarmLevel (feeder/mvtrans/consumer) 分别统计
+     * - 条/台/户数 = 去重 deviceId(一次越限就算一个设备)
+     * - 次数 = 事件总数(一天内可多次)
+     * 按 firstOverTime 今日范围过滤。
      */
      */
     private SeVoltageDashboardVO queryVoltageToday(String id, Integer type) {
     private SeVoltageDashboardVO queryVoltageToday(String id, Integer type) {
         LocalDateTime todayStart = LocalDate.now().atStartOfDay();
         LocalDateTime todayStart = LocalDate.now().atStartOfDay();
         LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
         LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
 
 
-        // == 线路越限事件统计(alarm_level='feeder')==
-        LambdaQueryWrapper<FhzgSeVoltageEvent> feederWrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
-                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder")
-                .ge(FhzgSeVoltageEvent::getFirstOverTime, todayStart)
-                .lt(FhzgSeVoltageEvent::getFirstOverTime, tomorrowStart);
-        applyNodeFilter(feederWrapper, id, type);
-        List<FhzgSeVoltageEvent> feederEvents = voltageEventMapper.selectList(feederWrapper);
+        // 三个维度分别查
+        int[] feederStats = countVoltageByAlarmLevel(id, type, "feeder", todayStart, tomorrowStart);
+        int[] mvtransStats = countVoltageByAlarmLevel(id, type, "mvtrans", todayStart, tomorrowStart);
+        int[] consumerStats = countVoltageByAlarmLevel(id, type, "consumer", todayStart, tomorrowStart);
 
 
-        int overUpperFeeder = 0, overLowerFeeder = 0;
-        for (FhzgSeVoltageEvent e : feederEvents) {
-            String t = e.getAlarmType();
-            if (t == null) continue;
-            if (t.contains("over")) overUpperFeeder++;
-            if (t.contains("under")) overLowerFeeder++;
-        }
+        return SeVoltageDashboardVO.builder()
+                // 越上限:线路/台区/用户
+                .overUpperFeederCount(feederStats[0]).overUpperFeederTimes(feederStats[2])
+                .overUpperAreaCount(mvtransStats[0]).overUpperAreaTimes(mvtransStats[2])
+                .overUpperConsumerCount(consumerStats[0]).overUpperConsumerTimes(consumerStats[2])
+                // 越下限:线路/台区/用户
+                .overLowerFeederCount(feederStats[1]).overLowerFeederTimes(feederStats[3])
+                .overLowerAreaCount(mvtransStats[1]).overLowerAreaTimes(mvtransStats[3])
+                .overLowerConsumerCount(consumerStats[1]).overLowerConsumerTimes(consumerStats[3])
+                .build();
+    }
+
+    /**
+     * 按 alarmLevel 统计今日电压越限
+     * @return int[4]: [0]=越上限去重数, [1]=越下限去重数, [2]=越上限次数, [3]=越下限次数
+     */
+    private int[] countVoltageByAlarmLevel(String id, Integer type, String alarmLevel,
+                                            LocalDateTime start, LocalDateTime end) {
+        LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, alarmLevel)
+                .ge(FhzgSeVoltageEvent::getFirstOverTime, start)
+                .lt(FhzgSeVoltageEvent::getFirstOverTime, end);
+        applyNodeFilter(wrapper, id, type);
 
 
-        // == 配变越限事件统计(alarm_level='mvtrans')==
-        LambdaQueryWrapper<FhzgSeVoltageEvent> mvtransWrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
-                .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
-                .ge(FhzgSeVoltageEvent::getFirstOverTime, todayStart)
-                .lt(FhzgSeVoltageEvent::getFirstOverTime, tomorrowStart);
-        applyNodeFilter(mvtransWrapper, id, type);
-        List<FhzgSeVoltageEvent> mvtransEvents = voltageEventMapper.selectList(mvtransWrapper);
+        List<FhzgSeVoltageEvent> events = voltageEventMapper.selectList(wrapper);
 
 
-        int overUpperArea = 0, overLowerArea = 0;
-        for (FhzgSeVoltageEvent e : mvtransEvents) {
+        Set<String> overDeviceIds = new HashSet<>();
+        Set<String> underDeviceIds = new HashSet<>();
+        int overTimes = 0, underTimes = 0;
+
+        for (FhzgSeVoltageEvent e : events) {
             String t = e.getAlarmType();
             String t = e.getAlarmType();
             if (t == null) continue;
             if (t == null) continue;
-            if (t.contains("over")) overUpperArea++;
-            if (t.contains("under")) overLowerArea++;
+            if (t.contains("over")) {
+                if (e.getDeviceId() != null) overDeviceIds.add(e.getDeviceId());
+                overTimes++;
+            } else if (t.contains("under")) {
+                if (e.getDeviceId() != null) underDeviceIds.add(e.getDeviceId());
+                underTimes++;
+            }
         }
         }
 
 
-        return SeVoltageDashboardVO.builder()
-                .todayOverUpperAreaCount(overUpperArea)
-                .todayOverLowerAreaCount(overLowerArea)
-                .todayOverUpperFeederCount(overUpperFeeder)
-                .todayOverLowerFeederCount(overLowerFeeder)
-                .build();
+        return new int[]{overDeviceIds.size(), underDeviceIds.size(), overTimes, underTimes};
     }
     }
 
 
     /**
     /**
@@ -399,6 +410,8 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
         } else {
         } else {
             BigDecimal maxLoadRate = BigDecimal.ZERO;
             BigDecimal maxLoadRate = BigDecimal.ZERO;
             BigDecimal sumAvg = BigDecimal.ZERO;
             BigDecimal sumAvg = BigDecimal.ZERO;
+            long sumHeavy = 0;
+            long sumOverload = 0;
             for (FhzgSeCapacityDaily d : dailyRecords) {
             for (FhzgSeCapacityDaily d : dailyRecords) {
                 if (d.getMaxLoadRate() != null && d.getMaxLoadRate().compareTo(maxLoadRate) > 0) {
                 if (d.getMaxLoadRate() != null && d.getMaxLoadRate().compareTo(maxLoadRate) > 0) {
                     maxLoadRate = d.getMaxLoadRate();
                     maxLoadRate = d.getMaxLoadRate();
@@ -406,33 +419,31 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
                 if (d.getAvgLoadRate() != null) {
                 if (d.getAvgLoadRate() != null) {
                     sumAvg = sumAvg.add(d.getAvgLoadRate());
                     sumAvg = sumAvg.add(d.getAvgLoadRate());
                 }
                 }
+                if (d.getHeavyCount() != null) sumHeavy += d.getHeavyCount();
+                if (d.getOverloadCount() != null) sumOverload += d.getOverloadCount();
             }
             }
             BigDecimal avgLoadRate = !dailyRecords.isEmpty()
             BigDecimal avgLoadRate = !dailyRecords.isEmpty()
                     ? sumAvg.divide(BigDecimal.valueOf(dailyRecords.size()), 2, RoundingMode.HALF_UP)
                     ? sumAvg.divide(BigDecimal.valueOf(dailyRecords.size()), 2, RoundingMode.HALF_UP)
                     : BigDecimal.ZERO;
                     : BigDecimal.ZERO;
+            int totalEvents = (int) (sumHeavy + sumOverload);
             vo = SeCapacityDashboardVO.builder()
             vo = SeCapacityDashboardVO.builder()
                     .maxLoadRate(maxLoadRate)
                     .maxLoadRate(maxLoadRate)
                     .avgLoadRate(avgLoadRate)
                     .avgLoadRate(avgLoadRate)
+                    .heavyLoadEventCount((int) sumHeavy)
+                    .overloadEventCount((int) sumOverload)
+                    .totalEventCount(totalEvents)
                     .build();
                     .build();
+            if (totalEvents > 0) {
+                vo.setHeavyLoadRate(new BigDecimal(sumHeavy * 100)
+                        .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+                vo.setOverloadRate(new BigDecimal(sumOverload * 100)
+                        .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setHeavyLoadRate(BigDecimal.ZERO);
+                vo.setOverloadRate(BigDecimal.ZERO);
+            }
         }
         }
 
 
-        // 2. 重过载事件次数:从事件表统计
-        LocalDateTime rangeStart = startDate.atStartOfDay();
-        LocalDateTime rangeEnd = endDate.atTime(23, 59, 59);
-        int[] eventCounts = countCurrentEvents(id, type, rangeStart, rangeEnd);
-        vo.setHeavyLoadEventCount(eventCounts[0]);
-        vo.setOverloadEventCount(eventCounts[1]);
-        int totalEvents = eventCounts[0] + eventCounts[1];
-        vo.setTotalEventCount(totalEvents);
-        if (totalEvents > 0) {
-            vo.setHeavyLoadRate(new BigDecimal(eventCounts[0] * 100)
-                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
-            vo.setOverloadRate(new BigDecimal(eventCounts[1] * 100)
-                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
-        } else {
-            vo.setHeavyLoadRate(BigDecimal.ZERO);
-            vo.setOverloadRate(BigDecimal.ZERO);
-        }
         return vo;
         return vo;
     }
     }
 
 

+ 118 - 25
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);
                 }
                 }
             }
             }
         }
         }
@@ -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);
@@ -194,18 +214,39 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                     currentEventMapper.insert(event);
                     currentEventMapper.insert(event);
                     state.setAlarmTriggered(1);
                     state.setAlarmTriggered(1);
                     state.setAlarmEventId(event.getId());
                     state.setAlarmEventId(event.getId());
-                    // 异步触发重构算法(需要 feeder_id + point_time)
+                    // 取事件窗口内最大负载率断面时刻作为重构触发断面时刻
+                    LocalDateTime maxLrTime = findMaxLoadRateTime(feederId, state.getFirstOverTime(), snapTime);
                     eventPublisher.publishEvent(new ReconTriggerEvent(
                     eventPublisher.publishEvent(new ReconTriggerEvent(
                             event.getId(), feederId, feederName, countyId, subsId,
                             event.getId(), feederId, feederName, countyId, subsId,
-                            alarmType, snapTime, 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);
             }
             }
@@ -218,8 +259,9 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                        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);
     }
     }
 
 
@@ -246,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);
 
 
@@ -269,7 +314,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     // 电压状态机
     // 电压状态机
     // ============================================================
     // ============================================================
 
 
-    private void checkVoltageMonitor(String deviceId, String deviceType,
+    private void checkVoltageMonitor(String deviceId, String deviceType, String deviceName,
                                       BigDecimal voltageRatio, int voltageStatus,
                                       BigDecimal voltageRatio, int voltageStatus,
                                       LocalDateTime snapTime, String feederId, String feederName,
                                       LocalDateTime snapTime, String feederId, String feederName,
                                       String subsId, String countyId, String mvtransId, String consumerId) {
                                       String subsId, String countyId, String mvtransId, String consumerId) {
@@ -292,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) {
@@ -308,7 +360,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                 BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
                                 BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
                                 int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
                                 int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceId(deviceId).deviceName(deviceName)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .feederId(feederId).feederName(feederName)
                                         .feederId(feederId).feederName(feederName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
@@ -331,7 +383,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                 String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
                                 String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
                                 BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
                                 BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceId(deviceId).deviceName(deviceName)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .feederId(feederId).feederName(feederName)
                                         .feederId(feederId).feederName(feederName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
@@ -347,14 +399,12 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                             // consumer 级别暂走旧逻辑(如有可后续优化)
                             // 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);
             }
             }
@@ -433,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;
@@ -501,6 +551,26 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         try { return new BigDecimal(v); } catch (NumberFormatException e) { return null; }
         try { return new BigDecimal(v); } catch (NumberFormatException e) { return null; }
     }
     }
 
 
+    /**
+     * 查事件窗口 [start, end] 内该馈线最大负载率的那条断面的 snapshotTime
+     */
+    private LocalDateTime findMaxLoadRateTime(String feederId, LocalDateTime start, LocalDateTime end) {
+        if (feederId == null || start == null) return end;
+        List<FhzgSeSnapshotDetail> list = snapshotDetailMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getFeederId, feederId)
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                        .isNotNull(FhzgSeSnapshotDetail::getLoadRate)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, start)
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, end != null ? end : LocalDateTime.now())
+                        .orderByDesc(FhzgSeSnapshotDetail::getLoadRate)
+                        .last("LIMIT 1"));
+        if (list != null && !list.isEmpty()) {
+            return list.get(0).getSnapshotTime();
+        }
+        return end;
+    }
+
     /**
     /**
      * 按 countyId 从 SeSnapshotConstants.COUNTIES 查 countyName。
      * 按 countyId 从 SeSnapshotConstants.COUNTIES 查 countyName。
      * COUNTIES 的 key=countyId(=maintOrg UUID),value=[countySid, countyName]
      * COUNTIES 的 key=countyId(=maintOrg UUID),value=[countySid, countyName]
@@ -526,6 +596,29 @@ 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;

+ 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);
+                }
+            }
         }
         }
     }
     }
 }
 }