Explorar o código

运行部分修正

lisonglin hai 2 semanas
pai
achega
699b440c89
Modificáronse 19 ficheiros con 742 adicións e 280 borrados
  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 MVTRANS_BASE_KV = new BigDecimal("0.22");
     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.lt.bf.adapter.SiStateEstimationClient;
 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.PageResult;
 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")
-    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("参数缺失");
         }
-        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")
-    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("参数缺失");
         }
-        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;
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.AllArgsConstructor;
@@ -22,11 +24,15 @@ public class FhzgSeMonitorCurrent implements Serializable {
     private Long id;
     private String feederId;
     private String feederName;
+    /** 必须用 IGNORED,否则 updateById 跳过 null → DB 残留旧值 → 状态机误判 */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private LocalDateTime firstOverTime;
     private BigDecimal lastLoadRate;
     private Integer lastStatus;
     private LocalDateTime lastSnapTime;
     private Integer alarmTriggered;
+    /** 必须用 IGNORED,同上(null 表示无活跃事件) */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Long alarmEventId;
     private LocalDateTime createTime;
     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;
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.AllArgsConstructor;
@@ -28,11 +30,15 @@ public class FhzgSeMonitorVoltage implements Serializable {
     private String countyId;
     private String mvtransId;
     private String consumerId;
+    /** 必须用 IGNORED,否则 updateById 跳过 null → DB 残留旧值 → 状态机误判 */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private LocalDateTime firstOverTime;
     private BigDecimal lastVoltageRatio;
     private Integer lastVoltageStatus;
     private LocalDateTime lastSnapTime;
     private Integer alarmTriggered;
+    /** 必须用 IGNORED,同上(null 表示无活跃事件) */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Long alarmEventId;
     private LocalDateTime createTime;
     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 java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * 异常告警列表行 VO(线路重过载 / 电压越限共用)
@@ -27,21 +28,30 @@ public class SeAlarmListVO implements Serializable {
     @ApiModelProperty(value = "告警事件ID")
     private Long alarmEventId;
 
+    @ApiModelProperty(value = "馈线ID")
+    private String feederId;
+
     @ApiModelProperty(value = "线路名称")
     private String feederName;
 
-    @ApiModelProperty(value = "展示值: 负载率'102%' 或 电压越限类型'线路电压越下限'")
+    @ApiModelProperty(value = "展示值: 负载率'102' 或 电压方向'越上限/越下限'")
     private String alarmDisplayValue;
 
     @ApiModelProperty(value = "越限时间(firstOverTime)")
     private String alarmTime;
 
+    @ApiModelProperty(value = "告警持续时长 如'1.5h'")
+    private String alarmDuration;
+
     @ApiModelProperty(value = "告警类型: current_heavy/current_overload/voltage_over/voltage_under")
     private String alarmType;
 
-    @ApiModelProperty(value = "告警层级: feeder/mvtrans (前端用来决定颜色)")
+    @ApiModelProperty(value = "告警层级: feeder/mvtrans/consumer (前端用来决定颜色)")
     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.math.BigDecimal;
-import java.util.List;
 
 /**
  * 电压质量指标 VO
  * <p>
- * 今日:从 fhzg_se_alarm_event 查(馈线+台区,不分级别),展示当前越限数
+ * 今日:从 fhzg_se_voltage_event 查(馈线+台区+用户),分条/台/户数(去重) + 次数(事件总数)
  * 历史(当月/全年):从 ads_mrt_voltage_over_limit_daily_result 查(8维度×4级别=16字段)
  *
  * @author lsl
@@ -32,20 +31,49 @@ public class SeVoltageDashboardVO implements Serializable {
     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)
@@ -98,4 +126,4 @@ public class SeVoltageDashboardVO implements Serializable {
     @ApiModelProperty(value = "越限总时长(小时)")
     @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
     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 = "联络点过多线路数")
     private Integer excessiveTiePointFeederCount;
 
-    @ApiModelProperty(value = "无联络/单辐射线路数")
+    @ApiModelProperty(value = "无联络线路数")
+    private Integer noConnectionFeederCount;
+
+    @ApiModelProperty(value = "单辐射线路数")
     private Integer singleRadialFeederCount;
 
     @ApiModelProperty(value = "存在大分支馈线数")
@@ -118,13 +121,13 @@ public class StaticGridDashboardVO implements Serializable {
     // 四、供电能力
     // ============================================================
 
-    @ApiModelProperty(value = "区域最大负载率(%)(预留,来自运行态指标)")
+    @ApiModelProperty(value = "区域最大负载率(%),从容量日汇总表统计")
     private BigDecimal regionalMaxLoadRate;
 
-    @ApiModelProperty(value = "区域平均负载率(%)(预留,来自运行态指标)")
+    @ApiModelProperty(value = "区域平均负载率(%),从容量日汇总表统计")
     private BigDecimal regionalAvgLoadRate;
 
-    @ApiModelProperty(value = "线路过载条数(预留,来自运行态指标)")
+    @ApiModelProperty(value = "线路重过载条数,从容量日汇总表统计重载+过载")
     private Integer lineOverloadCount;
 
     @ApiModelProperty(value = "主干线截面不匹配线路数")
@@ -136,12 +139,6 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "负荷组容量异常数")
     private Integer loadGroupCapacityAnomalyCount;
 
-    @ApiModelProperty(value = "综合线损率(%)(预留,来自运行态指标)")
-    private BigDecimal comprehensiveLossRate;
-
-    @ApiModelProperty(value = "功率因数合格率(%)(预留,来自运行态指标)")
-    private BigDecimal powerFactorPassRate;
-
     // ============================================================
     // 问题详情列表
     // ============================================================
@@ -182,10 +179,10 @@ public class StaticGridDashboardVO implements Serializable {
         @ApiModelProperty(value = "实际值")
         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;
 
+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.PageResult;
 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时返回自身)
      */
     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;
 
 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.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
 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.entity.vo.*;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
@@ -21,6 +24,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -58,30 +62,31 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     // ============================================================
 
     @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;
         LocalDateTime rangeStart = start.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>()
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, rangeStart)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, rangeEnd)
                 .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);
         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) {
-        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()
                 .alarmEventId(e.getId())
                 .feederName(e.getFeederName())
                 .feederId(e.getFeederId())
                 .alarmDisplayValue(displayValue)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmDuration(calcDuration(e.getFirstOverTime(), e.getRecoverTime(), e.getStatus()))
                 .alarmType(e.getAlarmType())
                 .alarmLevel("feeder")
                 .build();
@@ -108,29 +114,27 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     // ============================================================
 
     @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;
         LocalDateTime rangeStart = start.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>()
                 .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
                 .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);
         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) {
-        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()
                 .alarmEventId(e.getId())
+                .deviceName(e.getDeviceName())
                 .feederName(e.getFeederName())
                 .feederId(e.getFeederId())
                 .alarmDisplayValue(displayValue)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmDuration(calcDuration(e.getFirstOverTime(), e.getRecoverTime(), e.getStatus()))
                 .alarmType(e.getAlarmType())
                 .alarmLevel(e.getAlarmLevel())
+                .affectedConsumerCount(consumerCount)
                 .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) {
         boolean isOver = alarmType != null && alarmType.contains("over");
@@ -181,7 +211,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
             return buildOverloadDetail(currentEvent);
         }
 
-        // 电压事件(仅馈线级有详情,配变越限无详情)
+        // 电压事件
         FhzgSeVoltageEvent voltageEvent = voltageEventMapper.selectById(alarmEventId);
         if (voltageEvent != null && "feeder".equals(voltageEvent.getAlarmLevel())) {
             return buildFeederDetail(voltageEvent);
@@ -244,35 +274,12 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .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) {
-        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()
                 .alarmEventId(event.getId())
                 .alarmType(event.getAlarmType())
@@ -280,27 +287,41 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .countyName(event.getCountyName())
                 .feederName(event.getFeederName())
                 .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())
                 .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>()
                         .eq(FhzgSeVoltageEvent::getFeederId, feederId)
                         .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;
-        for (FhzgSeVoltageEvent e : list) {
+        for (FhzgSeVoltageEvent e : mvEvents) {
             if (e.getAlarmType() != null && e.getAlarmType().contains("over")) up++;
             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,
-                                   com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> column) {
+                                   SFunction<T, ?> column) {
+        if (StringUtils.isBlank(orderDir)) return;
         if ("asc".equalsIgnoreCase(orderDir)) {
             wrapper.orderByAsc(column);
         } 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,
                                  java.util.List<FeederSelectVO> result) {
         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.lt.bf.adapter.SiMeasurementClient;
 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.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
@@ -90,11 +91,13 @@ public class SeConsumerVoltageServiceImpl {
             FhzgSeConsumerRelation rel = consNoMap.get(consNo);
             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()
                     .snapshotTime(snapTime).deviceId(consNo).deviceType("consumer")
+                    .consumerName(rel.getCustomerName())
                     .feederId(rel.getFeederId()).subsId(rel.getSubsId()).countyId(rel.getCountyId())
                     .mvtransId(mvtransId).consumerId(consNo)
                     .lvVoltageA(measValue).voltageRatio(voltageRatio).voltageStatus(voltageStatus)
@@ -103,7 +106,7 @@ public class SeConsumerVoltageServiceImpl {
 
             upsertConsumerVoltageMonitor(consNo, "consumer",
                     rel.getFeederId(), rel.getSubsId(), rel.getCountyId(), mvtransId, consNo,
-                    voltageRatio, voltageStatus, snapTime);
+                    voltageRatio, voltageStatus, snapTime, rel);
         }
         log.info("配变{}用户电压处理完成 共{}户,成功{}户", mvtransId, consNos.size(), voltageMap.size());
     }
@@ -111,7 +114,8 @@ public class SeConsumerVoltageServiceImpl {
     private void upsertConsumerVoltageMonitor(String deviceId, String deviceType,
                                                String feederId, String subsId, String countyId,
                                                String mvtransId, String consumerId,
-                                               BigDecimal ratio, int status, LocalDateTime snapTime) {
+                                               BigDecimal ratio, int status, LocalDateTime snapTime,
+                                               FhzgSeConsumerRelation rel) {
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
@@ -133,6 +137,15 @@ public class SeConsumerVoltageServiceImpl {
             boolean intervalExceeded = lastSnap != null
                     && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES;
             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.setAlarmTriggered(0);
                 state.setAlarmEventId(null);
@@ -145,10 +158,12 @@ public class SeConsumerVoltageServiceImpl {
 
                     String alarmType = resolveConsumerAlarmType(status);
                     BigDecimal threshold = resolveConsumerThreshold(status);
+                    String countyName = lookupCountyName(countyId);
 
                     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)
                             .threshold(threshold)
                             .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_GENERAL_V_OVER) > 0) return 1;
         if (ratio.compareTo(SeMonitorThreshold.CONSUMER_SEVERE_V_UNDER) < 0) return 4;
@@ -188,6 +207,19 @@ public class SeConsumerVoltageServiceImpl {
         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) {
         switch (status) {
             case 1: return "voltage_over_general";
@@ -207,4 +239,4 @@ public class SeConsumerVoltageServiceImpl {
             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) {
         LocalDateTime todayStart = LocalDate.now().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();
             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 {
             BigDecimal maxLoadRate = BigDecimal.ZERO;
             BigDecimal sumAvg = BigDecimal.ZERO;
+            long sumHeavy = 0;
+            long sumOverload = 0;
             for (FhzgSeCapacityDaily d : dailyRecords) {
                 if (d.getMaxLoadRate() != null && d.getMaxLoadRate().compareTo(maxLoadRate) > 0) {
                     maxLoadRate = d.getMaxLoadRate();
@@ -406,33 +419,31 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
                 if (d.getAvgLoadRate() != null) {
                     sumAvg = sumAvg.add(d.getAvgLoadRate());
                 }
+                if (d.getHeavyCount() != null) sumHeavy += d.getHeavyCount();
+                if (d.getOverloadCount() != null) sumOverload += d.getOverloadCount();
             }
             BigDecimal avgLoadRate = !dailyRecords.isEmpty()
                     ? sumAvg.divide(BigDecimal.valueOf(dailyRecords.size()), 2, RoundingMode.HALF_UP)
                     : BigDecimal.ZERO;
+            int totalEvents = (int) (sumHeavy + sumOverload);
             vo = SeCapacityDashboardVO.builder()
                     .maxLoadRate(maxLoadRate)
                     .avgLoadRate(avgLoadRate)
+                    .heavyLoadEventCount((int) sumHeavy)
+                    .overloadEventCount((int) sumOverload)
+                    .totalEventCount(totalEvents)
                     .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;
     }
 

+ 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.common.SeMonitorThreshold;
 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.FhzgSeMonitorCurrent;
 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.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.DwdShbDsTransformerBaseMapper;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
 import lombok.RequiredArgsConstructor;
@@ -44,6 +46,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final DwdShbDsTransformerBaseMapper transformerBaseMapper;
     private final FeederGroupApplication feederGroupApplication;
     private final ApplicationEventPublisher eventPublisher;
 
@@ -87,13 +90,21 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
 
         // === 2. 配变 ===
         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++) {
                 JSONObject m = mvtrans.getJSONObject(i);
                 FhzgSeSnapshotDetail d = parseMvtrans(m, snapTime, feederMap);
                 if (d != null) {
                     batch.add(d);
-                    processMvtransMonitor(m, d, snapTime, feederMap);
+                    processMvtransMonitor(m, d, snapTime, feederMap, transformerNameMap);
                 }
             }
         }
@@ -168,6 +179,15 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         } else {
             LocalDateTime lastSnap = state.getLastSnapTime();
             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.setAlarmTriggered(0);
                 state.setAlarmEventId(null);
@@ -194,18 +214,39 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                     currentEventMapper.insert(event);
                     state.setAlarmTriggered(1);
                     state.setAlarmEventId(event.getId());
-                    // 异步触发重构算法(需要 feeder_id + point_time)
+                    // 取事件窗口内最大负载率断面时刻作为重构触发断面时刻
+                    LocalDateTime maxLrTime = findMaxLoadRateTime(feederId, state.getFirstOverTime(), snapTime);
                     eventPublisher.publishEvent(new ReconTriggerEvent(
                             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 {
                 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);
             }
@@ -218,8 +259,9 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                        LocalDateTime snapTime, Map<String, String[]> feederMap) {
         if (detail.getVoltageStatus() == null) return;
         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);
     }
 
@@ -246,14 +288,17 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
 
     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;
         String mvtransId = detail.getDeviceId();
         String[] loc = feederMap.get(detail.getFeederId());
         int status = detail.getVoltageStatus();
         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,
                 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,
                                       LocalDateTime snapTime, String feederId, String feederName,
                                       String subsId, String countyId, String mvtransId, String consumerId) {
@@ -292,6 +337,13 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         } else {
             LocalDateTime lastSnap = state.getLastSnapTime();
             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);
             }
             if (isOverLimit) {
@@ -308,7 +360,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                 BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
                                 int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceId(deviceId).deviceName(deviceName)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .feederId(feederId).feederName(feederName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
@@ -331,7 +383,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                                 String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
                                 BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
                                 FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceId(deviceId).deviceName(deviceName)
                                         .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
                                         .feederId(feederId).feederName(feederName)
                                         .subsId(subsId).countyId(countyId).countyName(countyName)
@@ -347,14 +399,12 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                             // consumer 级别暂走旧逻辑(如有可后续优化)
                         }
             } else {
+                // 设备恢复正常(任何 deviceType 都走):关自己的事件
                 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);
             }
@@ -433,14 +483,14 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
 
     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_UNDER) < 0) return 2;
         return 0;
     }
 
     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_UNDER) < 0) return 2;
         return 0;
@@ -501,6 +551,26 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         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。
      * COUNTIES 的 key=countyId(=maintOrg UUID),value=[countySid, countyName]
@@ -526,6 +596,29 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         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,
                                               String parentSubsId, String parentCountyId) {
         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.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
+import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
+import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
 import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
+import com.alibaba.fastjson.JSON;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -50,6 +53,7 @@ public class StaticGridDashboardServiceImpl
     private final StaticIndicatorMetricMapper metricMapper;
     private final SjztPwwyyqxtPrmPdrOutageMvDsService outageDurationService;
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
+    private final StaticProblemDetailMapper problemDetailMapper;
 
     // ============================================================
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
@@ -144,6 +148,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
+        vo.setProblemDetails(buildProblemDetails(result.getId()));
         return vo;
     }
 
@@ -174,6 +179,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, substationId, null, ym);
+        vo.setProblemDetails(buildProblemDetails(resultId));
         return vo;
     }
 
@@ -204,6 +210,7 @@ public class StaticGridDashboardServiceImpl
                 ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, null, feederId, ym);
+        vo.setProblemDetails(buildProblemDetails(resultId));
         return vo;
     }
 
@@ -318,6 +325,9 @@ public class StaticGridDashboardServiceImpl
                 .sameBusInterconnectionIssueCount(toInteger(metricMap.get("same_bus_interconnection_issue_count")))
                 .excessiveTiePointFeederCount(toInteger(metricMap.get("excessive_tie_point_feeder_count")))
                 .singleRadialFeederCount(toInteger(metricMap.get("single_radial_feeder_count")))
+                .noConnectionFeederCount(calcNoConnectionFeederCount(
+                        metricMap.get("feeder_total_count"),
+                        metricMap.get("tied_feeder_total_count")))
                 .largeBranchFeederCount(toInteger(metricMap.get("large_branch_issue_feeder_count")))
                 .unreasonableSegmentCount(toInteger(metricMap.get("unreasonable_segment_count_feeder_count")))
                 .mainTrunkSegmentDenseCount(toInteger(metricMap.get("main_trunk_lantern_issue_feeder_count")))
@@ -332,8 +342,6 @@ public class StaticGridDashboardServiceImpl
                 .mainTrunkSectionMismatchCount(toInteger(metricMap.get("main_trunk_bottleneck_feeder_count")))
                 .connectedCapacityExceedFeederCount(toInteger(metricMap.get("connected_capacity_exceed_feeder_count")))
                 .loadGroupCapacityAnomalyCount(toInteger(metricMap.get("load_group_capacity_anomaly_count")))
-                .comprehensiveLossRate(null)
-                .powerFactorPassRate(null)
                 .build();
     }
 
@@ -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) {
         if (StringUtils.isBlank(value)) return null;
         try {
@@ -504,16 +554,18 @@ public class StaticGridDashboardServiceImpl
             vo.setRegionalAvgLoadRate(BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
         }
 
-        // 过载线路数
+        // 线路重过载条数(重载 ≥80% + 过载 ≥100%)
         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 {
-            // 区县/变电站:去重有过载记录的馈线数
+            // 区县/变电站:去重有重载或过载记录的馈线数
             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)
                     .distinct()
                     .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);
         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) {
             JSONArray categories = countyResult.getJSONArray("categories");
             if (categories != null) {
                 for (int i = 0; i < categories.size(); i++) {
                     JSONObject category = categories.getJSONObject(i);
-                    parseCategoryMetrics(category, resultId, "county", null, null, null, null, null, null, metricList);
+                    parseCategoryMetrics(category, resultId, "county", null, null,
+                            null, null, null, null, metricList, problemDetailList);
                 }
             }
         }
 
-        // 5. 写指标明细 — feeder 级
+        // feeder 级
         if (calcResult != null) {
             JSONArray feederResults = calcResult.getJSONArray("feeder_results");
             if (feederResults != null) {
@@ -148,49 +152,24 @@ public class StaticIndicatorResultServiceImpl
                             parseCategoryMetrics(category, resultId, "feeder", feederId,
                                     feederName, sourceSubstationId, sourceSubstationName,
                                     sourceBusId, supplyAreaType,
-                                    metricList);
+                                    metricList, problemDetailList);
                         }
                     }
                 }
             }
         }
+
+        // 批量插入指标明细
         for (StaticIndicatorMetric m : metricList) {
             metricMapper.insert(m);
         }
         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);
         return resultId;
@@ -241,7 +220,8 @@ public class StaticIndicatorResultServiceImpl
                                        String feederId, String feederName,
                                        String sourceSubstationId, String sourceSubstationName,
                                        String sourceBusId, String supplyAreaType,
-                                       List<StaticIndicatorMetric> metricList) {
+                                       List<StaticIndicatorMetric> metricList,
+                                       List<StaticProblemDetail> problemDetailList) {
         String categoryCode = category.getString("category_code");
         String categoryName = category.getString("category_name");
         JSONArray metrics = category.getJSONArray("metrics");
@@ -274,6 +254,34 @@ public class StaticIndicatorResultServiceImpl
                     .isProblem(m.getBoolean("is_problem") != null && m.getBoolean("is_problem") ? 1 : 0)
                     .build();
             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);
+                }
+            }
         }
     }
 }