Sfoglia il codice sorgente

运行部分修正

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

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

@@ -50,4 +50,39 @@ public final class SeMonitorThreshold {
     public static final BigDecimal FEEDER_BASE_KV = new BigDecimal("10");
     public static final BigDecimal 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;
+}

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

@@ -1,6 +1,8 @@
 package com.hdkj.lt.bf.entity;
 
+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,12 +24,16 @@ 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;
-}
+}

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

@@ -1,6 +1,8 @@
 package com.hdkj.lt.bf.entity;
 
+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,12 +30,16 @@ 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;
+}

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

@@ -89,7 +89,10 @@ public class StaticGridDashboardVO implements Serializable {
     @ApiModelProperty(value = "联络点过多线路数")
     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);
-}
+}

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

@@ -1,11 +1,14 @@
 package com.hdkj.lt.bf.service.impl;
 
 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;
@@ -58,30 +61,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,9 +93,9 @@ 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())
@@ -108,29 +112,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());
@@ -258,20 +260,18 @@ public class SeAlarmServiceImpl implements SeAlarmService {
 
         // 查询事件窗口内该配变的低压侧电压 max/min
         BigDecimal maxVoltage = null, minVoltage = null;
-        if (event.getFirstOverTime() != null && event.getDeviceId() != null) {
-            List<FhzgSeSnapshotDetail> snaps = snapshotDetailMapper.selectList(
-                    new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
-                            .eq(FhzgSeSnapshotDetail::getDeviceId, event.getDeviceId())
-                            .eq(FhzgSeSnapshotDetail::getDeviceType, "mvtrans")
-                            .isNotNull(FhzgSeSnapshotDetail::getLvVoltageA)
-                            .ge(FhzgSeSnapshotDetail::getSnapshotTime, event.getFirstOverTime())
-                            .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime));
-            for (FhzgSeSnapshotDetail s : snaps) {
-                BigDecimal v = s.getLvVoltageA();
-                if (v == null) continue;
-                if (maxVoltage == null || v.compareTo(maxVoltage) > 0) maxVoltage = v;
-                if (minVoltage == null || v.compareTo(minVoltage) < 0) minVoltage = v;
-            }
+        List<FhzgSeSnapshotDetail> mvSnaps = snapshotDetailMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getDeviceId, event.getDeviceId())
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "mvtrans")
+                        .isNotNull(FhzgSeSnapshotDetail::getLvVoltageA)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, event.getFirstOverTime())
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime));
+        for (FhzgSeSnapshotDetail s : mvSnaps) {
+            BigDecimal v = s.getLvVoltageA();
+            if (v == null) continue;
+            if (maxVoltage == null || v.compareTo(maxVoltage) > 0) maxVoltage = v;
+            if (minVoltage == null || v.compareTo(minVoltage) < 0) minVoltage = v;
         }
 
         return SeAlarmDetailVO.builder()
@@ -288,17 +288,12 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .build();
     }
 
-    private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
-        LocalDateTime endTime = event.getRecoverTime();
-        if (endTime == null && event.getStatus() == 1) {
-            FhzgSeMonitorCurrent state = monitorCurrentMapper.selectOne(
-                    new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
-                            .eq(FhzgSeMonitorCurrent::getFeederId, event.getFeederId())
-                            .last("LIMIT 1"));
-            if (state != null) endTime = state.getLastSnapTime();
-        }
-        if (endTime == null) endTime = event.getTriggerTime();
+    // ============================================================
+    // 4. 重过载事件详情
+    // ============================================================
 
+    private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
+        String abnormalIndicator = resolveOverloadDisplay(event.getAlarmType());
         return SeAlarmDetailVO.builder()
                 .alarmEventId(event.getId())
                 .alarmType(event.getAlarmType())
@@ -306,27 +301,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++;
         }
@@ -404,8 +413,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 {
@@ -414,7 +428,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     }
 
     private void collectFeeders(com.hdkj.fhzg.optimization.response.FeederGroupTreeNode node,
-                                java.util.List<FeederSelectVO> result) {
+                                 java.util.List<FeederSelectVO> result) {
         if (node.getType() == 4) {
             result.add(new FeederSelectVO(node.getId(), node.getName()));
         }

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

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.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;
         }
     }
-}
+}

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

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

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

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.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);
                 }
             }
         }
@@ -143,7 +154,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
 
     private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
-                                      LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
         if (detail.getCurrentStatus() == null) return;
         String feederId = detail.getFeederId();
         String[] loc = feederMap.get(feederId);
@@ -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);
@@ -200,13 +220,33 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                             event.getId(), feederId, feederName, countyId, subsId,
                             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);
             }
@@ -216,11 +256,12 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
 
     private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
-                                      LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       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);
     }
 
@@ -247,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);
 
@@ -270,10 +314,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     // 电压状态机
     // ============================================================
 
-    private void checkVoltageMonitor(String deviceId, String deviceType,
-                                     BigDecimal voltageRatio, int voltageStatus,
-                                     LocalDateTime snapTime, String feederId, String feederName,
-                                     String subsId, String countyId, String mvtransId, String consumerId) {
+    private void checkVoltageMonitor(String deviceId, String deviceType, String deviceName,
+                                      BigDecimal voltageRatio, int voltageStatus,
+                                      LocalDateTime snapTime, String feederId, String feederName,
+                                      String subsId, String countyId, String mvtransId, String consumerId) {
         FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
                 new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
                         .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
@@ -293,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) {
@@ -300,62 +351,60 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                 if (state.getFirstOverTime() != null
                         && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
                         && state.getAlarmTriggered() == 0) {
-                    String countyName = lookupCountyName(countyId, null);
-
-                    if ("feeder".equals(deviceType)) {
-                        // === 馈线自身越限 → 创建 feeder 级独立事件 ===
-                        String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
-                        BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
-                        BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
-                        int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
-                        FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                .deviceId(deviceId).deviceName(feederName)
-                                .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
-                                .feederId(feederId).feederName(feederName)
-                                .subsId(subsId).countyId(countyId).countyName(countyName)
-                                .threshold(threshold)
-                                .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
-                                .triggerValue(voltageRatio)
-                                .voltageRangeMax(vr[0]).voltageRangeMin(vr[1])
-                                .overUpperMvtrans(mc[0]).overLowerMvtrans(mc[1])
-                                .lastValue(voltageRatio).status(1).build();
-                        voltageEventMapper.insert(event);
-                        state.setAlarmTriggered(1); state.setAlarmEventId(event.getId());
-                        // 馈线越下限 → 异步触发重构算法
-                        if ("feeder".equals(deviceType) && "voltage_under".equals(alarmType)) {
-                            eventPublisher.publishEvent(new ReconTriggerEvent(
-                                    event.getId(), feederId, feederName, countyId, subsId,
-                                    alarmType, snapTime, state.getFirstOverTime()));
+                            String countyName = lookupCountyName(countyId, null);
+
+                            if ("feeder".equals(deviceType)) {
+                                // === 馈线自身越限 → 创建 feeder 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
+                                int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(deviceName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio)
+                                        .voltageRangeMax(vr[0]).voltageRangeMin(vr[1])
+                                        .overUpperMvtrans(mc[0]).overLowerMvtrans(mc[1])
+                                        .lastValue(voltageRatio).status(1).build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1); state.setAlarmEventId(event.getId());
+                                // 馈线越下限 → 异步触发重构算法
+                                if ("feeder".equals(deviceType) && "voltage_under".equals(alarmType)) {
+                                    eventPublisher.publishEvent(new ReconTriggerEvent(
+                                            event.getId(), feederId, feederName, countyId, subsId,
+                                            alarmType, snapTime, state.getFirstOverTime()));
+                                }
+                            } else if ("mvtrans".equals(deviceType)) {
+                                // === 台区越限 → 创建 mvtrans 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(deviceName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .mvtransId(mvtransId)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio).lastValue(voltageRatio).status(1)
+                                        .build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1);
+                                state.setAlarmEventId(event.getId());
+                            }
+                            // consumer 级别暂走旧逻辑(如有可后续优化)
                         }
-                    } else if ("mvtrans".equals(deviceType)) {
-                        // === 台区越限 → 创建 mvtrans 级独立事件 ===
-                        String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
-                        BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
-                        FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
-                                .deviceId(deviceId).deviceName(feederName)
-                                .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
-                                .feederId(feederId).feederName(feederName)
-                                .subsId(subsId).countyId(countyId).countyName(countyName)
-                                .mvtransId(mvtransId)
-                                .threshold(threshold)
-                                .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
-                                .triggerValue(voltageRatio).lastValue(voltageRatio).status(1)
-                                .build();
-                        voltageEventMapper.insert(event);
-                        state.setAlarmTriggered(1);
-                        state.setAlarmEventId(event.getId());
-                    }
-                    // consumer 级别暂走旧逻辑(如有可后续优化)
-                }
             } else {
+                // 设备恢复正常(任何 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);
             }
@@ -434,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;
@@ -547,8 +596,31 @@ 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) {
+                                              String parentSubsId, String parentCountyId) {
         if (nodes == null) return;
         for (FeederGroupTreeNode node : nodes) {
             String subsId = parentSubsId, countyId = parentCountyId;

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

@@ -5,12 +5,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.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);
+                }
+            }
         }
     }
 }