瀏覽代碼

Merge remote-tracking branch 'origin/dev-0' into dev-0

liuaini 2 周之前
父節點
當前提交
5264afe4d8
共有 82 個文件被更改,包括 4535 次插入1366 次删除
  1. 11 0
      api/load-transfer-si-api/src/main/java/com/hdkj/lt/feign/IMeasurementApiClient.java
  2. 6 0
      api/load-transfer-si-api/src/main/java/com/hdkj/lt/feign/fallback/MeasurementApiClientFallBack.java
  3. 2 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/LoadTransferBFApplication.java
  4. 29 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiMeasurementClient.java
  5. 24 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiReconClient.java
  6. 9 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiStateEstimationClient.java
  7. 53 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeMonitorThreshold.java
  8. 25 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeSnapshotConstants.java
  9. 15 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/config/BfScheduleConfig.java
  10. 43 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/config/SeAsyncConfig.java
  11. 178 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  12. 1 2
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/StaticGridController.java
  13. 82 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgDwmVoltageOverLimitDaily.java
  14. 66 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeCapacityDaily.java
  15. 48 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeConsumerRelation.java
  16. 40 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeCurrentEvent.java
  17. 33 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorCurrent.java
  18. 39 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeMonitorVoltage.java
  19. 93 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeSnapshotDetail.java
  20. 52 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/FhzgSeVoltageEvent.java
  21. 30 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/OutageAggregationDTO.java
  22. 35 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/TodayOutageData.java
  23. 32 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/FeederSelectVO.java
  24. 156 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/IndicatorDashboardVO.java
  25. 53 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/PageResult.java
  26. 79 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeAlarmDetailVO.java
  27. 47 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeAlarmListVO.java
  28. 90 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeCapacityDashboardVO.java
  29. 46 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeOutageDashboardVO.java
  30. 101 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeVoltageDashboardVO.java
  31. 40 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/event/MvtransOverVoltageEvent.java
  32. 59 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/event/ReconTriggerEvent.java
  33. 136 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/listener/ReconTriggerListener.java
  34. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgDwmVoltageOverLimitDailyMapper.java
  35. 7 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeCapacityDailyMapper.java
  36. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeConsumerRelationMapper.java
  37. 7 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeCurrentEventMapper.java
  38. 7 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeMonitorCurrentMapper.java
  39. 7 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeMonitorVoltageMapper.java
  40. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeSnapshotDetailMapper.java
  41. 7 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeVoltageEventMapper.java
  42. 15 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/GridPdOutageEventBaseMapper.java
  43. 3 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/SjztPwwyyqxtPrmPdrOutageMvDsMapper.java
  44. 135 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/schedule/SeSnapshotScheduler.java
  45. 152 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/scheduler/SeCapacityDailyScheduler.java
  46. 24 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/IndicatorDashboardService.java
  47. 56 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeAlarmService.java
  48. 40 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeIndicatorService.java
  49. 31 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeSnapshotService.java
  50. 38 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SjztPwwyyqxtPrmPdrOutageMvDsService.java
  51. 95 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/IndicatorDashboardServiceImpl.java
  52. 333 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java
  53. 210 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeConsumerVoltageServiceImpl.java
  54. 491 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  55. 510 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  56. 151 15
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SjztPwwyyqxtPrmPdrOutageMvDsServiceImpl.java
  57. 2 2
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/StaticGridDashboardService.java
  58. 97 81
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java
  59. 13 0
      services/load-transfer-bf/src/main/resources/mapper/SjztPwwyyqxtPrmPdrOutageMvDsMapper.xml
  60. 0 123
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/entity/indicator/AlarmEvent.java
  61. 0 172
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/entity/indicator/IndicatorResult.java
  62. 0 128
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/IndicatorCalcConsumer.java
  63. 0 183
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/IndicatorEventConsumer.java
  64. 0 125
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/ReconCalcConsumer.java
  65. 0 15
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/mapper/indicator/AlarmEventMapper.java
  66. 0 15
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/mapper/indicator/IndicatorResultMapper.java
  67. 0 28
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/AlarmEvaluateService.java
  68. 0 31
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/IndicatorResultService.java
  69. 0 249
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/impl/AlarmEvaluateServiceImpl.java
  70. 0 175
      services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/impl/IndicatorResultServiceImpl.java
  71. 17 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/config/ScheduleConfig.java
  72. 24 2
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/algorithm/AlgorithmController.java
  73. 10 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/measurement/MeasurementApiController.java
  74. 61 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/schedule/StaticGridIndicatorScheduler.java
  75. 25 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/schedule/StaticGridScheduleConstants.java
  76. 18 4
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/AlgorithmService.java
  77. 8 3
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/impl/AlgorithmServiceImpl.java
  78. 11 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/measurement/IMeasurementApiService.java
  79. 72 0
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/measurement/impl/MeasurementApiService.java
  80. 3 2
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/remote/impl/AlgorithmRemoteCallServiceImpl.java
  81. 18 4
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/StaticIndicatorResultService.java
  82. 57 6
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/impl/StaticIndicatorResultServiceImpl.java

+ 11 - 0
api/load-transfer-si-api/src/main/java/com/hdkj/lt/feign/IMeasurementApiClient.java

@@ -51,4 +51,15 @@ public interface IMeasurementApiClient {
     @ApiOperation(value = "按日期获取曲线图信息")
     ApiResponse<RecordsVO> queryCommonQueryByDay(@RequestBody DistributionTransformerDTO distributionTransformerDTO);
 
+    /**
+     * 查询低压用户电压(A相,consType=2)
+     *
+     * @param request Map with keys: consNos (List<String>), startTime (String), endTime (String)
+     * @return consNo → measValue(伏特V)
+     */
+    @PostMapping("/queryConsumerVoltage")
+    @ApiOperation(value = "查询低压用户电压")
+    ApiResponse<java.util.Map<String, BigDecimal>> queryConsumerVoltage(
+            @RequestBody java.util.Map<String, Object> request);
+
 }

+ 6 - 0
api/load-transfer-si-api/src/main/java/com/hdkj/lt/feign/fallback/MeasurementApiClientFallBack.java

@@ -68,6 +68,12 @@ public class MeasurementApiClientFallBack implements FallbackFactory<IMeasuremen
                 return ApiResponse.fail(String.format("量测-queryPeriodByCommonQuery 远程调用服务异常:%s", cause.getMessage()));
 
             }
+
+            @Override
+            public ApiResponse<java.util.Map<String, BigDecimal>> queryConsumerVoltage(java.util.Map<String, Object> request) {
+                log.error("量测-queryConsumerVoltage 远程调用服务异常, 参数:{}", request);
+                return ApiResponse.fail(String.format("量测-queryConsumerVoltage 远程调用服务异常:%s", cause.getMessage()));
+            }
         };
     }
 }

+ 2 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/LoadTransferBFApplication.java

@@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 /**
@@ -20,6 +21,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 @SpringBootApplication(scanBasePackages = {SystemGlobalConstant.BIZMS_BASE_PACKAGE})
 @MapperScan(basePackages = {SystemGlobalConstant.BIZMS_DAO_PACKAGE, SystemGlobalConstant.BIZMS_MAPPER_PACKAGE})
 @EnableCaching
+@EnableAsync
 @EnableScheduling
 //@EnableSwagger2WebMvc
 public class LoadTransferBFApplication {

+ 29 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiMeasurementClient.java

@@ -0,0 +1,29 @@
+package com.hdkj.lt.bf.adapter;
+
+import com.hdkj.hussar.ApiResponse;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 服务调用si量测中心接口
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@FeignClient(value = "load-transfer-si", contextId = "SiMeasurementClient")
+public interface SiMeasurementClient {
+
+    /**
+     * 查询低压用户电压(A相,consType=2)
+     *
+     * @param request Map: consNos (List<String>), startTime (String), endTime (String)
+     * @return consNo → measValue(伏特V)
+     */
+    @PostMapping("/measurement/queryConsumerVoltage")
+    ApiResponse<Map<String, BigDecimal>> queryConsumerVoltage(@RequestBody Map<String, Object> request);
+}

+ 24 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiReconClient.java

@@ -0,0 +1,24 @@
+package com.hdkj.lt.bf.adapter;
+
+import com.hdkj.hussar.ApiResponse;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.Map;
+
+/**
+ * Feign 客户端:远程调用 SI 模块的重构结果入库接口
+ * <p>
+ * 同事端点 POST /recon/trigger 接收 {eventId, params}
+ * 其中 params 包含 feeder_id + point_time,同事自行组装 county/拓扑等信息。
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@FeignClient(value = "load-transfer-si", contextId = "SiReconClient")
+public interface SiReconClient {
+
+    @PostMapping("/recon/trigger")
+    ApiResponse<Map> triggerRecon(@RequestBody Map<String, Object> request);
+}

+ 9 - 1
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/adapter/SiStateEstimationClient.java

@@ -1,5 +1,6 @@
 package com.hdkj.lt.bf.adapter;
 
+import com.hdkj.fhzg.optimization.request.MaintOrgRequest;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.lt.core.bizms.modle.dto.StateEstimation;
 import com.hdkj.lt.core.bizms.modle.request.StateEstimationEquipRequest;
@@ -7,11 +8,14 @@ import com.hdkj.lt.core.bizms.modle.request.StateEstimationRequest;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.List;
 
 /**
  * 服务调用si状估集成接口
  */
-@FeignClient(value = "load-transfer-si")
+@FeignClient(value = "load-transfer-si", contextId = "SiStateEstimationClient")
 public interface SiStateEstimationClient {
 
     @ApiOperation(value = "分时段查询中压线路下设备节点状估数据")
@@ -21,4 +25,8 @@ public interface SiStateEstimationClient {
     @ApiOperation(value = "分时段查询设备状估数据")
     @PostMapping("/stateEstimation/queryPeriodByEquip")
     ApiResponse<StateEstimation> queryPeriodByEquip(StateEstimationEquipRequest request);
+
+    @ApiOperation(value = "按县域查询设备状估数据(同事封装,含全量馈线并发调用)")
+    @PostMapping("/stateEstimation/queryEquipByCounty")
+    ApiResponse<List<StateEstimation>> queryEquipByCounty(@RequestBody MaintOrgRequest request);
 }

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

@@ -0,0 +1,53 @@
+package com.hdkj.lt.bf.common;
+
+import java.math.BigDecimal;
+
+/**
+ * 状估监测阈值常量
+ * <p>
+ * 馈线电压: ±7% (ratio 0.93~1.07)
+ * 配变电压: -10% ~ +7% (ratio 0.90~1.07)
+ * 用户0.4kV: ±10%一般, -20%~+15%严重 (ratio 0.80~1.10一般, 0.80~0.90/1.10~1.15严重)
+ * 负载率: ≥80%重载, ≥100%过载
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+public final class SeMonitorThreshold {
+
+    private SeMonitorThreshold() {}
+
+    // === 馈线电压 ±7% ===
+    public static final BigDecimal FEEDER_V_OVER = new BigDecimal("1.07");
+    public static final BigDecimal FEEDER_V_UNDER = new BigDecimal("0.93");
+
+    // === 配变电压 -10% ~ +7% ===
+    public static final BigDecimal MVTRANS_V_OVER = new BigDecimal("1.07");
+    public static final BigDecimal MVTRANS_V_UNDER = new BigDecimal("0.90");
+
+    // === 用户0.4kV ±10%一般 ===
+    public static final BigDecimal CONSUMER_GENERAL_V_OVER = new BigDecimal("1.10");
+    public static final BigDecimal CONSUMER_GENERAL_V_UNDER = new BigDecimal("0.90");
+
+    // === 用户0.4kV -20%~+15%严重 ===
+    public static final BigDecimal CONSUMER_SEVERE_V_OVER = new BigDecimal("1.15");
+    public static final BigDecimal CONSUMER_SEVERE_V_UNDER = new BigDecimal("0.80");
+
+    // === 负载率 ===
+    public static final BigDecimal LOAD_RATE_HEAVY = new BigDecimal("80");
+    public static final BigDecimal LOAD_RATE_OVERLOAD = new BigDecimal("100");
+
+    // === 持续越限触发: 时间跨度 ≥ 60分钟 ===
+    public static final long TRIGGER_DURATION_MINUTES = 60L;
+
+    // === 断面间隔(分钟) — 超过则重置 ===
+    public static final long SNAP_INTERVAL_MINUTES = 15L;
+
+    // === 量测中心分批查 consNo 每批大小 ===
+    public static final int CONS_BATCH_SIZE = 50;
+
+    // === 基准电压 ===
+    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");
+}

+ 25 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeSnapshotConstants.java

@@ -0,0 +1,25 @@
+package com.hdkj.lt.bf.common;
+
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 状估断面定时任务常量
+ * <p>
+ * 区县列表:maintOrg/countyId(通用ID) → [countySid, countyName]
+ * maintOrg 用于调用同事的 queryEquipByCounty 接口。
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+public interface SeSnapshotConstants {
+
+    /** 区县列表:maintOrg/countyId → [countySid, countyName] */
+    List<Map.Entry<String, String[]>> COUNTIES = Arrays.asList(
+            new AbstractMap.SimpleEntry<>("BA6481B8F34143FDA7E1C35A76846911", new String[]{"洪湖", "国网洪湖市供电公司"}),
+            new AbstractMap.SimpleEntry<>("54C4BBBB8FE4428E96B6AE12247C4DD4", new String[]{"秭归", "国网秭归县供电公司"}),
+            new AbstractMap.SimpleEntry<>("60FDC66B3FC143DC9C39001215DDCC61", new String[]{"长阳", "国网长阳县供电公司"})
+    );
+}

+ 15 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/config/BfScheduleConfig.java

@@ -0,0 +1,15 @@
+package com.hdkj.lt.bf.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启用 @Scheduled 支持,用于状估断面定时拉取等任务。
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@Configuration
+@EnableScheduling
+public class BfScheduleConfig {
+}

+ 43 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/config/SeAsyncConfig.java

@@ -0,0 +1,43 @@
+package com.hdkj.lt.bf.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 状估监测模块异步线程池配置
+ * <p>
+ * 用于 @Async 标注的异步方法(当前: 配变越限→查用户电压的异步监听器)。
+ * 线程名前缀 se-async- 便于日志排查。
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Configuration
+public class SeAsyncConfig implements AsyncConfigurer {
+
+    @Bean(name = "seAsyncExecutor")
+    public ThreadPoolTaskExecutor seAsyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(4);
+        executor.setMaxPoolSize(8);
+        executor.setQueueCapacity(200);
+        executor.setKeepAliveSeconds(60);
+        executor.setThreadNamePrefix("se-async-");
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(60);
+        // CallerRunsPolicy: 队列满时由调用线程执行,避免丢任务
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+
+    @Override
+    public Executor getAsyncExecutor() {
+        return seAsyncExecutor();
+    }
+}

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

@@ -0,0 +1,178 @@
+package com.hdkj.lt.bf.controller.optimization;
+
+import com.hdkj.hussar.ApiResponse;
+import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
+import com.hdkj.lt.bf.entity.vo.PageResult;
+import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
+import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
+import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
+import com.hdkj.lt.bf.entity.vo.SeAlarmListVO;
+import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
+import com.hdkj.lt.bf.service.IndicatorDashboardService;
+import com.hdkj.lt.bf.service.SeAlarmService;
+import com.hdkj.lt.bf.service.SeIndicatorService;
+import com.hdkj.lt.core.mvc.BaseController;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * 运行态指标 仪表盘 Controller
+ * <p>
+ * POST /indicator/dashboard — 返回综合态势仪表盘数据
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+@Slf4j
+@RestController
+@AllArgsConstructor
+@RequestMapping("/indicator")
+public class IndicatorController extends BaseController {
+
+    private final IndicatorDashboardService indicatorDashboardService;
+    private final SeIndicatorService seIndicatorService;
+    private final SeAlarmService seAlarmService;
+
+    /**
+     * 查询运行态仪表盘
+     *
+     * @param params {id: 节点ID, type: 节点类型(2/3/4)}
+     */
+//    @PostMapping("/dashboard")
+//    public ApiResponse<IndicatorDashboardVO> dashboard(@RequestBody Map<String, String> params) {
+//        String id = params.get("id");
+//        String type = params.get("type");
+//        IndicatorDashboardVO vo = indicatorDashboardService.queryDashboard(id, type);
+//        if (vo == null) {
+//            return ApiResponse.fail("参数不合法或无数据");
+//        }
+//        return ApiResponse.success(vo);
+//    }
+
+    /**
+     * 停电时户数(无时间维度切换)
+     *
+     * @param params {id, type(2/3/4)}
+     */
+    @PostMapping("/outage")
+    public ApiResponse<SeOutageDashboardVO> outage(@RequestBody Map<String, String> params) {
+        String id = params.get("id");
+        String type = params.get("type");
+        if (id == null || type == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seIndicatorService.queryOutage(id, Integer.parseInt(type)));
+    }
+
+    /**
+     * 电压质量(today/month/year)
+     *
+     * @param params {id, type(2/3/4), timeScope(today/month/year)}
+     */
+    @PostMapping("/voltage")
+    public ApiResponse<SeVoltageDashboardVO> voltage(@RequestBody Map<String, String> params) {
+        String id = params.get("id");
+        String type = params.get("type");
+        String timeScope = params.get("timeScope");
+        if (id == null || type == null || timeScope == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seIndicatorService.queryVoltage(id, Integer.parseInt(type), timeScope));
+    }
+
+    /**
+     * 供电能力(today/month/year)
+     *
+     * @param params {id, type(2/3/4), timeScope(today/month/year)}
+     */
+    @PostMapping("/capacity")
+    public ApiResponse<SeCapacityDashboardVO> capacity(@RequestBody Map<String, String> params) {
+        String id = params.get("id");
+        String type = params.get("type");
+        String timeScope = params.get("timeScope");
+        if (id == null || type == null || timeScope == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seIndicatorService.queryCapacity(id, Integer.parseInt(type), timeScope));
+    }
+
+    /**
+     * 异常告警-线路重过载列表
+     *
+     * @param params {id, type(2/3/4), feederName(可选), date(可选), 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 date = params.get("date");
+        String orderBy = params.get("orderBy");
+        String pageStr = params.get("page");
+        String pageSizeStr = params.get("pageSize");
+        if (id == null || type == 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, date, orderBy, page, pageSize));
+    }
+
+    /**
+     * 异常告警-电压越限列表
+     *
+     * @param params {id, type(2/3/4), feederName(可选), date(可选), orderBy(asc/desc,可选)}
+     */
+    @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 date = params.get("date");
+        String orderBy = params.get("orderBy");
+        String pageStr = params.get("page");
+        String pageSizeStr = params.get("pageSize");
+        if (id == null || type == 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, date, orderBy, page, pageSize));
+    }
+
+    /**
+     * 运行异常信息详情
+     *
+     * @param params {alarmEventId}
+     */
+    @PostMapping("/alarm/detail")
+    public ApiResponse<SeAlarmDetailVO> alarmDetail(@RequestBody Map<String, String> params) {
+        String alarmEventId = params.get("alarmEventId");
+        if (alarmEventId == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seAlarmService.queryAlarmDetail(Long.parseLong(alarmEventId)));
+    }
+
+    /**
+     * 线路名称下拉列表
+     *
+     * @param params {id, type(2/3/4)}
+     */
+    @PostMapping("/alarm/feederList")
+    public ApiResponse<java.util.List<FeederSelectVO>> feederList(@RequestBody Map<String, String> params) {
+        String id = params.get("id");
+        String type = params.get("type");
+        if (id == null || type == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seAlarmService.queryFeederList(id, Integer.parseInt(type)));
+    }
+}

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

@@ -41,10 +41,9 @@ public class StaticGridController extends BaseController {
      */
     @PostMapping("/dashboard")
     public ApiResponse<StaticGridDashboardVO> dashboard(@RequestBody Map<String, String> params) {
-        String daytime = params.get("daytime");
         String id = params.get("id");
         String type = params.get("type");
-        StaticGridDashboardVO vo = staticGridDashboardService.queryLatestDashboard(daytime,id,type);
+        StaticGridDashboardVO vo = staticGridDashboardService.queryLatestDashboard(id, type);
         return ApiResponse.success(vo);
     }
 }

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

@@ -0,0 +1,82 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 中台T-2电压越限日聚合表
+ * <p>
+ * dim_level=county/substation/feeder,按 over_date(yyyymmdd) 分日存储。
+ * 16个count字段覆盖4维度(台区/用户/馈线/母线) × 4级别(越上限严重/一般 + 越下限严重/一般)。
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@TableName("ads_mrt_voltage_over_limit_daily_result")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgDwmVoltageOverLimitDaily implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 统计日期yyyymmdd */
+    private String overDate;
+
+    /** county/substation/feeder */
+    private String dimLevel;
+
+    private String provinceId;
+    private String provinceName;
+    private String cityOrgId;
+    private String cityOrgName;
+    private String maintainerId;
+    private String maintainerName;
+    private String substationId;
+    private String substationName;
+    private String feederId;
+    private String feederName;
+
+    // --- 越上限-严重 ---
+    private Integer overUpperSevereTgCount;
+    private Integer overUpperSevereCustCount;
+    private Integer overUpperSevereFeederCount;
+    private Integer overUpperSevereGeneratrixCount;
+
+    // --- 越上限-一般 ---
+    private Integer overUpperMildTgCount;
+    private Integer overUpperMildCustCount;
+    private Integer overUpperMildFeederCount;
+    private Integer overUpperMildGeneratrixCount;
+
+    // --- 越下限-严重 ---
+    private Integer overLowerSevereTgCount;
+    private Integer overLowerSevereCustCount;
+    private Integer overLowerSevereFeederCount;
+    private Integer overLowerSevereGeneratrixCount;
+
+    // --- 越下限-一般 ---
+    private Integer overLowerMildTgCount;
+    private Integer overLowerMildCustCount;
+    private Integer overLowerMildFeederCount;
+    private Integer overLowerMildGeneratrixCount;
+
+    /** 当日越限总时长(小时) */
+    private BigDecimal totalOverLimitHours;
+
+    private LocalDateTime ctime;
+    private LocalDateTime mtime;
+}

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

@@ -0,0 +1,66 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.Builder;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 供电能力日汇总
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@TableName("fhzg_se_capacity_daily")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeCapacityDaily implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 统计日期 */
+    private LocalDate statDate;
+
+    /** 馈线ID */
+    private String feederId;
+
+    /** 馈线名称 */
+    private String feederName;
+
+    /** 区县ID */
+    private String countyId;
+
+    /** 变电站ID */
+    private String subsId;
+
+    /** 当天最大负载率% */
+    private BigDecimal maxLoadRate;
+
+    /** 当天平均负载率% */
+    private BigDecimal avgLoadRate;
+
+    /** 重载断面数 */
+    private Integer heavyCount;
+
+    /** 过载断面数 */
+    private Integer overloadCount;
+
+    /** 总断面数 */
+    private Integer totalSnapshots;
+
+    private java.time.LocalDateTime createTime;
+
+    private java.time.LocalDateTime updateTime;
+}

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

@@ -0,0 +1,48 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 用户与配变关系表(中台推入)
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@TableName("fhzg_se_consumer_relation")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeConsumerRelation implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 用户编号 */
+    private String custNo;
+
+    private String customerName;
+    private String mvtransName;
+    private String mvtransId;
+    private String countyId;
+    private String cityId;
+    private String feederId;
+    private String subsId;
+
+    /** 电压等级(0.4/10/20/6等) */
+    private String voltageLevel;
+
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,40 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("fhzg_se_current_event")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeCurrentEvent implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String feederId;
+    private String feederName;
+    private String alarmType;
+    private String countyId;
+    private String countyName;
+    private String subsId;
+    private LocalDateTime firstOverTime;
+    private LocalDateTime triggerTime;
+    private BigDecimal triggerLoadRate;
+    private BigDecimal maxLoadRate;
+    private BigDecimal lastLoadRate;
+    private Integer status;
+    private LocalDateTime recoverTime;
+    /** 重构方案状态:0未触发 1生成中 2有方案 3无方案 -1调用失败 */
+    private Integer reconStatus;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,33 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("fhzg_se_monitor_current")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeMonitorCurrent implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String feederId;
+    private String feederName;
+    private LocalDateTime firstOverTime;
+    private BigDecimal lastLoadRate;
+    private Integer lastStatus;
+    private LocalDateTime lastSnapTime;
+    private Integer alarmTriggered;
+    private Long alarmEventId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,39 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("fhzg_se_monitor_voltage")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeMonitorVoltage implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String deviceId;
+    private String deviceType;
+    private String feederId;
+    private String feederName;
+    private String subsId;
+    private String countyId;
+    private String mvtransId;
+    private String consumerId;
+    private LocalDateTime firstOverTime;
+    private BigDecimal lastVoltageRatio;
+    private Integer lastVoltageStatus;
+    private LocalDateTime lastSnapTime;
+    private Integer alarmTriggered;
+    private Long alarmEventId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,93 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.Builder;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 状估断面明细
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@TableName("fhzg_se_snapshot_detail")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeSnapshotDetail implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 断面时间(15分精度) */
+    private LocalDateTime snapshotTime;
+
+    /** 设备psrId */
+    private String deviceId;
+
+    /** feeder/mvtrans/consumer */
+    private String deviceType;
+
+    /** 所属馈线 */
+    private String feederId;
+
+    /** 馈线名称 */
+    private String feederName;
+
+    /** 所属变电站(冗余) */
+    private String subsId;
+
+    /** 所属区县(冗余) */
+    private String countyId;
+
+    /** 所属配变(用户专用) */
+    private String mvtransId;
+
+    /** 用户编号(用户专用) */
+    private String consumerId;
+
+    private String consumerName;
+
+    /** seoPs 有功MW(馈线) */
+    private BigDecimal activePower;
+
+    /** seoQs 无功MVar(馈线) */
+    private BigDecimal reactivePower;
+
+    /** seoIs 电流A(馈线) */
+    private BigDecimal currentValue;
+
+    /** seoUs 中压侧电压kV(馈线/配变) */
+    private BigDecimal voltageValue;
+
+    /** 负载率%(馈线) */
+    private BigDecimal loadRate;
+
+    /** seiLvUas 低压A相kV(配变) */
+    private BigDecimal lvVoltageA;
+
+    /** 配变容量MW */
+    private BigDecimal capacity;
+
+    /** 电压比值(馈线/10, 配变/0.22, 用户/0.22) */
+    private BigDecimal voltageRatio;
+
+    /** 0正常 1重载 2过载(只馈线) */
+    private Integer currentStatus;
+
+    /** 0正常 1越上限 2越下限(馈线/配变) | 1一般越上 2严重越上 3一般越下 4严重越下(用户) */
+    private Integer voltageStatus;
+
+    private LocalDateTime createTime;
+}

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

@@ -0,0 +1,52 @@
+package com.hdkj.lt.bf.entity;
+
+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;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@TableName("fhzg_se_voltage_event")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class FhzgSeVoltageEvent implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String deviceId;
+    private String deviceName;
+    private String deviceType;
+    private String alarmLevel;
+    private String alarmType;
+    private String feederId;
+    private String feederName;
+    private String subsId;
+    private String countyId;
+    private String countyName;
+    private String mvtransId;
+    private String consumerId;
+    private BigDecimal threshold;
+    private LocalDateTime firstOverTime;
+    private LocalDateTime triggerTime;
+    private BigDecimal triggerValue;
+    private BigDecimal voltageRangeMax;
+    private BigDecimal voltageRangeMin;
+    private Integer overUpperMvtrans;
+    private Integer overLowerMvtrans;
+    @TableField("`last_value`")
+    private BigDecimal lastValue;
+    private Integer status;
+    private LocalDateTime recoverTime;
+    /** 重构方案状态:0未触发 1生成中 2有方案 3无方案 -1调用失败 */
+    private Integer reconStatus;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,30 @@
+package com.hdkj.lt.bf.entity.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 停电聚合数据(T-2 表一次SQL返回总时户数 + 总影响用户数)
+ *
+ * @author lsl
+ * @since 2026-07-22
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OutageAggregationDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 总停电时户数 SUM(outage_hc_num) */
+    private BigDecimal totalHouseholds;
+
+    /** 总影响用户数 SUM(outage_mvcons_cnt) + SUM(outage_lvcons_cnt) */
+    private BigDecimal totalAffectedUsers;
+}

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

@@ -0,0 +1,35 @@
+package com.hdkj.lt.bf.entity.dto;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.hdkj.lt.base.serializer.BigDecimalSerializer;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 当天停电数据(来自 grid_pd_outage_event_base 实时表)
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class TodayOutageData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "当日停电时户数")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal totalHouseholds;
+
+    @ApiModelProperty(value = "当日平均停电时长(小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgDurationHours;
+}

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

@@ -0,0 +1,32 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 线路下拉选择框 VO
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "线路下拉选项")
+public class FeederSelectVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "馈线ID")
+    private String feederId;
+
+    @ApiModelProperty(value = "馈线名称")
+    private String feederName;
+}

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

@@ -0,0 +1,156 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.hdkj.lt.base.serializer.BigDecimalSerializer;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 运行态指标仪表盘 VO(前端综合态势展示)
+ * <p>
+ * 对应前端“运行态指标”仪表盘,按 3 大模块分组:
+ * 供电可靠性 / 电压质量 / 供电能力
+ * <p>
+ * 数据来源:
+ * - t_indicator_result(算法 raw_response 解析)
+ * - t_alarm_event(告警次数统计)
+ * - SjztPwwyyqxtPrmPdrOutageMvDsService(停电时户数)
+ * - 部分字段需新增数据源(标注 @pendingSource)
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "运行态指标仪表盘")
+public class IndicatorDashboardVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    // ============================================================
+    // 基本信息
+    // ============================================================
+
+    @ApiModelProperty(value = "数据批次ID(t_indicator_result.id)")
+    private Long indicatorResultId;
+
+    @ApiModelProperty(value = "批次时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime batchTime;
+
+    @ApiModelProperty(value = "区县名称")
+    private String countyName;
+
+    // ============================================================
+    // 一、供电可靠性
+    // ============================================================
+
+    @ApiModelProperty(value = "停电时户数(今日)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal outageHoursHouseholdsToday;
+
+    @ApiModelProperty(value = "停电时户数(全年累计)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal outageHoursHouseholdsYear;
+
+    @ApiModelProperty(value = "平均停电时长(今日,单位:小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgOutageDurationToday;
+
+    @ApiModelProperty(value = "平均停电时长(全年,单位:小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgOutageDurationYear;
+
+    // ============================================================
+    // 二、电压质量
+    // ============================================================
+
+    @ApiModelProperty(value = "IEC用户电压指数(预留,来源待定)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal iecUserVoltageIndex;
+
+    // --- 越上限 ---
+
+    @ApiModelProperty(value = "越上限台区数-严重")
+    private Integer overUpperLimitAreaSevere;
+
+    @ApiModelProperty(value = "越上限台区数-一般")
+    private Integer overUpperLimitAreaGeneral;
+
+    @ApiModelProperty(value = "越上限户数-严重")
+    private Integer overUpperLimitUserSevere;
+
+    @ApiModelProperty(value = "越上限户数-一般")
+    private Integer overUpperLimitUserGeneral;
+
+    // --- 越下限 ---
+
+    @ApiModelProperty(value = "越下限台区数-严重")
+    private Integer belowLowerLimitAreaSevere;
+
+    @ApiModelProperty(value = "越下限台区数-一般")
+    private Integer belowLowerLimitAreaGeneral;
+
+    @ApiModelProperty(value = "越下限户数-严重")
+    private Integer belowLowerLimitUserSevere;
+
+    @ApiModelProperty(value = "越下限户数-一般")
+    private Integer belowLowerLimitUserGeneral;
+
+    // ============================================================
+    // 三、供电能力
+    // ============================================================
+
+    @ApiModelProperty(value = "区域最大负载率(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal maxLoadRate;
+
+    @ApiModelProperty(value = "区域平均负载率(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgLoadRate;
+
+    // --- 重过载事件 ---
+
+    @ApiModelProperty(value = "重过载事件总次数")
+    private Integer overloadEventTotal;
+
+    @ApiModelProperty(value = "重载事件次数(黄色)")
+    private Integer heavyLoadEventCount;
+
+    @ApiModelProperty(value = "重载事件占比(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal heavyLoadRate;
+
+    @ApiModelProperty(value = "过载事件次数(红色)")
+    private Integer overloadEventCount;
+
+    @ApiModelProperty(value = "过载事件占比(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal overloadRate;
+
+    // --- 线损 ---
+
+    @ApiModelProperty(value = "公线线损率(%)(预留,来源待定)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal publicLineLossRate;
+
+    @ApiModelProperty(value = "线损率(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal lineLossRate;
+
+    @ApiModelProperty(value = "台区线损达标率(%)(预留,来源待定)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal areaLineLossComplianceRate;
+}

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

@@ -0,0 +1,53 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 通用分页查询结果
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "分页查询结果")
+public class PageResult<T> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "数据列表")
+    private List<T> records;
+
+    @ApiModelProperty(value = "总记录数")
+    private long total;
+
+    @ApiModelProperty(value = "总页数")
+    private long pages;
+
+    @ApiModelProperty(value = "当前页码")
+    private long current;
+
+    @ApiModelProperty(value = "每页大小")
+    private long pageSize;
+
+    public static <T> PageResult<T> of(IPage<?> mpPage, List<T> records) {
+        PageResult<T> r = new PageResult<>();
+        r.setRecords(records);
+        r.setTotal(mpPage.getTotal());
+        r.setPages(mpPage.getPages());
+        r.setCurrent(mpPage.getCurrent());
+        r.setPageSize(mpPage.getSize());
+        return r;
+    }
+}

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

@@ -0,0 +1,79 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 运行异常信息详情 VO
+ * <p>
+ * 重过载和电压越限共用,不适用字段留 null。
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "运行异常信息详情")
+public class SeAlarmDetailVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "告警事件ID")
+    private Long alarmEventId;
+
+    @ApiModelProperty(value = "告警类型: current_heavy/current_overload/voltage_over/voltage_under")
+    private String alarmType;
+
+    @ApiModelProperty(value = "告警类别: overload=重过载, voltage=电压越限")
+    private String alarmCategory;
+
+    @ApiModelProperty(value = "所属区县")
+    private String countyName;
+
+    @ApiModelProperty(value = "线路名称")
+    private String feederName;
+
+    @ApiModelProperty(value = "告警开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime alarmStartTime;
+
+    @ApiModelProperty(value = "告警结束时间(最后断面时间)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime alarmEndTime;
+
+    // === 重过载专用 ===
+
+    @ApiModelProperty(value = "最大负载率(%) — 重过载用")
+    private BigDecimal maxLoadRate;
+
+    // === 电压越限专用 ===
+
+    @ApiModelProperty(value = "异常指标: 线路电压越下限/线路电压越上限/台区电压越下限/台区电压越上限")
+    private String abnormalIndicator;
+
+    @ApiModelProperty(value = "线路最高电压(kV)")
+    private BigDecimal maxVoltage;
+
+    @ApiModelProperty(value = "线路最低电压(kV)")
+    private BigDecimal minVoltage;
+
+    @ApiModelProperty(value = "越上限台区数(与线路同时段告警满1小时)")
+    private Integer overUpperMvtransCount;
+
+    @ApiModelProperty(value = "越下限台区数(与线路同时段告警满1小时)")
+    private Integer overLowerMvtransCount;
+
+    @ApiModelProperty(value = "重构方案状态:0=未触发 1=生成中 2=有方案 3=无方案 -1=失败")
+    private Integer reconStatus;
+}

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

@@ -0,0 +1,47 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 异常告警列表行 VO(线路重过载 / 电压越限共用)
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "异常告警列表行")
+public class SeAlarmListVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "告警事件ID")
+    private Long alarmEventId;
+
+    @ApiModelProperty(value = "线路名称")
+    private String feederName;
+
+    @ApiModelProperty(value = "展示值: 负载率'102%' 或 电压越限类型'线路电压越下限'")
+    private String alarmDisplayValue;
+
+    @ApiModelProperty(value = "越限时间(firstOverTime)")
+    private String alarmTime;
+
+    @ApiModelProperty(value = "告警类型: current_heavy/current_overload/voltage_over/voltage_under")
+    private String alarmType;
+
+    @ApiModelProperty(value = "告警层级: feeder/mvtrans (前端用来决定颜色)")
+    private String alarmLevel;
+
+    @ApiModelProperty(value = "馈线ID")
+    private String feederId;
+}

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

@@ -0,0 +1,90 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.hdkj.lt.base.serializer.BigDecimalSerializer;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 供电能力指标 VO
+ * <p>
+ * 从 fhzg_se_snapshot_detail 聚合(馈线保留1年),和中台T-2电压越限表完全独立
+ * 今日馈线返回96点时序(折线图),变电站/区县只返回聚合值
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "供电能力指标")
+public class SeCapacityDashboardVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "区域最大负载率(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal maxLoadRate;
+
+    @ApiModelProperty(value = "区域平均负载率(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgLoadRate;
+
+    @ApiModelProperty(value = "重载事件次数")
+    private Integer heavyLoadEventCount;
+
+    @ApiModelProperty(value = "过载事件次数")
+    private Integer overloadEventCount;
+
+    @ApiModelProperty(value = "重载事件占比(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal heavyLoadRate;
+
+    @ApiModelProperty(value = "过载事件占比(%)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal overloadRate;
+
+    @ApiModelProperty(value = "总事件次数")
+    private Integer totalEventCount;
+
+    // ============================================================
+    // 今日馈线96点时序(仅馈线type=4+today时返回)
+    // ============================================================
+
+    @ApiModelProperty(value = "馈线负载率96点时序(仅馈线+今日)")
+    private List<TimeSeriesPoint> loadRateSeries;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class TimeSeriesPoint implements Serializable {
+        private static final long serialVersionUID = 1L;
+        @ApiModelProperty(value = "时间点 HH:mm")
+        private String time;
+        @ApiModelProperty(value = "负载率(%)")
+        @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+        private BigDecimal loadRate;
+        @ApiModelProperty(value = "电流(A)")
+        @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+        private BigDecimal currentValue;
+        @ApiModelProperty(value = "电压(kV)")
+        @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+        private BigDecimal voltageValue;
+        @ApiModelProperty(value = "有功功率(MW)")
+        @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+        private BigDecimal activePower;
+        @ApiModelProperty(value = "无功功率(MVar)")
+        @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+        private BigDecimal reactivePower;
+    }
+}

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

@@ -0,0 +1,46 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.hdkj.lt.base.serializer.BigDecimalSerializer;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 停电时户数指标 VO(对应前端"停电时户数"卡片,无时间维度切换)
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "停电时户数指标")
+public class SeOutageDashboardVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "停电时户数(今日)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal outageHoursHouseholdsToday;
+
+    @ApiModelProperty(value = "停电时户数(全年累计)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal outageHoursHouseholdsYear;
+
+    @ApiModelProperty(value = "平均停电时长(今日,单位:小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgOutageDurationToday;
+
+    @ApiModelProperty(value = "平均停电时长(全年,单位:小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal avgOutageDurationYear;
+}

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

@@ -0,0 +1,101 @@
+package com.hdkj.lt.bf.entity.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.hdkj.lt.base.serializer.BigDecimalSerializer;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 电压质量指标 VO
+ * <p>
+ * 今日:从 fhzg_se_alarm_event 查(馈线+台区,不分级别),展示当前越限数
+ * 历史(当月/全年):从 ads_mrt_voltage_over_limit_daily_result 查(8维度×4级别=16字段)
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "电压质量指标")
+public class SeVoltageDashboardVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    // ============================================================
+    // 今日(查 alarm_event,不分一般/严重)
+    // ============================================================
+
+    @ApiModelProperty(value = "今日越上限台区数")
+    private Integer todayOverUpperAreaCount;
+
+    @ApiModelProperty(value = "今日越下限台区数")
+    private Integer todayOverLowerAreaCount;
+
+    @ApiModelProperty(value = "今日越上限馈线数")
+    private Integer todayOverUpperFeederCount;
+
+    @ApiModelProperty(value = "今日越下限馈线数")
+    private Integer todayOverLowerFeederCount;
+
+    // ============================================================
+    // 历史(当月/全年,查中台T-2 ads_mrt_voltage_over_limit_daily_result)
+    // 8维度 × 4级别
+    // ============================================================
+    // 维度: 台区(tg) / 用户(cust) / 馈线(feeder) / 母线(generatrix)
+    // 级别: 越上限严重(overUpperSevere) / 越上限一般(overUpperMild)
+    //       越下限严重(overLowerSevere) / 越下限一般(overLowerMild)
+
+    // --- 越上限-严重 ---
+    @ApiModelProperty(value = "越上限台区数-严重")
+    private Integer overUpperSevereTgCount;
+    @ApiModelProperty(value = "越上限户数-严重")
+    private Integer overUpperSevereCustCount;
+    @ApiModelProperty(value = "越上限馈线数-严重")
+    private Integer overUpperSevereFeederCount;
+    @ApiModelProperty(value = "越上限母线数-严重")
+    private Integer overUpperSevereGeneratrixCount;
+
+    // --- 越上限-一般 ---
+    @ApiModelProperty(value = "越上限台区数-一般")
+    private Integer overUpperMildTgCount;
+    @ApiModelProperty(value = "越上限户数-一般")
+    private Integer overUpperMildCustCount;
+    @ApiModelProperty(value = "越上限馈线数-一般")
+    private Integer overUpperMildFeederCount;
+    @ApiModelProperty(value = "越上限母线数-一般")
+    private Integer overUpperMildGeneratrixCount;
+
+    // --- 越下限-严重 ---
+    @ApiModelProperty(value = "越下限台区数-严重")
+    private Integer overLowerSevereTgCount;
+    @ApiModelProperty(value = "越下限户数-严重")
+    private Integer overLowerSevereCustCount;
+    @ApiModelProperty(value = "越下限馈线数-严重")
+    private Integer overLowerSevereFeederCount;
+    @ApiModelProperty(value = "越下限母线数-严重")
+    private Integer overLowerSevereGeneratrixCount;
+
+    // --- 越下限-一般 ---
+    @ApiModelProperty(value = "越下限台区数-一般")
+    private Integer overLowerMildTgCount;
+    @ApiModelProperty(value = "越下限户数-一般")
+    private Integer overLowerMildCustCount;
+    @ApiModelProperty(value = "越下限馈线数-一般")
+    private Integer overLowerMildFeederCount;
+    @ApiModelProperty(value = "越下限母线数-一般")
+    private Integer overLowerMildGeneratrixCount;
+
+    @ApiModelProperty(value = "越限总时长(小时)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal totalOverLimitHours;
+}

+ 40 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/event/MvtransOverVoltageEvent.java

@@ -0,0 +1,40 @@
+package com.hdkj.lt.bf.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 配变电压越限事件
+ * <p>
+ * 配变某断面电压越限时发布,异步监听器去查该配变下用户的电压。
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Getter
+public class MvtransOverVoltageEvent extends ApplicationEvent {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String mvtransId;
+    private final String feederId;
+    private final String countyId;
+    private final String subsId;
+    private final LocalDateTime snapshotTime;
+    private final BigDecimal voltageRatio;
+
+    public MvtransOverVoltageEvent(Object source, String mvtransId, String feederId,
+                                   String countyId, String subsId,
+                                   LocalDateTime snapshotTime, BigDecimal voltageRatio) {
+        super(source);
+        this.mvtransId = mvtransId;
+        this.feederId = feederId;
+        this.countyId = countyId;
+        this.subsId = subsId;
+        this.snapshotTime = snapshotTime;
+        this.voltageRatio = voltageRatio;
+    }
+}

+ 59 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/event/ReconTriggerEvent.java

@@ -0,0 +1,59 @@
+package com.hdkj.lt.bf.event;
+
+import lombok.Getter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 线路重过载告警持续1小时 → 触发重构算法事件
+ * <p>
+ * SeSnapshotService.processFeederCurrent 检测到持续1h过载/重载后发布此事件,
+ * 由 @Async 监听器异步调用同事的重构接口。
+ * 同事接口仅需两个参数:feeder_id(问题线路)、point_time(持续1h的起始断面时刻)
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@Getter
+public class ReconTriggerEvent implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 告警事件ID(t_current_event / t_voltage_event) */
+    private final Long eventId;
+
+    /** 问题线路ID */
+    private final String feederId;
+
+    /** 馈线名称 */
+    private final String feederName;
+
+    /** 区县ID */
+    private final String countyId;
+
+    /** 变电站ID */
+    private final String subsId;
+
+    /** 告警类型(current_overload / current_heavy / voltage_over / voltage_under) */
+    private final String alarmType;
+
+    /** 持续1小时的那个起始断面时刻 */
+    private final LocalDateTime pointTime;
+
+    /** 触发时间 */
+    private final LocalDateTime triggerTime;
+
+    public ReconTriggerEvent(Long eventId, String feederId, String feederName,
+                              String countyId, String subsId, String alarmType,
+                              LocalDateTime pointTime, LocalDateTime triggerTime) {
+        this.eventId = eventId;
+        this.feederId = feederId;
+        this.feederName = feederName;
+        this.countyId = countyId;
+        this.subsId = subsId;
+        this.alarmType = alarmType;
+        this.pointTime = pointTime;
+        this.triggerTime = triggerTime;
+    }
+}

+ 136 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/listener/ReconTriggerListener.java

@@ -0,0 +1,136 @@
+package com.hdkj.lt.bf.listener;
+
+import com.hdkj.hussar.ApiResponse;
+import com.hdkj.lt.bf.adapter.SiReconClient;
+import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
+import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.event.ReconTriggerEvent;
+import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 重构算法触发监听器
+ * <p>
+ * 当线路重过载/电压越限告警持续1小时触发时,异步调用同事编写的重构接口。
+ * 通过 Feign 客户端 SiReconClient 调用 SI 模块的 POST /recon/trigger。
+ * <p>
+ * 调用过程中同步更新告警事件的 reconStatus:
+ * <ul>
+ *   <li>1 — 重构生成中</li>
+ *   <li>2 — 重构完成(有方案)</li>
+ *   <li>3 — 重构完成(无方案)</li>
+ *   <li>-1 — 调用失败</li>
+ * </ul>
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ReconTriggerListener {
+
+    private final SiReconClient siReconClient;
+    private final FhzgSeCurrentEventMapper currentEventMapper;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /** 重构状态常量 */
+    private static final int RECON_PENDING   = 0;  // 未触发
+    private static final int RECON_RUNNING   = 1;  // 生成中
+    private static final int RECON_COMPLETED = 2;  // 有方案
+    private static final int RECON_NO_PLAN   = 3;  // 无方案
+    private static final int RECON_FAILED    = -1; // 调用失败
+
+    /**
+     * 异步监听:告警持续1h → 触发重构算法
+     */
+    @Async
+    @EventListener
+    public void onReconTrigger(ReconTriggerEvent event) {
+        // 1. 标记为"生成中"
+        updateReconStatus(event.getEventId(), event.getAlarmType(), RECON_RUNNING);
+
+        // 2. 构造请求参数
+        Map<String, Object> params = new HashMap<>();
+        params.put("feeder_id", event.getFeederId());
+        params.put("point_time", event.getPointTime() != null
+                ? event.getPointTime().format(DT_FMT) : null);
+
+        Map<String, Object> request = new HashMap<>();
+        request.put("eventId", String.valueOf(event.getEventId()));
+        request.put("params", params);
+
+        log.info("重构触发: eventId={}, feederId={}, alarmType={}, pointTime={}, request={}",
+                event.getEventId(), event.getFeederId(), event.getAlarmType(),
+                event.getPointTime(), request);
+
+        // 3. Feign 调用同事接口
+        try {
+            ApiResponse<Map> resp = siReconClient.triggerRecon(request);
+            log.info("重构接口调用成功: eventId={}, response={}", event.getEventId(), resp);
+
+            // 4. 解析响应判断是否有方案
+            int status = parseReconResponse(resp);
+            updateReconStatus(event.getEventId(), event.getAlarmType(), status);
+        } catch (Exception e) {
+            log.error("重构接口调用失败: eventId={}, feederId={}, alarmType={}, error={}",
+                    event.getEventId(), event.getFeederId(), event.getAlarmType(), e.getMessage(), e);
+            updateReconStatus(event.getEventId(), event.getAlarmType(), RECON_FAILED);
+        }
+    }
+
+    /**
+     * 解析同事重构接口响应,判断是否有方案
+     */
+    private int parseReconResponse(ApiResponse<Map> resp) {
+        if (resp == null || !resp.isSuccess()) {
+            return RECON_FAILED;
+        }
+        Map data = resp.getData();
+        if (data != null) {
+            Object success = data.get("success");
+            if (Boolean.TRUE.equals(success)) {
+                return RECON_COMPLETED;
+            } else if (Boolean.FALSE.equals(success)) {
+                return RECON_NO_PLAN;
+            }
+        }
+        // 调通了但没有明确的 success 标记,保守返回"有方案"
+        return RECON_COMPLETED;
+    }
+
+    /**
+     * 根据 alarmType 前缀更新对应告警事件的 reconStatus
+     */
+    private void updateReconStatus(Long eventId, String alarmType, int reconStatus) {
+        if (eventId == null) return;
+
+        try {
+            if (alarmType != null && alarmType.startsWith("current_")) {
+                FhzgSeCurrentEvent update = new FhzgSeCurrentEvent();
+                update.setId(eventId);
+                update.setReconStatus(reconStatus);
+                currentEventMapper.updateById(update);
+            } else if (alarmType != null && alarmType.startsWith("voltage_")) {
+                FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+                update.setId(eventId);
+                update.setReconStatus(reconStatus);
+                voltageEventMapper.updateById(update);
+            }
+        } catch (Exception e) {
+            log.error("更新告警事件 reconStatus 失败: eventId={}, alarmType={}, status={}, error={}",
+                    eventId, alarmType, reconStatus, e.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,9 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgDwmVoltageOverLimitDaily;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FhzgDwmVoltageOverLimitDailyMapper extends BaseMapper<FhzgDwmVoltageOverLimitDaily> {
+}

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

@@ -0,0 +1,7 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
+
+public interface FhzgSeCapacityDailyMapper extends BaseMapper<FhzgSeCapacityDaily> {
+}

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

@@ -0,0 +1,9 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeConsumerRelation;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FhzgSeConsumerRelationMapper extends BaseMapper<FhzgSeConsumerRelation> {
+}

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

@@ -0,0 +1,7 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
+
+public interface FhzgSeCurrentEventMapper extends BaseMapper<FhzgSeCurrentEvent> {
+}

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

@@ -0,0 +1,7 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
+
+public interface FhzgSeMonitorCurrentMapper extends BaseMapper<FhzgSeMonitorCurrent> {
+}

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

@@ -0,0 +1,7 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
+
+public interface FhzgSeMonitorVoltageMapper extends BaseMapper<FhzgSeMonitorVoltage> {
+}

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

@@ -0,0 +1,9 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FhzgSeSnapshotDetailMapper extends BaseMapper<FhzgSeSnapshotDetail> {
+}

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

@@ -0,0 +1,7 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+
+public interface FhzgSeVoltageEventMapper extends BaseMapper<FhzgSeVoltageEvent> {
+}

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

@@ -0,0 +1,15 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.core.bizms.modle.po.GridPdOutageEventBase;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 停电事件表 Mapper(bf 侧,用于查询当天实时停电数据)
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+@Mapper
+public interface GridPdOutageEventBaseMapper extends BaseMapper<GridPdOutageEventBase> {
+}

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

@@ -3,6 +3,7 @@ package com.hdkj.lt.bf.mapper;
 import com.hdkj.lt.bf.entity.SjztPwwyyqxtPrmPdrOutageMvDs;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
+import com.hdkj.lt.bf.entity.dto.OutageAggregationDTO;
 import org.apache.ibatis.annotations.Param;
 
 import java.math.BigDecimal;
@@ -19,6 +20,8 @@ import java.util.List;
 public interface SjztPwwyyqxtPrmPdrOutageMvDsMapper extends BaseMapper<SjztPwwyyqxtPrmPdrOutageMvDs> {
 
     BigDecimal selectStaticOutageDurationHoursHouseholds(@Param("feederNameList")List<String> feederNameList, @Param("startOfMonth")LocalDateTime startOfMonth, @Param("endOfToday")LocalDateTime endOfToday);
+
+    OutageAggregationDTO selectOutageAggregation(@Param("feederNameList")List<String> feederNameList, @Param("startTime")LocalDateTime startTime, @Param("endTime")LocalDateTime endTime);
 }
 
 

+ 135 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/schedule/SeSnapshotScheduler.java

@@ -0,0 +1,135 @@
+package com.hdkj.lt.bf.schedule;
+
+import com.alibaba.fastjson.JSON;
+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.service.SeSnapshotService;
+import com.hdkj.lt.core.bizms.modle.dto.StateEstimation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * 状估断面定时拉取任务
+ * <p>
+ * 每15分钟调用同事的 queryEquipByCounty 接口,按县域拉取最新状估数据,
+ * 聚合后喂给 SeSnapshotService.processSnapshot() 解析入库,
+ * 完成状估数据→断面明细→状态机监测→告警事件的完整链路。
+ * <p>
+ * 县域间通过 CompletableFuture 并行拉取,互不阻塞。
+ *
+ * @author lsl
+ * @since 2026-07-26
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SeSnapshotScheduler {
+
+    private final SiStateEstimationClient siStateEstimationClient;
+    private final SeSnapshotService seSnapshotService;
+
+    @Scheduled(cron = "0 */15 * * * ?")
+    public void pullStateEstimation() {
+        log.info("【状估拉取】开始定时拉取状估数据");
+
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (Map.Entry<String, String[]> county : SeSnapshotConstants.COUNTIES) {
+            String maintOrg = county.getKey();
+            String countySid = county.getValue()[0];
+            String countyName = county.getValue()[1];
+            futures.add(CompletableFuture.runAsync(() ->
+                    pullByCounty(maintOrg, countySid, countyName)));
+        }
+        // 等待所有县城拉取完成
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+    }
+
+    private void pullByCounty(String maintOrg, String countySid, String countyName) {
+        try {
+            MaintOrgRequest request = new MaintOrgRequest();
+            request.setMaintOrg(maintOrg);
+
+            ApiResponse<List<StateEstimation>> resp = siStateEstimationClient.queryEquipByCounty(request);
+            if (resp == null || !resp.isSuccess() || resp.getData() == null || resp.getData().isEmpty()) {
+                log.warn("【状估拉取】县域 {} 无数据或调用失败, resp={}, msg={}",
+                        countyName, resp != null ? resp.isSuccess() : "null",
+                        resp != null ? resp.getMsg() : "");
+                return;
+            }
+
+            // 将 List<StateEstimation> 聚合成单个 StateEstimation
+            StateEstimation merged = mergeCountyResults(resp.getData());
+            if (merged == null) return;
+
+            // 序列化 JSON 并喂给解析方法
+            String json = JSON.toJSONString(merged);
+            seSnapshotService.processSnapshot(json);
+
+            log.info("【状估拉取】县域 {} 状估数据拉取解析完成, 断面时刻={}", countyName, merged.getPointTime());
+        } catch (Exception e) {
+            log.error("【状估拉取】县域 {} 拉取异常: {}", countyName, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 聚合 List<StateEstimation> → 单个 StateEstimation
+     * <p>
+     * queryEquipByCounty 返回每条馈线一个 StateEstimation 对象,
+     * 需要把 feeder/mvtrans/switch 等列表合并到一起。
+     */
+    private StateEstimation mergeCountyResults(List<StateEstimation> list) {
+        if (list == null || list.isEmpty()) return null;
+
+        StateEstimation merged = new StateEstimation();
+        List<StateEstimation.StateEstimationCommonResult> allFeeders = new ArrayList<>();
+        List<StateEstimation.StateEstimationTransResult> allMvtrans = new ArrayList<>();
+        List<StateEstimation.StateEstimationSwitchResult> allSwitches = new ArrayList<>();
+        List<StateEstimation.StateEstimationCommonResult> allEquips = new ArrayList<>();
+        List<StateEstimation.StateEstimationSegmentResult> allSegments = new ArrayList<>();
+        List<String> pointTime = null;
+
+        for (StateEstimation se : list) {
+            if (se == null) continue;
+            if (pointTime == null && se.getPointTime() != null) {
+                pointTime = se.getPointTime();
+            }
+            if (se.getPeriodFeederSeResult() != null) {
+                allFeeders.addAll(se.getPeriodFeederSeResult());
+            }
+            if (se.getPeriodMVTransSeResult() != null) {
+                allMvtrans.addAll(se.getPeriodMVTransSeResult());
+            }
+            if (se.getPeriodSwitchSeResult() != null) {
+                allSwitches.addAll(se.getPeriodSwitchSeResult());
+            }
+            if (se.getPeriodEquipResult() != null) {
+                allEquips.addAll(se.getPeriodEquipResult());
+            }
+            if (se.getPeriodSegmentSeResult() != null) {
+                allSegments.addAll(se.getPeriodSegmentSeResult());
+            }
+        }
+
+        if (allFeeders.isEmpty() && allMvtrans.isEmpty()) {
+            log.warn("【状估拉取】聚合后无任何数据");
+            return null;
+        }
+
+        merged.setPeriodFeederSeResult(allFeeders);
+        merged.setPeriodMVTransSeResult(allMvtrans);
+        merged.setPeriodSwitchSeResult(allSwitches);
+        merged.setPeriodEquipResult(allEquips);
+        merged.setPeriodSegmentSeResult(allSegments);
+        merged.setPointTime(pointTime);
+        return merged;
+    }
+}

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

@@ -0,0 +1,152 @@
+package com.hdkj.lt.bf.scheduler;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
+import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
+import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 供电能力日汇总定时任务
+ * 每天凌晨1点,将昨天的断面明细聚合写入 fhzg_se_capacity_daily
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SeCapacityDailyScheduler {
+
+    private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
+    private final FhzgSeCapacityDailyMapper capacityDailyMapper;
+
+    /**
+     * 每天凌晨1点执行
+     */
+    @Scheduled(cron = "0 0 1 * * ?")
+    public void aggregateYesterdayCapacity() {
+        LocalDate yesterday = LocalDate.now().minusDays(1);
+        log.info("[供电能力日汇总] 开始处理 {} 数据", yesterday);
+
+        try {
+            aggregateForDate(yesterday);
+            log.info("[供电能力日汇总] {} 处理完成", yesterday);
+        } catch (Exception e) {
+            log.error("[供电能力日汇总] {} 处理失败", yesterday, e);
+        }
+    }
+
+    /**
+     * 聚合指定日期的断面数据到日汇总表
+     */
+    private void aggregateForDate(LocalDate date) {
+        LocalDateTime startTime = date.atStartOfDay();
+        LocalDateTime endTime = date.atTime(23, 59, 59);
+
+        // 查昨天所有馈线断面
+        LambdaQueryWrapper<FhzgSeSnapshotDetail> wrapper = new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                .isNotNull(FhzgSeSnapshotDetail::getLoadRate)
+                .ge(FhzgSeSnapshotDetail::getSnapshotTime, startTime)
+                .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime);
+
+        List<FhzgSeSnapshotDetail> snapshots = snapshotDetailMapper.selectList(wrapper);
+
+        if (snapshots.isEmpty()) {
+            log.info("[供电能力日汇总] {} 无断面数据,跳过", date);
+            return;
+        }
+
+        // 按馈线分组聚合
+        Map<String, DailyAccumulator> accumulatorMap = new HashMap<>();
+        for (FhzgSeSnapshotDetail s : snapshots) {
+            String feederId = s.getFeederId();
+            if (feederId == null) continue;
+
+            DailyAccumulator acc = accumulatorMap.computeIfAbsent(feederId, k -> new DailyAccumulator());
+            acc.countyId = s.getCountyId();
+            acc.subsId = s.getSubsId();
+
+            BigDecimal loadRate = s.getLoadRate();
+            if (loadRate != null) {
+                if (loadRate.compareTo(acc.maxLoadRate) > 0) {
+                    acc.maxLoadRate = loadRate;
+                }
+                acc.sumLoadRate = acc.sumLoadRate.add(loadRate);
+                acc.totalSnapshots++;
+            }
+
+            Integer status = s.getCurrentStatus();
+            if (status != null) {
+                if (status == 1) acc.heavyCount++;
+                if (status == 2) acc.overloadCount++;
+            }
+        }
+
+        // 先删当天已有数据(避免重复运行时叠加)
+        LambdaQueryWrapper<FhzgSeCapacityDaily> deleteWrapper = new LambdaQueryWrapper<FhzgSeCapacityDaily>()
+                .eq(FhzgSeCapacityDaily::getStatDate, date);
+        capacityDailyMapper.delete(deleteWrapper);
+
+        // 批量插入
+        List<FhzgSeCapacityDaily> dailyRecords = new ArrayList<>();
+        for (Map.Entry<String, DailyAccumulator> entry : accumulatorMap.entrySet()) {
+            String feederId = entry.getKey();
+            DailyAccumulator acc = entry.getValue();
+
+            BigDecimal avgLoadRate = acc.totalSnapshots > 0
+                    ? acc.sumLoadRate.divide(BigDecimal.valueOf(acc.totalSnapshots), 2, RoundingMode.HALF_UP)
+                    : BigDecimal.ZERO;
+
+            FhzgSeCapacityDaily daily = FhzgSeCapacityDaily.builder()
+                    .statDate(date)
+                    .feederId(feederId)
+                    .feederName(null)
+                    .countyId(acc.countyId)
+                    .subsId(acc.subsId)
+                    .maxLoadRate(acc.maxLoadRate)
+                    .avgLoadRate(avgLoadRate)
+                    .heavyCount(acc.heavyCount)
+                    .overloadCount(acc.overloadCount)
+                    .totalSnapshots(acc.totalSnapshots)
+                    .createTime(LocalDateTime.now())
+                    .updateTime(LocalDateTime.now())
+                    .build();
+            dailyRecords.add(daily);
+        }
+
+        // 批量插入
+        for (FhzgSeCapacityDaily record : dailyRecords) {
+            capacityDailyMapper.insert(record);
+        }
+
+        log.info("[供电能力日汇总] {} 处理完成: {} 条馈线日汇总", date, dailyRecords.size());
+    }
+
+    /**
+     * 日聚合累加器
+     */
+    private static class DailyAccumulator {
+        String countyId;
+        String subsId;
+        BigDecimal maxLoadRate = BigDecimal.ZERO;
+        BigDecimal sumLoadRate = BigDecimal.ZERO;
+        int heavyCount = 0;
+        int overloadCount = 0;
+        int totalSnapshots = 0;
+    }
+}

+ 24 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/IndicatorDashboardService.java

@@ -0,0 +1,24 @@
+package com.hdkj.lt.bf.service;
+
+import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
+
+/**
+ * 运行态指标仪表盘 Service
+ * <p>
+ * 对应前端"综合态势"仪表盘,3大模块:
+ * 供电可靠性 / 电压质量 / 供电能力
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+public interface IndicatorDashboardService {
+
+    /**
+     * 查询运行态仪表盘数据
+     *
+     * @param id   节点设备ID(区县maintOrgId / 变电站ID / 馈线ID)
+     * @param type 节点类型 2=区县 3=变电站 4=馈线
+     * @return 仪表盘 VO,停电数据已接入,其余字段预留 null
+     */
+    IndicatorDashboardVO queryDashboard(String id, String type);
+}

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

@@ -0,0 +1,56 @@
+package com.hdkj.lt.bf.service;
+
+import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
+import com.hdkj.lt.bf.entity.vo.PageResult;
+import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
+import com.hdkj.lt.bf.entity.vo.SeAlarmListVO;
+
+import java.util.List;
+
+/**
+ * 异常告警查询 Service
+ * <p>
+ * 对应前端"异常告警"页面:
+ * - 线路重过载列表(Tab1)+ 分页
+ * - 电压越限列表(Tab2)+ 分页
+ * - 运行异常信息详情
+ * - 线路名称下拉选择
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+public interface SeAlarmService {
+
+    /**
+     * 分页查询指定日期重过载告警列表
+     *
+     * @param date  查询日期 yyyy-MM-dd,为空则当天
+     * @param orderDir 排序方向 asc/desc,为空则 desc
+     */
+    PageResult<SeAlarmListVO> queryOverloadAlarmList(String id, Integer type, String feederName,
+                                                      String date, String orderDir,
+                                                      Integer page, Integer pageSize);
+
+    /**
+     * 分页查询指定日期电压越限告警列表
+     *
+     * @param date  查询日期 yyyy-MM-dd,为空则当天
+     * @param orderDir 排序方向 asc/desc,为空则 desc
+     */
+    PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
+                                                     String date, String orderDir,
+                                                     Integer page, Integer pageSize);
+
+    /**
+     * 查询运行异常信息详情
+     */
+    SeAlarmDetailVO queryAlarmDetail(Long alarmEventId);
+
+    /**
+     * 查询线路下拉列表(通过id+type过滤后的全量线路)
+     *
+     * @param id   节点ID
+     * @param type 节点类型 2=区县 3=变电站 4=馈线(type=4时返回自身)
+     */
+    List<FeederSelectVO> queryFeederList(String id, Integer type);
+}

+ 40 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeIndicatorService.java

@@ -0,0 +1,40 @@
+package com.hdkj.lt.bf.service;
+
+import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
+
+/**
+ * 状估监测体系 三个查询接口 Service
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+public interface SeIndicatorService {
+
+    /**
+     * 停电时户数(无时间维度切换)
+     *
+     * @param id   节点ID
+     * @param type 节点类型 2=区县 3=变电站 4=馈线
+     */
+    SeOutageDashboardVO queryOutage(String id, Integer type);
+
+    /**
+     * 电压质量
+     *
+     * @param id       节点ID
+     * @param type     节点类型 2=区县 3=变电站 4=馈线
+     * @param timeScope today/month/year
+     */
+    SeVoltageDashboardVO queryVoltage(String id, Integer type, String timeScope);
+
+    /**
+     * 供电能力
+     *
+     * @param id       节点ID
+     * @param type     节点类型 2=区县 3=变电站 4=馈线
+     * @param timeScope today/month/year
+     */
+    SeCapacityDashboardVO queryCapacity(String id, Integer type, String timeScope);
+}

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

@@ -0,0 +1,31 @@
+package com.hdkj.lt.bf.service;
+
+/**
+ * 状估断面处理 Service
+ * <p>
+ * 接收同事推送的状估 JSON 字符串(多区县断面),解析后:
+ * 1. 写断面明细表 fhzg_se_snapshot_detail
+ * 2. UPSERT 电流状态机 fhzg_se_monitor_current(馈线负载率)
+ * 3. UPSERT 电压状态机 fhzg_se_monitor_voltage(馈线/配变/用户电压)
+ * 4. 写告警事件 fhzg_se_current_event / fhzg_se_voltage_event
+ * 5. 配变电压越限时发 Spring 事件,异步查用户电压
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+public interface SeSnapshotService {
+
+    /**
+     * 处理一个状估断面(单县 JSONObject)
+     *
+     * @param snapshotJson 状估JSON字符串(单县)
+     */
+    void processSnapshot(String snapshotJson);
+
+    /**
+     * 处理一批状估断面(多县 JSONArray)
+     *
+     * @param snapshotJsonArray 状估JSON字符串(多县数组)
+     */
+    void processSnapshots(String snapshotJsonArray);
+}

+ 38 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SjztPwwyyqxtPrmPdrOutageMvDsService.java

@@ -1,9 +1,11 @@
 package com.hdkj.lt.bf.service;
 
 import com.hdkj.lt.bf.entity.SjztPwwyyqxtPrmPdrOutageMvDs;
+import com.hdkj.lt.bf.entity.dto.TodayOutageData;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -13,5 +15,41 @@ import java.util.List;
 */
 public interface SjztPwwyyqxtPrmPdrOutageMvDsService extends IService<SjztPwwyyqxtPrmPdrOutageMvDs> {
 
+    /**
+     * 按馈线名单+时间范围查询停电时户数(月份硬编码,静态网架用)
+     */
     BigDecimal selectStaticOutageDurationHoursHouseholds(List<String> feederNameList);
+
+    /**
+     * 分层分级获取停电聚合数据(树递归 + 自定义时间范围,适用于 T-2 表)
+     * <p>
+     * 支持三种节点维度:
+     * type=2 区县 — 递归收集该区县所有馈线的停电时户数
+     * type=3 变电站 — 递归收集该变电站所有馈线的停电时户数
+     * type=4 馈线 — 直接查该单条馈线的停电时户数
+     * <p>
+     * 返回 T-2 表的总停电时户数和总影响用户数(中压+低压),调用方自行计算平均时长。
+     *
+     * @param id        节点设备ID(区县maintOrgId / 变电站ID / 馈线ID)
+     * @param type      节点类型 2=区县 3=变电站 4=馈线
+     * @param startTime 开始时间 inclusive
+     * @param endTime   结束时间 exclusive
+     * @return 停电聚合数据(总时户数 + 总影响用户数),无数据时返回 (0, 0)
+     */
+    TodayOutageData getOutageDurationHoursHouseholds(String id, Integer type,
+                                                     LocalDateTime startTime, LocalDateTime endTime);
+
+    /**
+     * 获取当日停电数据(来自 grid_pd_outage_event_base 实时表)
+     * <p>
+     * 不需要树递归,按节点类型直接过滤字段:
+     * type=2 → county_id
+     * type=3 → rdt_subs_id
+     * type=4 → rdt_feeder_id
+     *
+     * @param id   节点设备ID
+     * @param type 节点类型 2=区县 3=变电站 4=馈线
+     * @return 今日停电数据(时户数 + 平均时长),无数据时返回 (0, 0)
+     */
+    TodayOutageData getTodayOutageData(String id, Integer type);
 }

+ 95 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/IndicatorDashboardServiceImpl.java

@@ -0,0 +1,95 @@
+package com.hdkj.lt.bf.service.impl;
+
+import com.hdkj.lt.bf.entity.dto.TodayOutageData;
+import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
+import com.hdkj.lt.bf.service.IndicatorDashboardService;
+import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 运行态指标仪表盘 Service 实现
+ * <p>
+ * 当前实现:
+ * - ✅ 供电可靠性:停电时户数(今日 + 全年)、平均停电时长(今日 + 全年)
+ * - ⏳ 电压质量、供电能力:预留 null(待后续接入数据源)
+ *
+ * @author lsl
+ * @since 2026-07-21
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class IndicatorDashboardServiceImpl implements IndicatorDashboardService {
+
+    private final SjztPwwyyqxtPrmPdrOutageMvDsService outageService;
+
+    @Override
+    public IndicatorDashboardVO queryDashboard(String id, String type) {
+        if (StringUtils.isBlank(id) || StringUtils.isBlank(type)) {
+            log.warn("参数缺失: id={}, type={}", id, type);
+            return null;
+        }
+
+        int nodeType;
+        try {
+            nodeType = Integer.parseInt(type);
+        } catch (NumberFormatException e) {
+            log.warn("type 参数非法: {}", type);
+            return null;
+        }
+
+        // ========== 供电可靠性 ==========
+
+        // 今日 — 实时表 grid_pd_outage_event_base
+        TodayOutageData today = outageService.getTodayOutageData(id, nodeType);
+
+        // 全年(1月1日 → 昨天)— T-2 表 sjzt_pwwyyqxt_prm_pdr_outage_mv_ds
+        LocalDate todayDate = LocalDate.now();
+        int year = todayDate.getYear();
+        LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
+        LocalDateTime yesterdayEnd = todayDate.minusDays(1).atTime(23, 59, 59);
+
+        TodayOutageData yearStats = outageService.getOutageDurationHoursHouseholds(id, nodeType, yearStart, yesterdayEnd);
+
+        // ========== 组装 VO ==========
+
+        return IndicatorDashboardVO.builder()
+                // 基本信息(预留)
+                .indicatorResultId(null)
+                .batchTime(null)
+                .countyName(null)
+                // 一、供电可靠性
+                .outageHoursHouseholdsToday(today.getTotalHouseholds())
+                .avgOutageDurationToday(today.getAvgDurationHours())
+                .outageHoursHouseholdsYear(yearStats.getTotalHouseholds())
+                .avgOutageDurationYear(yearStats.getAvgDurationHours())
+                // 二、电压质量(全部预留)
+                .iecUserVoltageIndex(null)
+                .overUpperLimitAreaSevere(null)
+                .overUpperLimitAreaGeneral(null)
+                .overUpperLimitUserSevere(null)
+                .overUpperLimitUserGeneral(null)
+                .belowLowerLimitAreaSevere(null)
+                .belowLowerLimitAreaGeneral(null)
+                .belowLowerLimitUserSevere(null)
+                .belowLowerLimitUserGeneral(null)
+                // 三、供电能力(全部预留)
+                .maxLoadRate(null)
+                .avgLoadRate(null)
+                .overloadEventTotal(null)
+                .heavyLoadEventCount(null)
+                .heavyLoadRate(null)
+                .overloadEventCount(null)
+                .overloadRate(null)
+                .publicLineLossRate(null)
+                .lineLossRate(null)
+                .areaLineLossComplianceRate(null)
+                .build();
+    }
+}

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

@@ -0,0 +1,333 @@
+package com.hdkj.lt.bf.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+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.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.application.FeederGroupApplication;
+import com.hdkj.lt.bf.entity.vo.*;
+import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.service.SeAlarmService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 异常告警查询 Service 实现 V2
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SeAlarmServiceImpl implements SeAlarmService {
+
+    private final FhzgSeCurrentEventMapper currentEventMapper;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final FeederGroupApplication feederGroupApplication;
+    private final FhzgSeMonitorCurrentMapper monitorCurrentMapper;
+    private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
+
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private static final List<String> OVERLOAD_TYPES = Arrays.asList("current_heavy", "current_overload");
+    private static final List<String> VOLTAGE_TYPES = Arrays.asList(
+            "voltage_over", "voltage_under", "voltage_over_general", "voltage_over_severe",
+            "voltage_under_general", "voltage_under_severe");
+
+    // ============================================================
+    // 1. 重过载列表
+    // ============================================================
+
+    @Override
+    public PageResult<SeAlarmListVO> queryOverloadAlarmList(String id, Integer type, String feederName,
+                                                             String date, String orderDir,
+                                                             Integer page, Integer pageSize) {
+        if (page == null || page < 1) page = 1;
+        if (pageSize == null || pageSize < 1) pageSize = 20;
+
+        LocalDate queryDate = parseDate(date);
+        LocalDateTime dayStart = queryDate.atStartOfDay();
+        LocalDateTime dayEnd = queryDate.plusDays(1).atStartOfDay();
+
+        Page<FhzgSeCurrentEvent> mpPage = new Page<>(page, pageSize);
+
+        LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
+                .ge(FhzgSeCurrentEvent::getFirstOverTime, dayStart)
+                .lt(FhzgSeCurrentEvent::getFirstOverTime, dayEnd)
+                .eq(FhzgSeCurrentEvent::getStatus, 1)
+                .in(FhzgSeCurrentEvent::getAlarmType, OVERLOAD_TYPES);
+
+        applyCurrentFilter(wrapper, id, type);
+        if (StringUtils.isNotBlank(feederName)) {
+            wrapper.like(FhzgSeCurrentEvent::getFeederName, feederName);
+        }
+        applyOrderBy(wrapper, orderDir, FhzgSeCurrentEvent::getFirstOverTime);
+
+        Page<FhzgSeCurrentEvent> result = currentEventMapper.selectPage(mpPage, wrapper);
+        List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toOverloadVO).collect(Collectors.toList());
+        return PageResult.of(result, voList);
+    }
+
+    private SeAlarmListVO toOverloadVO(FhzgSeCurrentEvent e) {
+        String displayValue = e.getLastLoadRate() != null
+                ? e.getLastLoadRate().setScale(0, java.math.RoundingMode.HALF_UP).toPlainString()
+                : "80";
+        return SeAlarmListVO.builder()
+                .alarmEventId(e.getId())
+                .feederName(e.getFeederName())
+                .feederId(e.getFeederId())
+                .alarmDisplayValue(displayValue)
+                .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmType(e.getAlarmType())
+                .alarmLevel("feeder")
+                .build();
+    }
+
+    // ============================================================
+    // 2. 电压越限列表
+    // ============================================================
+
+    @Override
+    public PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
+                                                            String date, String orderDir,
+                                                            Integer page, Integer pageSize) {
+        if (page == null || page < 1) page = 1;
+        if (pageSize == null || pageSize < 1) pageSize = 20;
+
+        LocalDate queryDate = parseDate(date);
+        LocalDateTime dayStart = queryDate.atStartOfDay();
+        LocalDateTime dayEnd = queryDate.plusDays(1).atStartOfDay();
+
+        Page<FhzgSeVoltageEvent> mpPage = new Page<>(page, pageSize);
+
+        LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                .ge(FhzgSeVoltageEvent::getFirstOverTime, dayStart)
+                .lt(FhzgSeVoltageEvent::getFirstOverTime, dayEnd)
+                .eq(FhzgSeVoltageEvent::getStatus, 1)
+                .in(FhzgSeVoltageEvent::getAlarmLevel, "feeder", "mvtrans");
+
+        applyVoltageFilter(wrapper, id, type);
+        if (StringUtils.isNotBlank(feederName)) {
+            wrapper.like(FhzgSeVoltageEvent::getFeederName, feederName);
+        }
+        applyOrderBy(wrapper, orderDir, FhzgSeVoltageEvent::getFirstOverTime);
+
+        Page<FhzgSeVoltageEvent> result = voltageEventMapper.selectPage(mpPage, wrapper);
+        List<SeAlarmListVO> voList = result.getRecords().stream().map(this::toVoltageVO).collect(Collectors.toList());
+        return PageResult.of(result, voList);
+    }
+
+    private SeAlarmListVO toVoltageVO(FhzgSeVoltageEvent e) {
+        String displayValue = resolveVoltageDisplay(e.getAlarmType(), e.getAlarmLevel());
+        String feederName = e.getDeviceName();
+        // 台区告警时显示馈线名称
+        if ("mvtrans".equals(e.getAlarmLevel())) {
+            feederName = e.getFeederName();
+        }
+        return SeAlarmListVO.builder()
+                .alarmEventId(e.getId())
+                .feederName(feederName)
+                .feederId(e.getFeederId())
+                .alarmDisplayValue(displayValue)
+                .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
+                .alarmType(e.getAlarmType())
+                .alarmLevel(e.getAlarmLevel())
+                .build();
+    }
+
+    private String resolveVoltageDisplay(String alarmType, String alarmLevel) {
+        boolean isMvtrans = "mvtrans".equals(alarmLevel);
+        String prefix = isMvtrans ? "台区" : "线路";
+        if (alarmType != null && alarmType.contains("over")) return prefix + "电压越上限";
+        if (alarmType != null && alarmType.contains("under")) return prefix + "电压越下限";
+        return prefix + "电压越限";
+    }
+
+    // ============================================================
+    // 3. 运行异常信息详情
+    // ============================================================
+
+    @Override
+    public SeAlarmDetailVO queryAlarmDetail(Long alarmEventId) {
+        if (alarmEventId == null) return null;
+
+        // 先查重过载事件
+        FhzgSeCurrentEvent currentEvent = currentEventMapper.selectById(alarmEventId);
+        if (currentEvent != null) {
+            return buildOverloadDetail(currentEvent);
+        }
+
+        // 再查电压越限事件
+        FhzgSeVoltageEvent voltageEvent = voltageEventMapper.selectById(alarmEventId);
+        if (voltageEvent != null) {
+            return buildVoltageDetail(voltageEvent);
+        }
+
+        return null;
+    }
+
+    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();
+
+        return SeAlarmDetailVO.builder()
+                .alarmEventId(event.getId())
+                .alarmType(event.getAlarmType())
+                .alarmCategory("overload")
+                .countyName(event.getCountyName())
+                .feederName(event.getFeederName())
+                .alarmStartTime(event.getFirstOverTime())
+                .alarmEndTime(endTime)
+                .maxLoadRate(event.getMaxLoadRate())
+                .reconStatus(event.getReconStatus())
+                .build();
+    }
+
+    private SeAlarmDetailVO buildVoltageDetail(FhzgSeVoltageEvent event) {
+        String indicator = resolveVoltageDisplay(event.getAlarmType(), event.getAlarmLevel());
+        String feederName = event.getFeederName();
+
+        LocalDateTime endTime = event.getRecoverTime();
+        if (endTime == null && event.getStatus() == 1) {
+            FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
+                    new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                            .eq(FhzgSeMonitorVoltage::getDeviceId, event.getDeviceId())
+                            .eq(FhzgSeMonitorVoltage::getDeviceType, event.getDeviceType())
+                            .last("LIMIT 1"));
+            if (state != null) endTime = state.getLastSnapTime();
+        }
+        if (endTime == null) endTime = event.getTriggerTime();
+
+        return SeAlarmDetailVO.builder()
+                .alarmEventId(event.getId())
+                .alarmType(event.getAlarmType())
+                .alarmCategory("voltage")
+                .countyName(event.getCountyName())
+                .feederName(feederName)
+                .alarmStartTime(event.getFirstOverTime())
+                .alarmEndTime(endTime)
+                .abnormalIndicator(indicator)
+                .maxVoltage(event.getVoltageRangeMax())
+                .minVoltage(event.getVoltageRangeMin())
+                .overUpperMvtransCount(event.getOverUpperMvtrans())
+                .overLowerMvtransCount(event.getOverLowerMvtrans())
+                .reconStatus(event.getReconStatus())
+                .build();
+    }
+
+    // ============================================================
+    // 辅助
+    // ============================================================
+
+    private void applyCurrentFilter(LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper, String id, Integer type) {
+        if (type == 2) wrapper.eq(FhzgSeCurrentEvent::getCountyId, id);
+        else if (type == 3) wrapper.eq(FhzgSeCurrentEvent::getSubsId, id);
+        else if (type == 4) wrapper.eq(FhzgSeCurrentEvent::getFeederId, id);
+    }
+
+    private void applyVoltageFilter(LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper, String id, Integer type) {
+        if (type == 2) wrapper.eq(FhzgSeVoltageEvent::getCountyId, id);
+        else if (type == 3) wrapper.eq(FhzgSeVoltageEvent::getSubsId, id);
+        else if (type == 4) wrapper.eq(FhzgSeVoltageEvent::getFeederId, id);
+    }
+
+    // ============================================================
+    // 线路下拉列表
+    // ============================================================
+
+    @Override
+    public List<FeederSelectVO> queryFeederList(String id, Integer type) {
+        List<FeederSelectVO> result = new java.util.ArrayList<>();
+        if (id == null || type == null) return result;
+
+        try {
+            java.util.List<com.hdkj.fhzg.optimization.response.FeederGroupTreeNode> tree = feederGroupApplication.getFeederGroupTree();
+            if (tree == null) return result;
+
+            // 找到目标节点
+            com.hdkj.fhzg.optimization.response.FeederGroupTreeNode target = findNode(tree, id, type);
+            if (target == null) return result;
+
+            // 收集其下所有 type=4 的馈线
+            collectFeeders(target, result);
+        } catch (Exception e) {
+            log.warn("查询线路下拉列表失败 id={}, type={}", id, type, e);
+        }
+        return result;
+    }
+
+    private com.hdkj.fhzg.optimization.response.FeederGroupTreeNode findNode(
+            java.util.List<com.hdkj.fhzg.optimization.response.FeederGroupTreeNode> nodes,
+            String id, Integer type) {
+        if (nodes == null) return null;
+        for (com.hdkj.fhzg.optimization.response.FeederGroupTreeNode node : nodes) {
+            if (type.equals(node.getType()) && id.equals(node.getId())) {
+                return node;
+            }
+            if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+                com.hdkj.fhzg.optimization.response.FeederGroupTreeNode found = findNode(node.getChildren(), id, type);
+                if (found != null) return found;
+            }
+        }
+        return null;
+    }
+
+    // ============================================================
+    // 辅助:日期解析 + 排序
+    // ============================================================
+
+    private LocalDate parseDate(String date) {
+        if (StringUtils.isBlank(date)) return LocalDate.now();
+        try {
+            return LocalDate.parse(date);
+        } catch (Exception e) {
+            log.warn("日期格式非法: {}, 默认当天", date);
+            return LocalDate.now();
+        }
+    }
+
+    private <T> void applyOrderBy(LambdaQueryWrapper<T> wrapper, String orderDir,
+                                   com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> column) {
+        if ("asc".equalsIgnoreCase(orderDir)) {
+            wrapper.orderByAsc(column);
+        } else {
+            wrapper.orderByDesc(column);
+        }
+    }
+
+    private void collectFeeders(com.hdkj.fhzg.optimization.response.FeederGroupTreeNode node,
+                                 java.util.List<FeederSelectVO> result) {
+        if (node.getType() == 4) {
+            result.add(new FeederSelectVO(node.getId(), node.getName()));
+        }
+        if (node.getChildren() != null) {
+            for (com.hdkj.fhzg.optimization.response.FeederGroupTreeNode child : node.getChildren()) {
+                collectFeeders(child, result);
+            }
+        }
+    }
+}

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

@@ -0,0 +1,210 @@
+package com.hdkj.lt.bf.service.impl;
+
+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.entity.FhzgSeConsumerRelation;
+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.event.MvtransOverVoltageEvent;
+import com.hdkj.lt.bf.mapper.FhzgSeConsumerRelationMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 用户电压异步监测 Service V2
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SeConsumerVoltageServiceImpl {
+
+    private final FhzgSeConsumerRelationMapper consumerRelationMapper;
+    private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
+    private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final SiMeasurementClient siMeasurementClient;
+
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Async("seAsyncExecutor")
+    @EventListener
+    public void onMvtransOverVoltage(MvtransOverVoltageEvent event) {
+        String mvtransId = event.getMvtransId();
+        LocalDateTime snapTime = event.getSnapshotTime();
+        log.info("配变电压越限,异步查用户电压 mvtransId={}, snapTime={}", mvtransId, snapTime);
+
+        List<FhzgSeConsumerRelation> relations = consumerRelationMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeConsumerRelation>()
+                        .eq(FhzgSeConsumerRelation::getMvtransId, mvtransId));
+        if (relations == null || relations.isEmpty()) return;
+
+        List<String> consNos = relations.stream()
+                .map(FhzgSeConsumerRelation::getCustNo).filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (consNos.isEmpty()) return;
+        Map<String, FhzgSeConsumerRelation> consNoMap = relations.stream()
+                .filter(r -> r.getCustNo() != null)
+                .collect(Collectors.toMap(FhzgSeConsumerRelation::getCustNo, r -> r, (a, b) -> a));
+
+        String startTime = snapTime.minusMinutes(15).format(DT_FMT);
+        String endTime = snapTime.format(DT_FMT);
+
+        Map<String, Object> reqParam = new HashMap<>();
+        reqParam.put("consNos", consNos);
+        reqParam.put("startTime", startTime);
+        reqParam.put("endTime", endTime);
+
+        Map<String, BigDecimal> voltageMap;
+        try {
+            ApiResponse<Map<String, BigDecimal>> resp = siMeasurementClient.queryConsumerVoltage(reqParam);
+            if (resp == null || resp.getData() == null) return;
+            voltageMap = resp.getData();
+        } catch (Exception e) {
+            log.error("Feign调SI量测中心失败 mvtransId={}", mvtransId, e);
+            return;
+        }
+
+        for (Map.Entry<String, BigDecimal> entry : voltageMap.entrySet()) {
+            String consNo = entry.getKey();
+            BigDecimal measValue = entry.getValue();
+            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());
+
+            FhzgSeSnapshotDetail detail = FhzgSeSnapshotDetail.builder()
+                    .snapshotTime(snapTime).deviceId(consNo).deviceType("consumer")
+                    .feederId(rel.getFeederId()).subsId(rel.getSubsId()).countyId(rel.getCountyId())
+                    .mvtransId(mvtransId).consumerId(consNo)
+                    .lvVoltageA(measValue).voltageRatio(voltageRatio).voltageStatus(voltageStatus)
+                    .build();
+            snapshotDetailMapper.insert(detail);
+
+            upsertConsumerVoltageMonitor(consNo, "consumer",
+                    rel.getFeederId(), rel.getSubsId(), rel.getCountyId(), mvtransId, consNo,
+                    voltageRatio, voltageStatus, snapTime);
+        }
+        log.info("配变{}用户电压处理完成 共{}户,成功{}户", mvtransId, consNos.size(), voltageMap.size());
+    }
+
+    private void upsertConsumerVoltageMonitor(String deviceId, String deviceType,
+                                               String feederId, String subsId, String countyId,
+                                               String mvtransId, String consumerId,
+                                               BigDecimal ratio, int status, LocalDateTime snapTime) {
+        FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
+                new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                        .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
+                        .eq(FhzgSeMonitorVoltage::getDeviceType, deviceType)
+                        .last("LIMIT 1"));
+        boolean isOverLimit = status != 0;
+
+        if (state == null) {
+            state = FhzgSeMonitorVoltage.builder()
+                    .deviceId(deviceId).deviceType(deviceType)
+                    .feederId(feederId).subsId(subsId).countyId(countyId)
+                    .mvtransId(mvtransId).consumerId(consumerId)
+                    .lastVoltageRatio(ratio).lastVoltageStatus(status)
+                    .lastSnapTime(snapTime).alarmTriggered(0).build();
+            if (isOverLimit) { state.setFirstOverTime(snapTime); }
+            monitorVoltageMapper.insert(state);
+        } else {
+            LocalDateTime lastSnap = state.getLastSnapTime();
+            boolean intervalExceeded = lastSnap != null
+                    && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES;
+            if (intervalExceeded) {
+                state.setFirstOverTime(null);
+                state.setAlarmTriggered(0);
+                state.setAlarmEventId(null);
+            }
+            if (isOverLimit) {
+                if (state.getFirstOverTime() == null) state.setFirstOverTime(snapTime);
+                if (state.getFirstOverTime() != null
+                        && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
+                        && state.getAlarmTriggered() == 0) {
+
+                    String alarmType = resolveConsumerAlarmType(status);
+                    BigDecimal threshold = resolveConsumerThreshold(status);
+
+                    FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                            .deviceId(deviceId).deviceType("consumer").alarmLevel("consumer").alarmType(alarmType)
+                            .feederId(feederId).subsId(subsId).countyId(countyId)
+                            .mvtransId(mvtransId).consumerId(consumerId)
+                            .threshold(threshold)
+                            .firstOverTime(state.getFirstOverTime())
+                            .triggerTime(snapTime)
+                            .triggerValue(ratio).lastValue(ratio)
+                            .status(1).build();
+                    voltageEventMapper.insert(event);
+                    state.setAlarmTriggered(1);
+                    state.setAlarmEventId(event.getId());
+                }
+            } else {
+                if (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+                    update.setId(state.getAlarmEventId());
+                    update.setStatus(2);
+                    update.setRecoverTime(snapTime);
+                    update.setLastValue(ratio);
+                    voltageEventMapper.updateById(update);
+                }
+                state.setFirstOverTime(null);
+                state.setAlarmTriggered(0);
+                state.setAlarmEventId(null);
+            }
+            state.setLastVoltageRatio(ratio);
+            state.setLastVoltageStatus(status);
+            state.setLastSnapTime(snapTime);
+            monitorVoltageMapper.updateById(state);
+        }
+    }
+
+    private int calcConsumerVoltageStatus(BigDecimal ratio, String voltageLevel) {
+        if (ratio == null) 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;
+        if (ratio.compareTo(SeMonitorThreshold.CONSUMER_GENERAL_V_UNDER) < 0) return 3;
+        return 0;
+    }
+
+    private String resolveConsumerAlarmType(int status) {
+        switch (status) {
+            case 1: return "voltage_over_general";
+            case 2: return "voltage_over_severe";
+            case 3: return "voltage_under_general";
+            case 4: return "voltage_under_severe";
+            default: return "voltage_over";
+        }
+    }
+
+    private BigDecimal resolveConsumerThreshold(int status) {
+        switch (status) {
+            case 1: return SeMonitorThreshold.CONSUMER_GENERAL_V_OVER;
+            case 2: return SeMonitorThreshold.CONSUMER_SEVERE_V_OVER;
+            case 3: return SeMonitorThreshold.CONSUMER_GENERAL_V_UNDER;
+            case 4: return SeMonitorThreshold.CONSUMER_SEVERE_V_UNDER;
+            default: return null;
+        }
+    }
+}

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

@@ -0,0 +1,491 @@
+package com.hdkj.lt.bf.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.hdkj.lt.bf.application.FeederGroupApplication;
+import com.hdkj.lt.bf.entity.FhzgDwmVoltageOverLimitDaily;
+import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
+import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
+import com.hdkj.lt.bf.entity.dto.TodayOutageData;
+import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
+import com.hdkj.lt.bf.mapper.FhzgDwmVoltageOverLimitDailyMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
+import com.hdkj.lt.bf.service.SeIndicatorService;
+import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
+import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 状估监测体系 三个查询接口 Service 实现
+ *
+ * @author lsl
+ * @since 2026-07-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SeIndicatorServiceImpl implements SeIndicatorService {
+
+    private final SjztPwwyyqxtPrmPdrOutageMvDsService outageService;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final FhzgDwmVoltageOverLimitDailyMapper voltageDailyMapper;
+    private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
+    private final FhzgSeCapacityDailyMapper capacityDailyMapper;
+    private final FeederGroupApplication feederGroupApplication;
+
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+    // ============================================================
+    // 1. 停电时户数(复用已有逻辑,无时间维度)
+    // ============================================================
+
+    @Override
+    public SeOutageDashboardVO queryOutage(String id, Integer type) {
+        if (StringUtils.isBlank(id) || type == null) {
+            return null;
+        }
+
+        // 今日 — 实时表
+        TodayOutageData today = outageService.getTodayOutageData(id, type);
+
+        // 全年(1.1 ~ 昨天)— T-2表
+        LocalDate todayDate = LocalDate.now();
+        int year = todayDate.getYear();
+        LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
+        LocalDateTime yesterdayEnd = todayDate.minusDays(1).atTime(23, 59, 59);
+        TodayOutageData yearStats = outageService.getOutageDurationHoursHouseholds(id, type, yearStart, yesterdayEnd);
+
+        return SeOutageDashboardVO.builder()
+                .outageHoursHouseholdsToday(today.getTotalHouseholds())
+                .avgOutageDurationToday(today.getAvgDurationHours())
+                .outageHoursHouseholdsYear(yearStats.getTotalHouseholds())
+                .avgOutageDurationYear(yearStats.getAvgDurationHours())
+                .build();
+    }
+
+    // ============================================================
+    // 2. 电压质量
+    // ============================================================
+
+    @Override
+    public SeVoltageDashboardVO queryVoltage(String id, Integer type, String timeScope) {
+        if (StringUtils.isBlank(id) || type == null || StringUtils.isBlank(timeScope)) {
+            return null;
+        }
+
+        if ("today".equalsIgnoreCase(timeScope)) {
+            return queryVoltageToday(id, type);
+        } else {
+            return queryVoltageHistory(id, type, timeScope);
+        }
+    }
+
+    /**
+     * 今日电压:COUNT 聚合到 SQL,按 alarmLevel + alarmType 分组
+     * 返回4个计数:台区越上限/下限 + 馈线越上限/下限
+     */
+    private SeVoltageDashboardVO queryVoltageToday(String id, Integer type) {
+        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+        LocalDateTime now = LocalDateTime.now();
+
+        int overUpperArea = countVoltageEvents(id, type, todayStart, now, "mvtrans", "over");
+        int overLowerArea = countVoltageEvents(id, type, todayStart, now, "mvtrans", "under");
+        int overUpperFeeder = countVoltageEvents(id, type, todayStart, now, "feeder", "over");
+        int overLowerFeeder = countVoltageEvents(id, type, todayStart, now, "feeder", "under");
+
+        return SeVoltageDashboardVO.builder()
+                .todayOverUpperAreaCount(overUpperArea)
+                .todayOverLowerAreaCount(overLowerArea)
+                .todayOverUpperFeederCount(overUpperFeeder)
+                .todayOverLowerFeederCount(overLowerFeeder)
+                .build();
+    }
+
+    /**
+     * 按节点+时段+类型统计电压越限事件数
+     */
+    private int countVoltageEvents(String id, Integer type,
+                                    LocalDateTime start, LocalDateTime end,
+                                    String alarmLevel, String overOrUnder) {
+        LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                .ge(FhzgSeVoltageEvent::getTriggerTime, start)
+                .le(FhzgSeVoltageEvent::getTriggerTime, end)
+                .eq(FhzgSeVoltageEvent::getStatus, 1)
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, alarmLevel)
+                .like(FhzgSeVoltageEvent::getAlarmType, overOrUnder);
+        applyNodeFilter(wrapper, id, type);
+        Long count = voltageEventMapper.selectCount(wrapper);
+        return count != null ? count.intValue() : 0;
+    }
+
+    /**
+     * 历史电压:查 ads_mrt_voltage_over_limit_daily_result,SUM 聚合到 SQL
+     * 当月 = 月初~昨天;全年 = 1.1~昨天
+     * 17:00前查历史当月/全年 = 中台[到前天] + 我们alarm_event[昨天];17:00后 = 中台[到昨天]
+     */
+    private SeVoltageDashboardVO queryVoltageHistory(String id, Integer type, String timeScope) {
+        LocalDate today = LocalDate.now();
+        LocalDate endDate;
+        LocalDate startDate;
+
+        if ("month".equalsIgnoreCase(timeScope)) {
+            startDate = today.withDayOfMonth(1);
+        } else {
+            startDate = LocalDate.of(today.getYear(), 1, 1);
+        }
+
+        // 17:00前:中台到前天 + 我们昨天;17:00后:中台到昨天
+        int hour = LocalDateTime.now().getHour();
+        if (hour < 17) {
+            endDate = today.minusDays(2);
+        } else {
+            endDate = today.minusDays(1);
+        }
+
+        // 如果 endDate < startDate,返回空
+        if (endDate.isBefore(startDate)) {
+            return SeVoltageDashboardVO.builder().build();
+        }
+
+        String dimLevel = resolveDimLevel(type);
+
+        // 一条 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")
+                .eq("dim_level", dimLevel)
+                .ge("over_date", startDate.format(DATE_FMT))
+                .le("over_date", endDate.format(DATE_FMT));
+
+        // 维度过滤
+        if (type == 2) {
+            qw.eq("maintainer_id", id);
+        } else if (type == 3) {
+            qw.eq("substation_id", id);
+        } else if (type == 4) {
+            qw.eq("feeder_id", id);
+        }
+
+        List<FhzgDwmVoltageOverLimitDaily> records = voltageDailyMapper.selectList(qw);
+        FhzgDwmVoltageOverLimitDaily r = (records != null && !records.isEmpty()) ? records.get(0) : null;
+
+        int overUpperSevereTg = r != null ? safeInt(r.getOverUpperSevereTgCount()) : 0;
+        int overUpperSevereCust = r != null ? safeInt(r.getOverUpperSevereCustCount()) : 0;
+        int overUpperSevereFeeder = r != null ? safeInt(r.getOverUpperSevereFeederCount()) : 0;
+        int overUpperSevereGen = r != null ? safeInt(r.getOverUpperSevereGeneratrixCount()) : 0;
+        int overUpperMildTg = r != null ? safeInt(r.getOverUpperMildTgCount()) : 0;
+        int overUpperMildCust = r != null ? safeInt(r.getOverUpperMildCustCount()) : 0;
+        int overUpperMildFeeder = r != null ? safeInt(r.getOverUpperMildFeederCount()) : 0;
+        int overUpperMildGen = r != null ? safeInt(r.getOverUpperMildGeneratrixCount()) : 0;
+        int overLowerSevereTg = r != null ? safeInt(r.getOverLowerSevereTgCount()) : 0;
+        int overLowerSevereCust = r != null ? safeInt(r.getOverLowerSevereCustCount()) : 0;
+        int overLowerSevereFeeder = r != null ? safeInt(r.getOverLowerSevereFeederCount()) : 0;
+        int overLowerSevereGen = r != null ? safeInt(r.getOverLowerSevereGeneratrixCount()) : 0;
+        int overLowerMildTg = r != null ? safeInt(r.getOverLowerMildTgCount()) : 0;
+        int overLowerMildCust = r != null ? safeInt(r.getOverLowerMildCustCount()) : 0;
+        int overLowerMildFeeder = r != null ? safeInt(r.getOverLowerMildFeederCount()) : 0;
+        int overLowerMildGen = r != null ? safeInt(r.getOverLowerMildGeneratrixCount()) : 0;
+        BigDecimal totalHours = r != null && r.getTotalOverLimitHours() != null
+                ? r.getTotalOverLimitHours() : BigDecimal.ZERO;
+
+        // 17:00前补昨日数据(从我们alarm_event查昨天)
+        if (hour < 17) {
+            LocalDateTime yesterdayStart = today.minusDays(1).atStartOfDay();
+            LocalDateTime yesterdayEnd = today.minusDays(1).atTime(23, 59, 59);
+            LambdaQueryWrapper<FhzgSeVoltageEvent> yesterdayWrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                    .ge(FhzgSeVoltageEvent::getTriggerTime, yesterdayStart)
+                    .le(FhzgSeVoltageEvent::getTriggerTime, yesterdayEnd)
+                    .eq(FhzgSeVoltageEvent::getStatus, 2);
+            applyNodeFilter(yesterdayWrapper, id, type);
+            List<FhzgSeVoltageEvent> yesterdayEvents = voltageEventMapper.selectList(yesterdayWrapper);
+            for (FhzgSeVoltageEvent e : yesterdayEvents) {
+                if ("voltage_over".equals(e.getAlarmType()) || e.getAlarmType() != null && e.getAlarmType().contains("over")) {
+                    overUpperMildTg++;
+                } else if (e.getAlarmType() != null && e.getAlarmType().contains("under")) {
+                    overLowerMildTg++;
+                }
+            }
+        }
+
+        return SeVoltageDashboardVO.builder()
+                .overUpperSevereTgCount(overUpperSevereTg)
+                .overUpperSevereCustCount(overUpperSevereCust)
+                .overUpperSevereFeederCount(overUpperSevereFeeder)
+                .overUpperSevereGeneratrixCount(overUpperSevereGen)
+                .overUpperMildTgCount(overUpperMildTg)
+                .overUpperMildCustCount(overUpperMildCust)
+                .overUpperMildFeederCount(overUpperMildFeeder)
+                .overUpperMildGeneratrixCount(overUpperMildGen)
+                .overLowerSevereTgCount(overLowerSevereTg)
+                .overLowerSevereCustCount(overLowerSevereCust)
+                .overLowerSevereFeederCount(overLowerSevereFeeder)
+                .overLowerSevereGeneratrixCount(overLowerSevereGen)
+                .overLowerMildTgCount(overLowerMildTg)
+                .overLowerMildCustCount(overLowerMildCust)
+                .overLowerMildFeederCount(overLowerMildFeeder)
+                .overLowerMildGeneratrixCount(overLowerMildGen)
+                .totalOverLimitHours(totalHours)
+                .build();
+    }
+
+    // ============================================================
+    // 3. 供电能力
+    // ============================================================
+
+    @Override
+    public SeCapacityDashboardVO queryCapacity(String id, Integer type, String timeScope) {
+        if (StringUtils.isBlank(id) || type == null || StringUtils.isBlank(timeScope)) {
+            return null;
+        }
+
+        // 今日 → 实时查断面明细(量小)
+        if ("today".equalsIgnoreCase(timeScope)) {
+            return queryCapacityToday(id, type);
+        }
+
+        // month/year → 查日汇总表
+        return queryCapacityHistory(id, type, timeScope);
+    }
+
+    /**
+     * 今日供电能力:实时查 snapshot_detail(量小,一个馈线一天最多96行)
+     * 馈线级别附加96点时序(负载率+电流+电压+有功+无功)
+     */
+    private SeCapacityDashboardVO queryCapacityToday(String id, Integer type) {
+        LocalDate today = LocalDate.now();
+        LocalDateTime startTime = today.atStartOfDay();
+        LocalDateTime endTime = LocalDateTime.now();
+
+        LambdaQueryWrapper<FhzgSeSnapshotDetail> wrapper = new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                .isNotNull(FhzgSeSnapshotDetail::getLoadRate)
+                .ge(FhzgSeSnapshotDetail::getSnapshotTime, startTime)
+                .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime);
+
+        applySnapshotFilter(wrapper, id, type);
+
+        List<FhzgSeSnapshotDetail> snapshots = snapshotDetailMapper.selectList(wrapper);
+
+        if (snapshots.isEmpty()) {
+            return SeCapacityDashboardVO.builder().build();
+        }
+
+        SeCapacityDashboardVO vo = aggregateSnapshots(snapshots);
+
+        // 馈线 → 附加96点时序
+        if (type == 4) {
+            vo.setLoadRateSeries(buildFeederTimeSeries(snapshots));
+        }
+        return vo;
+    }
+
+    /**
+     * 历史供电能力:查 capacity_daily 日汇总表(每馈线每天一行,量极小)
+     * month = 月初~昨天;year = 1.1~昨天
+     */
+    private SeCapacityDashboardVO queryCapacityHistory(String id, Integer type, String timeScope) {
+        LocalDate today = LocalDate.now();
+        LocalDate startDate;
+        LocalDate endDate;
+
+        if ("month".equalsIgnoreCase(timeScope)) {
+            startDate = today.withDayOfMonth(1);
+        } else {
+            startDate = LocalDate.of(today.getYear(), 1, 1);
+        }
+        endDate = today.minusDays(1);
+
+        if (endDate.isBefore(startDate)) {
+            return SeCapacityDashboardVO.builder().build();
+        }
+
+        LambdaQueryWrapper<FhzgSeCapacityDaily> wrapper = new LambdaQueryWrapper<FhzgSeCapacityDaily>()
+                .ge(FhzgSeCapacityDaily::getStatDate, startDate)
+                .le(FhzgSeCapacityDaily::getStatDate, endDate);
+
+        applyCapacityDailyFilter(wrapper, id, type);
+
+        List<FhzgSeCapacityDaily> dailyRecords = capacityDailyMapper.selectList(wrapper);
+
+        if (dailyRecords.isEmpty()) {
+            return SeCapacityDashboardVO.builder().build();
+        }
+
+        // 聚合日汇总
+        BigDecimal maxLoadRate = BigDecimal.ZERO;
+        BigDecimal sumAvg = BigDecimal.ZERO;
+        int totalHeavy = 0, totalOverload = 0, totalSnapshots = 0;
+        for (FhzgSeCapacityDaily d : dailyRecords) {
+            if (d.getMaxLoadRate() != null && d.getMaxLoadRate().compareTo(maxLoadRate) > 0) {
+                maxLoadRate = d.getMaxLoadRate();
+            }
+            if (d.getAvgLoadRate() != null) {
+                sumAvg = sumAvg.add(d.getAvgLoadRate());
+            }
+            totalHeavy += d.getHeavyCount() != null ? d.getHeavyCount() : 0;
+            totalOverload += d.getOverloadCount() != null ? d.getOverloadCount() : 0;
+            totalSnapshots += d.getTotalSnapshots() != null ? d.getTotalSnapshots() : 0;
+        }
+
+        int totalEvents = totalHeavy + totalOverload;
+        int recordCount = dailyRecords.size();
+        BigDecimal avgLoadRate = recordCount > 0
+                ? sumAvg.divide(BigDecimal.valueOf(recordCount), 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+        BigDecimal heavyRate = totalSnapshots > 0
+                ? BigDecimal.valueOf(totalHeavy * 100).divide(BigDecimal.valueOf(totalSnapshots), 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+        BigDecimal overloadRate = totalSnapshots > 0
+                ? BigDecimal.valueOf(totalOverload * 100).divide(BigDecimal.valueOf(totalSnapshots), 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+
+        return SeCapacityDashboardVO.builder()
+                .maxLoadRate(maxLoadRate)
+                .avgLoadRate(avgLoadRate)
+                .heavyLoadEventCount(totalHeavy)
+                .overloadEventCount(totalOverload)
+                .heavyLoadRate(heavyRate)
+                .overloadRate(overloadRate)
+                .totalEventCount(totalEvents)
+                .build();
+    }
+
+    /**
+     * 内存聚合断面列表 → VO(today用)
+     */
+    private SeCapacityDashboardVO aggregateSnapshots(List<FhzgSeSnapshotDetail> snapshots) {
+        BigDecimal maxLoadRate = snapshots.stream()
+                .map(FhzgSeSnapshotDetail::getLoadRate)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal::max)
+                .orElse(BigDecimal.ZERO);
+
+        BigDecimal avgLoadRate = snapshots.stream()
+                .map(FhzgSeSnapshotDetail::getLoadRate)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add)
+                .divide(BigDecimal.valueOf(snapshots.size()), 2, RoundingMode.HALF_UP);
+
+        int heavyCount = 0, overloadCount = 0;
+        for (FhzgSeSnapshotDetail s : snapshots) {
+            Integer status = s.getCurrentStatus();
+            if (status != null) {
+                if (status == 1) heavyCount++;
+                if (status == 2) overloadCount++;
+            }
+        }
+        int totalEvents = heavyCount + overloadCount;
+        int totalSnapshots = snapshots.size();
+        BigDecimal heavyRate = totalSnapshots > 0
+                ? BigDecimal.valueOf(heavyCount * 100).divide(BigDecimal.valueOf(totalSnapshots), 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+        BigDecimal overloadRate = totalSnapshots > 0
+                ? BigDecimal.valueOf(overloadCount * 100).divide(BigDecimal.valueOf(totalSnapshots), 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+
+        return SeCapacityDashboardVO.builder()
+                .maxLoadRate(maxLoadRate)
+                .avgLoadRate(avgLoadRate)
+                .heavyLoadEventCount(heavyCount)
+                .overloadEventCount(overloadCount)
+                .heavyLoadRate(heavyRate)
+                .overloadRate(overloadRate)
+                .totalEventCount(totalEvents)
+                .build();
+    }
+
+    /**
+     * 构建馈线今日96点时序(负载率+电流+电压+有功+无功)
+     */
+    private List<SeCapacityDashboardVO.TimeSeriesPoint> buildFeederTimeSeries(List<FhzgSeSnapshotDetail> snapshots) {
+        DateTimeFormatter hmFmt = DateTimeFormatter.ofPattern("HH:mm");
+        return snapshots.stream()
+                .sorted(Comparator.comparing(FhzgSeSnapshotDetail::getSnapshotTime))
+                .map(s -> SeCapacityDashboardVO.TimeSeriesPoint.builder()
+                        .time(s.getSnapshotTime().format(hmFmt))
+                        .loadRate(s.getLoadRate())
+                        .currentValue(s.getCurrentValue())
+                        .voltageValue(s.getVoltageValue())
+                        .activePower(s.getActivePower())
+                        .reactivePower(s.getReactivePower())
+                        .build())
+                .collect(Collectors.toList());
+    }
+
+    // ============================================================
+    // 辅助方法
+    // ============================================================
+
+    private void applyNodeFilter(LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper, String id, Integer type) {
+        if (type == 2) {
+            wrapper.eq(FhzgSeVoltageEvent::getCountyId, id);
+        } else if (type == 3) {
+            wrapper.eq(FhzgSeVoltageEvent::getSubsId, id);
+        } else if (type == 4) {
+            wrapper.eq(FhzgSeVoltageEvent::getFeederId, id);
+        }
+    }
+
+    private void applySnapshotFilter(LambdaQueryWrapper<FhzgSeSnapshotDetail> wrapper, String id, Integer type) {
+        if (type == 2) {
+            wrapper.eq(FhzgSeSnapshotDetail::getCountyId, id);
+        } else if (type == 3) {
+            wrapper.eq(FhzgSeSnapshotDetail::getSubsId, id);
+        } else if (type == 4) {
+            wrapper.eq(FhzgSeSnapshotDetail::getFeederId, id);
+        }
+    }
+
+    private void applyCapacityDailyFilter(LambdaQueryWrapper<FhzgSeCapacityDaily> wrapper, String id, Integer type) {
+        if (type == 2) {
+            wrapper.eq(FhzgSeCapacityDaily::getCountyId, id);
+        } else if (type == 3) {
+            wrapper.eq(FhzgSeCapacityDaily::getSubsId, id);
+        } else if (type == 4) {
+            wrapper.eq(FhzgSeCapacityDaily::getFeederId, id);
+        }
+    }
+
+    private String resolveDimLevel(Integer type) {
+        if (type == 2) return "county";
+        if (type == 3) return "substation";
+        if (type == 4) return "feeder";
+        return "county";
+    }
+
+    private int safeInt(Integer v) {
+        return v == null ? 0 : v;
+    }
+}

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

@@ -0,0 +1,510 @@
+package com.hdkj.lt.bf.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.hdkj.lt.bf.application.FeederGroupApplication;
+import com.hdkj.lt.bf.common.SeMonitorThreshold;
+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.event.MvtransOverVoltageEvent;
+import com.hdkj.lt.bf.event.ReconTriggerEvent;
+import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
+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.service.SeSnapshotService;
+import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SeSnapshotServiceImpl implements SeSnapshotService {
+
+    private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
+    private final FhzgSeMonitorCurrentMapper monitorCurrentMapper;
+    private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
+    private final FhzgSeCurrentEventMapper currentEventMapper;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final FeederGroupApplication feederGroupApplication;
+    private final ApplicationEventPublisher eventPublisher;
+
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public void processSnapshots(String snapshotJsonArray) {
+        JSONArray arr = JSON.parseArray(snapshotJsonArray);
+        if (arr == null || arr.isEmpty()) return;
+        for (int i = 0; i < arr.size(); i++) {
+            processSnapshot(arr.getJSONObject(i).toJSONString());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void processSnapshot(String snapshotJson) {
+        JSONObject root = JSON.parseObject(snapshotJson);
+        if (root == null) return;
+
+        JSONArray pointTimes = root.getJSONArray("pointTime");
+        if (pointTimes == null || pointTimes.isEmpty()) return;
+        LocalDateTime snapTime = LocalDateTime.parse(pointTimes.getString(0), DT_FMT);
+
+        Map<String, String[]> feederMap = buildFeederMapping();
+        List<FhzgSeSnapshotDetail> batch = new ArrayList<>();
+
+        // === 1. 馈线 ===
+        JSONArray feeders = root.getJSONArray("periodFeederSeResult");
+        if (feeders != null) {
+            for (int i = 0; i < feeders.size(); i++) {
+                JSONObject f = feeders.getJSONObject(i);
+                FhzgSeSnapshotDetail d = parseFeeder(f, snapTime, feederMap);
+                if (d != null) {
+                    batch.add(d);
+                    processFeederCurrent(f, d, snapTime, feederMap);
+                    processFeederVoltage(f, d, snapTime, feederMap);
+                }
+            }
+        }
+
+        // === 2. 配变 ===
+        JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
+        if (mvtrans != null) {
+            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);
+                }
+            }
+        }
+
+        if (!batch.isEmpty()) {
+            // 防重:过滤掉该断面时刻已存在的 device_id
+            Set<String> existingIds = new HashSet<>();
+            snapshotDetailMapper.selectList(
+                    new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                            .select(FhzgSeSnapshotDetail::getDeviceId)
+                            .eq(FhzgSeSnapshotDetail::getSnapshotTime, snapTime)
+                            .in(FhzgSeSnapshotDetail::getDeviceId,
+                                    batch.stream().map(FhzgSeSnapshotDetail::getDeviceId).collect(java.util.stream.Collectors.toList()))
+            ).forEach(e -> existingIds.add(e.getDeviceId()));
+
+            int inserted = 0;
+            for (FhzgSeSnapshotDetail d : batch) {
+                if (existingIds.contains(d.getDeviceId())) continue;
+                snapshotDetailMapper.insert(d);
+                inserted++;
+            }
+            log.info("状估断面处理完成 snapTime={}, 写入{}条(去重跳过{}条)",
+                    snapTime, inserted, batch.size() - inserted);
+        }
+    }
+
+    // ============================================================
+    // 馈线
+    // ============================================================
+
+    private FhzgSeSnapshotDetail parseFeeder(JSONObject f, LocalDateTime snapTime, Map<String, String[]> feederMap) {
+        String feederId = f.getString("psrId");
+        String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
+        BigDecimal loadRate = firstValue(f.getJSONArray("loadRates"));
+        BigDecimal voltage = firstValue(f.getJSONArray("seoUs"));
+        BigDecimal voltageRatio = voltage != null
+                ? voltage.divide(SeMonitorThreshold.FEEDER_BASE_KV, 4, RoundingMode.HALF_UP) : null;
+        return FhzgSeSnapshotDetail.builder()
+                .snapshotTime(snapTime).deviceId(feederId).deviceType("feeder")
+                .feederId(feederId).feederName(loc.length > 2 ? loc[2] : null)
+                .subsId(loc[0]).countyId(loc[1])
+                .activePower(firstValue(f.getJSONArray("seoPs")))
+                .reactivePower(firstValue(f.getJSONArray("seoQs")))
+                .currentValue(firstValue(f.getJSONArray("seoIs")))
+                .voltageValue(voltage).loadRate(loadRate).voltageRatio(voltageRatio)
+                .currentStatus(calcCurrentStatus(loadRate))
+                .voltageStatus(calcFeederVoltageStatus(voltageRatio))
+                .build();
+    }
+
+    private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
+        if (detail.getCurrentStatus() == null) return;
+        String feederId = detail.getFeederId();
+        String[] loc = feederMap.get(feederId);
+        String feederName = loc != null && loc.length > 2 ? loc[2] : null;
+        String countyId = loc != null ? loc[1] : "";
+        String subsId = loc != null ? loc[0] : "";
+        BigDecimal loadRate = detail.getLoadRate();
+        int status = detail.getCurrentStatus();
+        boolean isOverLimit = status != 0;
+
+        FhzgSeMonitorCurrent state = monitorCurrentMapper.selectOne(
+                new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
+                        .eq(FhzgSeMonitorCurrent::getFeederId, feederId).last("LIMIT 1"));
+
+        if (state == null) {
+            state = FhzgSeMonitorCurrent.builder()
+                    .feederId(feederId).feederName(feederName)
+                    .lastLoadRate(loadRate).lastStatus(status).lastSnapTime(snapTime)
+                    .alarmTriggered(0).build();
+            if (isOverLimit) state.setFirstOverTime(snapTime);
+            monitorCurrentMapper.insert(state);
+        } else {
+            LocalDateTime lastSnap = state.getLastSnapTime();
+            if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
+                state.setFirstOverTime(null);
+                state.setAlarmTriggered(0);
+                state.setAlarmEventId(null);
+            }
+            BigDecimal maxLoadRate = state.getLastLoadRate();
+            if (loadRate != null && (maxLoadRate == null || loadRate.compareTo(maxLoadRate) > 0))
+                maxLoadRate = loadRate;
+
+            if (isOverLimit) {
+                if (state.getFirstOverTime() == null) state.setFirstOverTime(snapTime);
+                if (state.getFirstOverTime() != null
+                        && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
+                        && state.getAlarmTriggered() == 0) {
+                    // 触发时往回看1小时窗口:有任意断面≥100%算过载,否则重载
+                    boolean hasOverload = hasOverloadSnapshotInWindow(feederId, state.getFirstOverTime(), snapTime);
+                    String alarmType = hasOverload ? "current_overload" : "current_heavy";
+                    String countyName = lookupCountyName(countyId, feederMap);
+                    FhzgSeCurrentEvent event = FhzgSeCurrentEvent.builder()
+                            .feederId(feederId).feederName(feederName).alarmType(alarmType)
+                            .countyId(countyId).countyName(countyName).subsId(subsId)
+                            .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                            .triggerLoadRate(loadRate).maxLoadRate(maxLoadRate).lastLoadRate(loadRate).status(1)
+                            .build();
+                    currentEventMapper.insert(event);
+                    state.setAlarmTriggered(1);
+                    state.setAlarmEventId(event.getId());
+                    // 异步触发重构算法(需要 feeder_id + point_time)
+                    eventPublisher.publishEvent(new ReconTriggerEvent(
+                            event.getId(), feederId, feederName, countyId, subsId,
+                            alarmType, snapTime, state.getFirstOverTime()));
+                }
+            } 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);
+                }
+                state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
+            }
+            state.setLastLoadRate(loadRate); state.setLastStatus(status); state.setLastSnapTime(snapTime);
+            monitorCurrentMapper.updateById(state);
+        }
+    }
+
+    private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
+                                       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,
+                loc != null ? loc[0] : "", loc != null ? loc[1] : "", null, null);
+    }
+
+    // ============================================================
+    // 配变
+    // ============================================================
+
+    private FhzgSeSnapshotDetail parseMvtrans(JSONObject m, LocalDateTime snapTime, Map<String, String[]> feederMap) {
+        String mvtransId = m.getString("psrId");
+        String feederId = m.getString("feederId");
+        String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
+        BigDecimal lvVoltageA = firstValue(m.getJSONArray("seiLvUas"));
+        BigDecimal voltageRatio = lvVoltageA != null
+                ? lvVoltageA.divide(SeMonitorThreshold.MVTRANS_BASE_KV, 4, RoundingMode.HALF_UP) : null;
+        BigDecimal capacity = null;
+        String capStr = m.getString("capacitys");
+        if (capStr != null) { try { capacity = new BigDecimal(capStr); } catch (NumberFormatException ignored) {} }
+        return FhzgSeSnapshotDetail.builder()
+                .snapshotTime(snapTime).deviceId(mvtransId).deviceType("mvtrans")
+                .feederId(feederId).subsId(loc[0]).countyId(loc[1])
+                .lvVoltageA(lvVoltageA).capacity(capacity).voltageRatio(voltageRatio)
+                .voltageStatus(calcMvtransVoltageStatus(voltageRatio))
+                .build();
+    }
+
+    private void processMvtransMonitor(JSONObject m, FhzgSeSnapshotDetail detail,
+                                        LocalDateTime snapTime, Map<String, String[]> feederMap) {
+        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,
+                snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
+                loc != null ? loc[0] : "", loc != null ? loc[1] : "", mvtransId, null);
+
+        if (status != 0) {
+            eventPublisher.publishEvent(new MvtransOverVoltageEvent(
+                    this, mvtransId, detail.getFeederId(),
+                    loc != null ? loc[1] : "", loc != null ? loc[0] : "",
+                    snapTime, detail.getVoltageRatio()));
+        }
+    }
+
+    // ============================================================
+    // 电压状态机
+    // ============================================================
+
+    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) {
+        FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
+                new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                        .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
+                        .eq(FhzgSeMonitorVoltage::getDeviceType, deviceType).last("LIMIT 1"));
+        boolean isOverLimit = voltageStatus != 0;
+
+        if (state == null) {
+            state = FhzgSeMonitorVoltage.builder()
+                    .deviceId(deviceId).deviceType(deviceType)
+                    .feederId(feederId).feederName(feederName)
+                    .subsId(subsId).countyId(countyId)
+                    .mvtransId(mvtransId).consumerId(consumerId)
+                    .lastVoltageRatio(voltageRatio).lastVoltageStatus(voltageStatus)
+                    .lastSnapTime(snapTime).alarmTriggered(0).build();
+            if (isOverLimit) state.setFirstOverTime(snapTime);
+            monitorVoltageMapper.insert(state);
+        } else {
+            LocalDateTime lastSnap = state.getLastSnapTime();
+            if (lastSnap != null && Duration.between(lastSnap, snapTime).toMinutes() > SeMonitorThreshold.SNAP_INTERVAL_MINUTES) {
+                state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
+            }
+            if (isOverLimit) {
+                if (state.getFirstOverTime() == null) state.setFirstOverTime(snapTime);
+                if (state.getFirstOverTime() != null
+                        && Duration.between(state.getFirstOverTime(), snapTime).toMinutes() >= SeMonitorThreshold.TRIGGER_DURATION_MINUTES
+                        && state.getAlarmTriggered() == 0) {
+                    String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                    String countyName = lookupCountyName(countyId, null);
+                    BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                    BigDecimal rangeMax = null, rangeMin = null;
+                    int overUpper = 0, overLower = 0;
+                    if ("feeder".equals(deviceType) && feederId != null) {
+                        BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
+                        rangeMax = vr[0]; rangeMin = vr[1];
+                        int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
+                        overUpper = mc[0]; overLower = mc[1];
+                    }
+                    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).consumerId(consumerId).threshold(threshold)
+                            .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                            .triggerValue(voltageRatio)
+                            .voltageRangeMax(rangeMax).voltageRangeMin(rangeMin)
+                            .overUpperMvtrans(overUpper).overLowerMvtrans(overLower)
+                            .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 (state.getAlarmTriggered() == 1 && state.getAlarmEventId() != null) {
+                    FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+                    update.setId(state.getAlarmEventId());
+                    update.setStatus(2); update.setRecoverTime(snapTime); update.setLastValue(voltageRatio);
+                    voltageEventMapper.updateById(update);
+                }
+                state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
+            }
+            state.setLastVoltageRatio(voltageRatio); state.setLastVoltageStatus(voltageStatus);
+            state.setLastSnapTime(snapTime);
+            monitorVoltageMapper.updateById(state);
+        }
+    }
+
+    // ============================================================
+    // 配变→用户恢复
+    // ============================================================
+
+    private void recoverUserAlarms(String mvtransId, LocalDateTime snapTime) {
+        FhzgSeVoltageEvent updateEvent = new FhzgSeVoltageEvent();
+        updateEvent.setStatus(2); updateEvent.setRecoverTime(snapTime);
+        voltageEventMapper.update(updateEvent,
+                new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                        .eq(FhzgSeVoltageEvent::getMvtransId, mvtransId)
+                        .eq(FhzgSeVoltageEvent::getAlarmLevel, "consumer")
+                        .eq(FhzgSeVoltageEvent::getStatus, 1));
+        FhzgSeMonitorVoltage reset = new FhzgSeMonitorVoltage();
+        reset.setFirstOverTime(null); reset.setAlarmTriggered(0);
+        reset.setAlarmEventId(null); reset.setLastVoltageStatus(0);
+        monitorVoltageMapper.update(reset,
+                new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                        .eq(FhzgSeMonitorVoltage::getMvtransId, mvtransId)
+                        .eq(FhzgSeMonitorVoltage::getDeviceType, "consumer"));
+    }
+
+    // ============================================================
+    // 查询辅助
+    // ============================================================
+
+    private BigDecimal[] queryVoltageRangeFromSnap(String feederId, LocalDateTime start, LocalDateTime end) {
+        List<FhzgSeSnapshotDetail> list = snapshotDetailMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getFeederId, feederId)
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                        .isNotNull(FhzgSeSnapshotDetail::getVoltageRatio)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, start)
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, end));
+        BigDecimal maxV = null, minV = null;
+        for (FhzgSeSnapshotDetail s : list) {
+            BigDecimal v = s.getVoltageRatio();
+            if (v == null) continue;
+            if (maxV == null || v.compareTo(maxV) > 0) maxV = v;
+            if (minV == null || v.compareTo(minV) < 0) minV = v;
+        }
+        return new BigDecimal[]{maxV, minV};
+    }
+
+    private int[] queryMvtransCounts(String feederId, LocalDateTime start, LocalDateTime end) {
+        List<FhzgSeVoltageEvent> list = voltageEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                        .eq(FhzgSeVoltageEvent::getFeederId, feederId)
+                        .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
+                        .eq(FhzgSeVoltageEvent::getStatus, 1)
+                        .ge(FhzgSeVoltageEvent::getFirstOverTime, start)
+                        .le(FhzgSeVoltageEvent::getFirstOverTime, end));
+        int up = 0, down = 0;
+        for (FhzgSeVoltageEvent e : list) {
+            if (e.getAlarmType() != null && e.getAlarmType().contains("over")) up++;
+            else if (e.getAlarmType() != null && e.getAlarmType().contains("under")) down++;
+        }
+        return new int[]{up, down};
+    }
+
+    /**
+     * 查窗口内是否有任意过载断面(≥100%)
+     */
+    private boolean hasOverloadSnapshotInWindow(String feederId, LocalDateTime start, LocalDateTime end) {
+        Long count = snapshotDetailMapper.selectCount(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getFeederId, feederId)
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                        .ge(FhzgSeSnapshotDetail::getLoadRate, SeMonitorThreshold.LOAD_RATE_OVERLOAD)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, start)
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, end));
+        return count != null && count > 0;
+    }
+
+    // ============================================================
+    // 状态判断 / 阈值
+    // ============================================================
+
+    private int calcCurrentStatus(BigDecimal loadRate) {
+        if (loadRate == null) return 0;
+        if (loadRate.compareTo(SeMonitorThreshold.LOAD_RATE_OVERLOAD) >= 0) return 2;
+        if (loadRate.compareTo(SeMonitorThreshold.LOAD_RATE_HEAVY) >= 0) return 1;
+        return 0;
+    }
+
+    private int calcFeederVoltageStatus(BigDecimal ratio) {
+        if (ratio == null) 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.compareTo(SeMonitorThreshold.MVTRANS_V_OVER) > 0) return 1;
+        if (ratio.compareTo(SeMonitorThreshold.MVTRANS_V_UNDER) < 0) return 2;
+        return 0;
+    }
+
+    private String resolveVoltageAlarmType(String deviceType, int status) {
+        if ("consumer".equals(deviceType)) {
+            switch (status) {
+                case 1: return "voltage_over_general";
+                case 2: return "voltage_over_severe";
+                case 3: return "voltage_under_general";
+                case 4: return "voltage_under_severe";
+                default: return "voltage_over";
+            }
+        }
+        return status == 1 ? "voltage_over" : "voltage_under";
+    }
+
+    private BigDecimal resolveVoltageThreshold(String deviceType, int status) {
+        if ("feeder".equals(deviceType)) return status == 1 ? SeMonitorThreshold.FEEDER_V_OVER : SeMonitorThreshold.FEEDER_V_UNDER;
+        if ("mvtrans".equals(deviceType)) return status == 1 ? SeMonitorThreshold.MVTRANS_V_OVER : SeMonitorThreshold.MVTRANS_V_UNDER;
+        switch (status) {
+            case 1: return SeMonitorThreshold.CONSUMER_GENERAL_V_OVER;
+            case 2: return SeMonitorThreshold.CONSUMER_SEVERE_V_OVER;
+            case 3: return SeMonitorThreshold.CONSUMER_GENERAL_V_UNDER;
+            case 4: return SeMonitorThreshold.CONSUMER_SEVERE_V_UNDER;
+            default: return null;
+        }
+    }
+
+    // ============================================================
+    // 辅助
+    // ============================================================
+
+    private BigDecimal firstValue(JSONArray arr) {
+        if (arr == null || arr.isEmpty()) return null;
+        String v = arr.getString(0);
+        if (v == null || v.isEmpty()) return null;
+        try { return new BigDecimal(v); } catch (NumberFormatException e) { return null; }
+    }
+
+    private String lookupCountyName(String countyId, Map<String, String[]> feederMap) {
+        return null;
+    }
+
+    private Map<String, String[]> buildFeederMapping() {
+        Map<String, String[]> map = new HashMap<>();
+        try {
+            List<FeederGroupTreeNode> tree = feederGroupApplication.getFeederGroupTree();
+            if (tree != null) buildFeederMappingRecursive(tree, map, "", "");
+        } 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;
+        for (FeederGroupTreeNode node : nodes) {
+            String subsId = parentSubsId, countyId = parentCountyId;
+            if (node.getType() == 2) countyId = node.getId();
+            else if (node.getType() == 3) subsId = node.getId();
+            else if (node.getType() == 4) map.put(node.getId(), new String[]{subsId, countyId, node.getName()});
+            if (node.getChildren() != null && !node.getChildren().isEmpty())
+                buildFeederMappingRecursive(node.getChildren(), map, subsId, countyId);
+        }
+    }
+}

+ 151 - 15
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SjztPwwyyqxtPrmPdrOutageMvDsServiceImpl.java

@@ -1,16 +1,25 @@
 package com.hdkj.lt.bf.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
+import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.entity.SjztPwwyyqxtPrmPdrOutageMvDs;
-import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
+import com.hdkj.lt.bf.entity.dto.OutageAggregationDTO;
+import com.hdkj.lt.bf.entity.dto.TodayOutageData;
+import com.hdkj.lt.bf.mapper.GridPdOutageEventBaseMapper;
 import com.hdkj.lt.bf.mapper.SjztPwwyyqxtPrmPdrOutageMvDsMapper;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
+import com.hdkj.lt.core.bizms.modle.po.GridPdOutageEventBase;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
-import java.time.LocalTime;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -18,26 +27,153 @@ import java.util.List;
 * @description 针对表【sjzt_pwwyyqxt_prm_pdr_outage_mv_ds(un4113_02_pwwyyqxt_prm_pdr_outage_mv_ds)】的数据库操作Service实现
 * @createDate 2026-07-18 14:32:38
 */
+@Slf4j
 @Service
+@RequiredArgsConstructor
 public class SjztPwwyyqxtPrmPdrOutageMvDsServiceImpl extends ServiceImpl<SjztPwwyyqxtPrmPdrOutageMvDsMapper, SjztPwwyyqxtPrmPdrOutageMvDs>
     implements SjztPwwyyqxtPrmPdrOutageMvDsService{
 
-    @Autowired
-    private SjztPwwyyqxtPrmPdrOutageMvDsMapper prmPdrOutageMvDsMapper;
+    private final SjztPwwyyqxtPrmPdrOutageMvDsMapper prmPdrOutageMvDsMapper;
+    private final FeederGroupApplication feederGroupApplication;
+    private final GridPdOutageEventBaseMapper gridPdOutageEventBaseMapper;
+
+    // ============================================================
+    // T-2 历史数据(sjzt_pwwyyqxt_prm_pdr_outage_mv_ds)
+    // ============================================================
 
-    /**
-     * 停电时户数查询(静态指标)
-     * @param feederNameList
-     * @return
-     */
     @Override
-    public BigDecimal selectStaticOutageDurationHoursHouseholds(List<String> feederNameList) {
-        LocalDateTime startOfMonth = LocalDate.now().withDayOfMonth(1).atStartOfDay();
-        LocalDateTime endOfToday = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
-        return prmPdrOutageMvDsMapper.selectStaticOutageDurationHoursHouseholds(feederNameList,startOfMonth,endOfToday);
+        public BigDecimal selectStaticOutageDurationHoursHouseholds(List<String> feederNameList) {
+            LocalDateTime startOfMonth = LocalDate.now().withDayOfMonth(1).atStartOfDay();
+            LocalDateTime endOfToday = LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MAX);
+        return prmPdrOutageMvDsMapper.selectStaticOutageDurationHoursHouseholds(feederNameList, startOfMonth, endOfToday);
+    }
+
+    @Override
+    public TodayOutageData getOutageDurationHoursHouseholds(String id, Integer type,
+                                                             LocalDateTime startTime, LocalDateTime endTime) {
+        List<FeederGroupTreeNode> tree = feederGroupApplication.getFeederGroupTree();
+        List<String> feederNames = new ArrayList<>();
+
+        if (type == 4) {
+            FeederGroupTreeNode node = findNodeRecursively(tree, 4, id);
+            if (node != null) {
+                feederNames.add(node.getName());
+            }
+        } else {
+            FeederGroupTreeNode node = findNodeRecursively(tree, type, id);
+            if (node != null && node.getChildren() != null) {
+                collectNodesByType(node.getChildren(), feederNames);
+            }
+        }
+
+        if (feederNames.isEmpty()) {
+            log.warn("未找到馈线名列表 id={}, type={}", id, type);
+            return TodayOutageData.builder()
+                    .totalHouseholds(BigDecimal.ZERO)
+                    .avgDurationHours(BigDecimal.ZERO)
+                    .build();
+        }
+
+        OutageAggregationDTO stats = prmPdrOutageMvDsMapper.selectOutageAggregation(feederNames, startTime, endTime);
+
+        BigDecimal totalHC = stats != null ? stats.getTotalHouseholds() : BigDecimal.ZERO;
+        BigDecimal totalUsers = stats != null ? stats.getTotalAffectedUsers() : BigDecimal.ZERO;
+
+        // 平均停电时长(小时) = 总时户数 / 总影响用户数
+        BigDecimal avgDuration = (totalUsers.compareTo(BigDecimal.ZERO) > 0)
+                ? totalHC.divide(totalUsers, 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+
+        return TodayOutageData.builder()
+                .totalHouseholds(totalHC)
+                .avgDurationHours(avgDuration)
+                .build();
     }
-}
 
+    // ============================================================
+    // 当日实时数据(grid_pd_outage_event_base)
+    // ============================================================
 
+    @Override
+    public TodayOutageData getTodayOutageData(String id, Integer type) {
+        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+        LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
+
+        LambdaQueryWrapper<GridPdOutageEventBase> wrapper = new LambdaQueryWrapper<GridPdOutageEventBase>()
+                .ge(GridPdOutageEventBase::getBeginTime, todayStart)
+                .lt(GridPdOutageEventBase::getBeginTime, tomorrowStart);
+
+        if (type == 2) {
+            wrapper.eq(GridPdOutageEventBase::getCountyId, id);
+        } else if (type == 3) {
+            wrapper.eq(GridPdOutageEventBase::getRdtSubsId, id);
+        } else if (type == 4) {
+            wrapper.eq(GridPdOutageEventBase::getRdtFeederId, id);
+        } else {
+            log.warn("不支持的节点类型 type={}", type);
+            return TodayOutageData.builder()
+                    .totalHouseholds(BigDecimal.ZERO)
+                    .avgDurationHours(BigDecimal.ZERO)
+                    .build();
+        }
+
+        List<GridPdOutageEventBase> events = gridPdOutageEventBaseMapper.selectList(wrapper);
 
+        BigDecimal totalHouseholds = BigDecimal.ZERO;
+        BigDecimal totalAffectedUsers = BigDecimal.ZERO;
 
+        for (GridPdOutageEventBase event : events) {
+            // 累计停电时户数
+            if (event.getHouseholdCnt() != null) {
+                try {
+                    totalHouseholds = totalHouseholds.add(new BigDecimal(event.getHouseholdCnt()));
+                } catch (NumberFormatException e) {
+                    log.warn("householdCnt 格式异常: {}", event.getHouseholdCnt());
+                }
+            }
+            // 累计停电影响用户数(分母)
+            if (event.getAffectedConsCnt() != null) {
+                totalAffectedUsers = totalAffectedUsers.add(new BigDecimal(event.getAffectedConsCnt()));
+            }
+        }
+
+        // 平均停电时长(小时) = 总时户数 / 总影响用户数
+        BigDecimal avgDurationHours = (totalAffectedUsers.compareTo(BigDecimal.ZERO) > 0)
+                ? totalHouseholds.divide(totalAffectedUsers, 2, RoundingMode.HALF_UP)
+                : BigDecimal.ZERO;
+
+        return TodayOutageData.builder()
+                .totalHouseholds(totalHouseholds)
+                .avgDurationHours(avgDurationHours)
+                .build();
+    }
+
+    // ============================================================
+    // 树递归辅助
+    // ============================================================
+
+    private FeederGroupTreeNode findNodeRecursively(List<FeederGroupTreeNode> nodes, int nodeType, String targetId) {
+        if (nodes == null || nodes.isEmpty()) return null;
+        for (FeederGroupTreeNode node : nodes) {
+            if (node.getType() == nodeType && targetId.equals(node.getId())) {
+                return node;
+            }
+            if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+                FeederGroupTreeNode found = findNodeRecursively(node.getChildren(), nodeType, targetId);
+                if (found != null) return found;
+            }
+        }
+        return null;
+    }
+
+    private void collectNodesByType(List<FeederGroupTreeNode> nodes, List<String> collector) {
+        if (nodes == null || nodes.isEmpty()) return;
+        for (FeederGroupTreeNode node : nodes) {
+            if (node.getType() == 4) {
+                collector.add(node.getName());
+            } else if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+                collectNodesByType(node.getChildren(), collector);
+            }
+        }
+    }
+}

+ 2 - 2
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/StaticGridDashboardService.java

@@ -13,10 +13,10 @@ import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
 public interface StaticGridDashboardService extends IService<StaticIndicatorResult> {
 
     /**
-     * 查询最新一次静态网架评估结果
+     * 查询最新一次静态网架评估结果(默认当月)
      *
      * @return 仪表盘 VO
      */
-    StaticGridDashboardVO queryLatestDashboard(String daytime, String id, String type);
+    StaticGridDashboardVO queryLatestDashboard(String id, String type);
 
 }

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

@@ -2,14 +2,13 @@ package com.hdkj.lt.bf.service.staticgrid.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.hdkj.fhzg.optimization.response.FeederGroupTreeNode;
-import com.hdkj.lt.bf.application.FeederGroupApplication;
+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.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 lombok.RequiredArgsConstructor;
@@ -36,7 +35,7 @@ import java.util.stream.Collectors;
  *   <li>type=4 馈线 — 取单条馈线的 feeder 级指标(聚合后 bool→0/1、rate→0%/100%)</li>
  * </ul>
  * 静态指标按月份过滤,前端传 daytime(如 2026-7-16),取该月最新一批数据。
- * 停电时户数通过馈线组树递归收集馈线名列表后查询
+ * 停电时户数通过 SjztPwwyyqxtPrmPdrOutageMvDsService 获取
  *
  * @author lsl
  * @since 2026-07-19
@@ -49,9 +48,8 @@ public class StaticGridDashboardServiceImpl
         implements StaticGridDashboardService {
 
     private final StaticIndicatorMetricMapper metricMapper;
-    private final StaticProblemDetailMapper problemDetailMapper;
-    private final FeederGroupApplication feederGroupApplication;
-    private final SjztPwwyyqxtPrmPdrOutageMvDsService pwwyyqxtPrmPdrOutageMvDsService;
+    private final SjztPwwyyqxtPrmPdrOutageMvDsService outageDurationService;
+    private final FhzgSeCapacityDailyMapper capacityDailyMapper;
 
     // ============================================================
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
@@ -97,7 +95,7 @@ public class StaticGridDashboardServiceImpl
     // ============================================================
 
     @Override
-    public StaticGridDashboardVO queryLatestDashboard(String daytime, String id, String type) {
+    public StaticGridDashboardVO queryLatestDashboard(String id, String type) {
         if (StringUtils.isBlank(id) || StringUtils.isBlank(type)) {
             log.warn("参数缺失: id={}, type={}", id, type);
             return null;
@@ -111,7 +109,7 @@ public class StaticGridDashboardServiceImpl
             return null;
         }
 
-        YearMonth ym = parseYearMonth(daytime);
+        YearMonth ym = YearMonth.now();
         if (nodeType == 2) {
             return queryByCounty(id, ym);
         } else if (nodeType == 3) {
@@ -140,8 +138,13 @@ public class StaticGridDashboardServiceImpl
         List<StaticIndicatorMetric> countyMetrics = getMetrics(result.getId(), "county", null, null);
         Map<String, String> metricMap = metricsToMap(countyMetrics);
         // 停电时户数
-        BigDecimal outage = handleOutageDurationHoursHouseholds(maintOrgId, 2);
-        return buildDashboardVO(result, metricMap, outage);
+        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
+                maintOrgId, 2,
+                ym.atDay(1).atStartOfDay(),
+                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
+        fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
+        return vo;
     }
 
     /**
@@ -165,8 +168,13 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
-        BigDecimal outage = handleOutageDurationHoursHouseholds(substationId, 3);
-        return buildDashboardVO(result, metricMap, outage);
+        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
+                substationId, 3,
+                ym.atDay(1).atStartOfDay(),
+                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
+        fillCapacityMetrics(vo, null, substationId, null, ym);
+        return vo;
     }
 
     /**
@@ -190,8 +198,13 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
-        BigDecimal outage = handleOutageDurationHoursHouseholds(feederId, 4);
-        return buildDashboardVO(result, metricMap, outage);
+        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
+                feederId, 4,
+                ym.atDay(1).atStartOfDay(),
+                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
+        fillCapacityMetrics(vo, null, null, feederId, ym);
+        return vo;
     }
 
     // ============================================================
@@ -324,42 +337,6 @@ public class StaticGridDashboardServiceImpl
                 .build();
     }
 
-    // ============================================================
-    // 停电时户数 — 改造为 (id, type)
-    // ============================================================
-
-    /**
-     * 根据节点类型和ID,从馈线组树收集馈线名列表,查询停电时户数
-     *
-     * @param id   节点设备ID
-     * @param type 节点类型 2=区县 3=变电站 4=馈线
-     * @return 停电时户数
-     */
-    public BigDecimal handleOutageDurationHoursHouseholds(String id, int type) {
-        List<FeederGroupTreeNode> tree = feederGroupApplication.getFeederGroupTree();
-        List<String> feederNames = new ArrayList<>();
-
-        if (type == 4) {
-            // 馈线:直接找该节点取 name
-            FeederGroupTreeNode node = findNodeRecursively(tree, 4, id);
-            if (node != null) {
-                feederNames.add(node.getName());
-            }
-        } else {
-            // 区县/变电站:找该节点 → 递归收集子树所有 type=4 的馈线名
-            FeederGroupTreeNode node = findNodeRecursively(tree, type, id);
-            if (node != null && node.getChildren() != null) {
-                collectNodesByType(node.getChildren(), feederNames);
-            }
-        }
-
-        if (feederNames.isEmpty()) {
-            log.warn("未找到馈线名列表 id={}, type={}", id, type);
-            return null;
-        }
-        return pwwyyqxtPrmPdrOutageMvDsService.selectStaticOutageDurationHoursHouseholds(feederNames);
-    }
-
     // ============================================================
     // 主表查询辅助
     // ============================================================
@@ -419,35 +396,6 @@ public class StaticGridDashboardServiceImpl
         return map;
     }
 
-    // ============================================================
-    // 树递归辅助
-    // ============================================================
-
-    private FeederGroupTreeNode findNodeRecursively(List<FeederGroupTreeNode> nodes, int nodeType, String targetId) {
-        if (nodes == null || nodes.isEmpty()) return null;
-        for (FeederGroupTreeNode node : nodes) {
-            if (node.getType() == nodeType && targetId.equals(node.getId())) {
-                return node;
-            }
-            if (node.getChildren() != null && !node.getChildren().isEmpty()) {
-                FeederGroupTreeNode found = findNodeRecursively(node.getChildren(), nodeType, targetId);
-                if (found != null) return found;
-            }
-        }
-        return null;
-    }
-
-    private void collectNodesByType(List<FeederGroupTreeNode> nodes, List<String> collector) {
-        if (nodes == null || nodes.isEmpty()) return;
-        for (FeederGroupTreeNode node : nodes) {
-            if (node.getType() == 4) {
-                collector.add(node.getName());
-            } else if (node.getChildren() != null && !node.getChildren().isEmpty()) {
-                collectNodesByType(node.getChildren(), collector);
-            }
-        }
-    }
-
     // ============================================================
     // 工具方法
     // ============================================================
@@ -506,4 +454,72 @@ public class StaticGridDashboardServiceImpl
             }
         }
     }
-}
+
+    // ============================================================
+    // 供电能力数据:从 capacity_daily 查区域/馈线负载率
+    // ============================================================
+
+    /**
+     * 从 fhzg_se_capacity_daily 查询区域/馈线最大负载率、平均负载率、过载条数
+     * 时间段:月初到昨天(当日数据尚未入日汇总表)
+     */
+    private void fillCapacityMetrics(StaticGridDashboardVO vo, String countyId,
+                                      String subsId, String feederId, YearMonth ym) {
+        LocalDate monthStart = ym.atDay(1);
+        LocalDate monthEnd = ym.atEndOfMonth();
+        LocalDate today = LocalDate.now();
+        LocalDate endDate = monthEnd.isBefore(today) ? monthEnd : today.minusDays(1);
+
+        // 按维度过滤
+        LambdaQueryWrapper<FhzgSeCapacityDaily> wrapper = new LambdaQueryWrapper<FhzgSeCapacityDaily>()
+                .ge(FhzgSeCapacityDaily::getStatDate, monthStart)
+                .le(FhzgSeCapacityDaily::getStatDate, endDate);
+        if (feederId != null) {
+            wrapper.eq(FhzgSeCapacityDaily::getFeederId, feederId);
+        } else if (subsId != null) {
+            wrapper.eq(FhzgSeCapacityDaily::getSubsId, subsId);
+        } else if (countyId != null) {
+            wrapper.eq(FhzgSeCapacityDaily::getCountyId, countyId);
+        }
+
+        List<FhzgSeCapacityDaily> list = capacityDailyMapper.selectList(wrapper);
+        if (list.isEmpty()) return;
+
+        // 区域最大负载率
+        BigDecimal maxLoad = list.stream()
+                .map(FhzgSeCapacityDaily::getMaxLoadRate)
+                .filter(Objects::nonNull)
+                .max(BigDecimal::compareTo)
+                .orElse(null);
+        vo.setRegionalMaxLoadRate(maxLoad);
+
+        // 区域平均负载率:对每日 avg_load_rate 取均值
+        double avg = list.stream()
+                .map(FhzgSeCapacityDaily::getAvgLoadRate)
+                .filter(Objects::nonNull)
+                .mapToDouble(BigDecimal::doubleValue)
+                .average()
+                .orElse(-1);
+        if (avg >= 0) {
+            vo.setRegionalAvgLoadRate(BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
+        }
+
+        // 过载线路数
+        if (feederId != null) {
+            // 单馈线:该月有任意过载记录即 1
+            boolean hasOverload = list.stream()
+                    .anyMatch(d -> d.getOverloadCount() != null && d.getOverloadCount() > 0);
+            vo.setLineOverloadCount(hasOverload ? 1 : 0);
+        } else {
+            // 区县/变电站:去重有过载记录的馈线数
+            long count = list.stream()
+                    .filter(d -> d.getOverloadCount() != null && d.getOverloadCount() > 0)
+                    .map(FhzgSeCapacityDaily::getFeederId)
+                    .distinct()
+                    .count();
+            if (count > 0) {
+                vo.setLineOverloadCount((int) count);
+            }
+        }
+    }
+}

+ 13 - 0
services/load-transfer-bf/src/main/resources/mapper/SjztPwwyyqxtPrmPdrOutageMvDsMapper.xml

@@ -116,4 +116,17 @@
         where td.outage_stime &gt; #{startOfMonth}
         and td.outage_stime &lt; #{endOfToday}
     </select>
+    <select id="selectOutageAggregation" resultType="com.hdkj.lt.bf.entity.dto.OutageAggregationDTO">
+        SELECT
+            COALESCE(SUM(td.outage_hc_num), 0) AS totalHouseholds,
+            COALESCE(SUM(COALESCE(td.outage_mvcons_cnt, 0) + COALESCE(td.outage_lvcons_cnt, 0)), 0) AS totalAffectedUsers
+        FROM sjzt_pwwyyqxt_prm_pdr_outage_mv_ds td
+        INNER JOIN(
+            <foreach collection="feederNameList" item="line" separator="UNION ALL">
+                select #{line} AS c
+            </foreach>
+        ) AS tmp ON td.outage_feeder_name = tmp.c
+        where td.outage_stime &gt;= #{startTime}
+        and td.outage_stime &lt; #{endTime}
+    </select>
 </mapper>

+ 0 - 123
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/entity/indicator/AlarmEvent.java

@@ -1,123 +0,0 @@
-package com.hdkj.lt.mq.entity.indicator;
-
-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;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-
-/**
- * 告警事件(t_alarm_event)
- *
- * @author lsl
- * @since 2026-07-17
- */
-@TableName("t_alarm_event")
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class AlarmEvent implements Serializable {
-
-    @TableField(exist = false)
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /**
-     * 批次ID
-     */
-    private String batchId;
-
-    /**
-     * 区县ID
-     */
-    private Integer countyId;
-
-    /**
-     * 区县短标识
-     */
-    private String countySid;
-
-    /**
-     * 区县名称
-     */
-    private String countyName;
-
-    /**
-     * 馈线ID
-     */
-    private String feederId;
-
-    /**
-     * 告警类型(逗号分隔)
-     */
-    private String alarmTypes;
-
-    /**
-     * 最大负载率(百分比)
-     */
-    private BigDecimal maxLoadRatePct;
-
-    /**
-     * 最大电压(pu)
-     */
-    private BigDecimal maxVoltagePu;
-
-    /**
-     * 最小电压(pu)
-     */
-    private BigDecimal minVoltagePu;
-
-    /**
-     * 关联指标结果ID
-     */
-    private Long indicatorResultId;
-
-    /**
-     * 告警次数(累计)
-     */
-    private Integer alarmCount;
-
-    /**
-     * 告警日期
-     */
-    private LocalDate alarmDate;
-
-    /**
-     * 首次告警时间
-     */
-    private LocalDateTime alarmTime;
-
-    /**
-     * 最近告警时间
-     */
-    private LocalDateTime lastAlarmTime;
-
-    /**
-     * 状态:0-待处理,1-重构中,2-已完成,3-已忽略,4-重构失败
-     */
-    private Integer status;
-
-    /**
-     * 创建时间
-     */
-    private LocalDateTime createTime;
-
-    /**
-     * 更新时间
-     */
-    private LocalDateTime updateTime;
-}

+ 0 - 172
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/entity/indicator/IndicatorResult.java

@@ -1,172 +0,0 @@
-package com.hdkj.lt.mq.entity.indicator;
-
-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;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-
-/**
- * 指标计算结果(t_indicator_result)
- *
- * @author lsl
- * @since 2026-07-17
- */
-@TableName("t_indicator_result")
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class IndicatorResult implements Serializable {
-
-    @TableField(exist = false)
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /**
-     * 批次ID
-     */
-    private String batchId;
-
-    /**
-     * 区县ID
-     */
-    private Integer countyId;
-
-    /**
-     * 区县短标识
-     */
-    private String countySid;
-
-    /**
-     * 区县名称
-     */
-    private String countyName;
-
-    /**
-     * 计算状态
-     */
-    private String status;
-
-    /**
-     * 计算结果
-     */
-    private String computeResult;
-
-    /**
-     * 生成时间(算法返回)
-     */
-    private String generatedAt;
-
-    /**
-     * 最大负载率(百分比)
-     */
-    private BigDecimal maxLoadRatePct;
-
-    /**
-     * 最小负载率(百分比)
-     */
-    private BigDecimal minLoadRatePct;
-
-    /**
-     * 平均负载率(百分比)
-     */
-    private BigDecimal avgLoadRatePct;
-
-    /**
-     * 告警阈值(百分比)
-     */
-    private BigDecimal alarmThresholdPct;
-
-    /**
-     * 最大电压(pu)
-     */
-    private BigDecimal maxVoltagePu;
-
-    /**
-     * 最小电压(pu)
-     */
-    private BigDecimal minVoltagePu;
-
-    /**
-     * 平均电压(pu)
-     */
-    private BigDecimal avgVoltagePu;
-
-    /**
-     * 总损耗率(百分比)
-     */
-    private BigDecimal totalLossRatePct;
-
-    /**
-     * 变压器损耗率(百分比)
-     */
-    private BigDecimal transformerLossRatePct;
-
-    /**
-     * 线路损耗率(百分比)
-     */
-    private BigDecimal lineLossRatePct;
-
-    /**
-     * 负载率超限 0/1
-     */
-    private Integer loadRateOverLimit;
-
-    /**
-     * 电压越上限 0/1
-     */
-    private Integer voltageOverUpperLimit;
-
-    /**
-     * 电压越下限 0/1
-     */
-    private Integer voltageUnderLowerLimit;
-
-    /**
-     * 超限数量
-     */
-    private Integer overLimitCount;
-
-    /**
-     * 超限馈线ID列表(逗号分隔)
-     */
-    private String overLimitFeederIds;
-
-    /**
-     * 算法是否触发重构 0/1
-     */
-    private Integer reconfigurationTriggered;
-
-    /**
-     * 触发类型
-     */
-    private String triggerType;
-
-    /**
-     * 问题馈线ID列表(逗号分隔)
-     */
-    private String problemFeederIds;
-
-    /**
-     * 原始响应JSON
-     */
-    private String rawResponse;
-
-    /**
-     * 创建时间
-     */
-    private LocalDateTime createTime;
-}

+ 0 - 128
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/IndicatorCalcConsumer.java

@@ -1,128 +0,0 @@
-package com.hdkj.lt.mq.listener;
-
-import com.alibaba.fastjson.JSON;
-import com.aliyun.openservices.ons.api.Action;
-import com.aliyun.openservices.ons.api.ConsumeContext;
-import com.aliyun.openservices.ons.api.Message;
-import com.aliyun.openservices.ons.api.MessageListener;
-import com.hdkj.lt.base.constants.RedisKeyConstants;
-import com.hdkj.lt.base.constants.RocketMQConstants;
-import com.hdkj.lt.core.mq.client.RocketMQTemplate;
-import com.hdkj.lt.core.mq.model.RocketMQMessageEntity;
-import com.hdkj.lt.feign.IAlgorithmApiClient;
-import com.hdkj.lt.modle.dto.indicator.IndicatorCalcMessage;
-import com.hdkj.lt.modle.dto.indicator.IndicatorCompletedEvent;
-import com.hdkj.lt.mq.entity.indicator.IndicatorResult;
-import com.hdkj.lt.mq.service.indicator.IndicatorResultService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.stereotype.Component;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * 指标计算消费者(indicator-calc-topic)
- * <p>
- * 消费指标计算任务消息,通过 Feign 调 si 模块算法接口获取指标JSON,
- * 入库后发送指标完成事件到 indicator-event-topic。
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class IndicatorCalcConsumer implements MessageListener {
-
-    private final IAlgorithmApiClient algorithmApiClient;
-    private final IndicatorResultService indicatorResultService;
-    private final RocketMQTemplate rocketMQTemplate;
-
-    @Override
-    public Action consume(Message message, ConsumeContext consumeContext) {
-        try {
-            log.info("===== 指标计算任务-开始消费 =====");
-            String body = new String(message.getBody(), StandardCharsets.UTF_8);
-            IndicatorCalcMessage calcMessage = JSON.parseObject(body, IndicatorCalcMessage.class);
-            if (calcMessage == null) {
-                log.warn("指标计算任务消息解析失败,body={}", body);
-                return Action.ReconsumeLater;
-            }
-            String batchId = calcMessage.getBatchId();
-            Integer countyId = calcMessage.getCountyId();
-
-            // 调算法接口获取指标JSON(返回 ApiResponse 外壳,data 字段为算法原始JSON)
-            String apiResponseJson = algorithmApiClient.queryIndicators();
-            if (StringUtils.isBlank(apiResponseJson)) {
-                log.warn("算法接口返回空 batchId={}, countyId={}", batchId, countyId);
-                return Action.ReconsumeLater;
-            }
-
-            // 从 ApiResponse 外壳提取 data(算法原始JSON)
-            com.alibaba.fastjson.JSONObject apiResp = JSON.parseObject(apiResponseJson);
-            String indicatorJson = apiResp.getString("data");
-            if (StringUtils.isBlank(indicatorJson)) {
-                log.warn("算法接口返回 data 为空, apiResponse={}", apiResponseJson);
-                return Action.ReconsumeLater;
-            }
-
-            // 入库
-            IndicatorResult result = indicatorResultService.saveResult(indicatorJson, batchId, countyId);
-
-            // 构建指标完成事件
-            IndicatorCompletedEvent event = IndicatorCompletedEvent.builder()
-                    .batchId(result.getBatchId())
-                    .countyId(result.getCountyId())
-                    .countySid(result.getCountySid())
-                    .countyName(result.getCountyName())
-                    .indicatorResultId(result.getId())
-                    .loadRateOverLimit(result.getLoadRateOverLimit())
-                    .voltageOverUpperLimit(result.getVoltageOverUpperLimit())
-                    .voltageUnderLowerLimit(result.getVoltageUnderLowerLimit())
-                    .overLimitFeederIds(splitToList(result.getOverLimitFeederIds()))
-                    .reconfigurationTriggered(result.getReconfigurationTriggered())
-                    .triggerType(result.getTriggerType())
-                    .problemFeederIds(splitToList(result.getProblemFeederIds()))
-                    .build();
-
-            // 发送到指标完成事件 topic
-            RocketMQMessageEntity messageEntity = RocketMQMessageEntity.builder()
-                    .topicId(RocketMQConstants.TOPIC_ID_INDICATOR_EVENT)
-                    .cacheKey(RedisKeyConstants.ASYNC_QUEUE_INDICATOR_EVENT)
-                    .tag(batchId)
-                    .body(event)
-                    .build();
-            rocketMQTemplate.sendAsyncMessage(messageEntity);
-
-            log.info("指标计算任务消费完成 batchId={}, countyId={}, indicatorResultId={}",
-                    batchId, countyId, result.getId());
-            return Action.CommitMessage;
-        } catch (Exception e) {
-            log.error("指标计算任务消费异常 message={}", message.getTopic(), e);
-            return Action.ReconsumeLater;
-        }
-    }
-
-    /**
-     * 逗号分隔字符串转 List
-     */
-    private List<String> splitToList(String commaStr) {
-        if (StringUtils.isBlank(commaStr)) {
-            return Collections.emptyList();
-        }
-        String[] arr = commaStr.split(",");
-        List<String> result = new ArrayList<String>(arr.length);
-        for (String s : arr) {
-            String trimmed = s.trim();
-            if (StringUtils.isNotBlank(trimmed)) {
-                result.add(trimmed);
-            }
-        }
-        return result;
-    }
-}

+ 0 - 183
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/IndicatorEventConsumer.java

@@ -1,183 +0,0 @@
-package com.hdkj.lt.mq.listener;
-
-import com.alibaba.fastjson.JSON;
-import com.aliyun.openservices.ons.api.Action;
-import com.aliyun.openservices.ons.api.ConsumeContext;
-import com.aliyun.openservices.ons.api.Message;
-import com.aliyun.openservices.ons.api.MessageListener;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.hdkj.lt.base.constants.RedisKeyConstants;
-import com.hdkj.lt.base.constants.RocketMQConstants;
-import com.hdkj.lt.core.mq.client.RocketMQTemplate;
-import com.hdkj.lt.core.mq.model.RocketMQMessageEntity;
-import com.hdkj.lt.modle.dto.indicator.IndicatorCompletedEvent;
-import com.hdkj.lt.modle.dto.recon.ReconCalcMessage;
-import com.hdkj.lt.mq.entity.indicator.AlarmEvent;
-import com.hdkj.lt.mq.entity.indicator.IndicatorResult;
-import com.hdkj.lt.mq.mapper.indicator.AlarmEventMapper;
-import com.hdkj.lt.mq.mapper.indicator.IndicatorResultMapper;
-import com.hdkj.lt.mq.service.indicator.AlarmEvaluateService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.nio.charset.StandardCharsets;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * 指标完成事件消费者(indicator-event-topic)
- * <p>
- * 消费指标完成事件,触发告警评估,对告警事件做抑制写入(同馈线同天活跃告警合并)。
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class IndicatorEventConsumer implements MessageListener {
-
-    private final AlarmEvaluateService alarmEvaluateService;
-    private final AlarmEventMapper alarmEventMapper;
-    private final IndicatorResultMapper indicatorResultMapper;
-    private final RocketMQTemplate rocketMQTemplate;
-
-    @Override
-    public Action consume(Message message, ConsumeContext consumeContext) {
-        try {
-            log.info("===== 指标完成事件-开始消费 =====");
-            String body = new String(message.getBody(), StandardCharsets.UTF_8);
-            IndicatorCompletedEvent event = JSON.parseObject(body, IndicatorCompletedEvent.class);
-            if (event == null) {
-                log.warn("指标完成事件消息解析失败,body={}", body);
-                return Action.ReconsumeLater;
-            }
-            Long indicatorResultId = event.getIndicatorResultId();
-            String batchId = event.getBatchId();
-            Integer countyId = event.getCountyId();
-
-            // 查指标结果拿 rawResponse(算法原始JSON,含 over_limit_details + feeder_results)
-            IndicatorResult indicatorResult = indicatorResultMapper.selectById(indicatorResultId);
-            String rawResponseJson = indicatorResult != null ? indicatorResult.getRawResponse() : null;
-
-            // 告警评估
-            List<AlarmEvent> alarmEvents = alarmEvaluateService.evaluateAlarms(indicatorResultId, batchId, countyId, rawResponseJson);
-            if (alarmEvents == null || alarmEvents.isEmpty()) {
-                log.info("指标完成事件无告警 indicatorResultId={}, batchId={}, countyId={}", indicatorResultId, batchId, countyId);
-                return Action.CommitMessage;
-            }
-
-            // 遍历写入告警事件(抑制),首次插入的告警触发重构计算
-            for (AlarmEvent alarmEvent : alarmEvents) {
-                boolean isNew = upsertAlarmEvent(alarmEvent);
-                if (isNew) {
-                    sendReconCalcMessage(alarmEvent);
-                }
-            }
-
-            log.info("指标完成事件消费完成 indicatorResultId={}, batchId={}, countyId={}, alarmCount={}",
-                    indicatorResultId, batchId, countyId, alarmEvents.size());
-            return Action.CommitMessage;
-        } catch (Exception e) {
-            log.error("指标完成事件消费异常 message={}", message.getTopic(), e);
-            return Action.ReconsumeLater;
-        }
-    }
-
-    /**
-     * 告警抑制写入:同馈线同天活跃告警合并,否则新增
-     *
-     * @return true=本次为新增插入(触发重构),false=更新已有告警(不触发)
-     */
-    private boolean upsertAlarmEvent(AlarmEvent alarmEvent) {
-        LocalDate alarmDate = alarmEvent.getAlarmDate() != null ? alarmEvent.getAlarmDate() : LocalDate.now();
-        LambdaQueryWrapper<AlarmEvent> wrapper = new LambdaQueryWrapper<AlarmEvent>()
-                .eq(AlarmEvent::getCountyId, alarmEvent.getCountyId())
-                .eq(AlarmEvent::getFeederId, alarmEvent.getFeederId())
-                .eq(AlarmEvent::getAlarmDate, alarmDate)
-                .in(AlarmEvent::getStatus, 0, 1)
-                .last("LIMIT 1");
-        AlarmEvent existing = alarmEventMapper.selectOne(wrapper);
-
-        if (existing != null) {
-            // 合并更新
-            existing.setBatchId(alarmEvent.getBatchId());
-            existing.setAlarmTypes(mergeAlarmTypes(existing.getAlarmTypes(), alarmEvent.getAlarmTypes()));
-            existing.setMaxLoadRatePct(alarmEvent.getMaxLoadRatePct());
-            existing.setMaxVoltagePu(alarmEvent.getMaxVoltagePu());
-            existing.setMinVoltagePu(alarmEvent.getMinVoltagePu());
-            existing.setIndicatorResultId(alarmEvent.getIndicatorResultId());
-            existing.setAlarmCount(existing.getAlarmCount() != null ? existing.getAlarmCount() + 1 : 1);
-            existing.setLastAlarmTime(LocalDateTime.now());
-            existing.setUpdateTime(LocalDateTime.now());
-            alarmEventMapper.updateById(existing);
-            log.info("告警抑制-更新 alarmEventId={}, feederId={}, alarmCount={}",
-                    existing.getId(), existing.getFeederId(), existing.getAlarmCount());
-            // 回填主键,便于上游必要时引用
-            alarmEvent.setId(existing.getId());
-            return false;
-        }
-        // 新增
-        alarmEvent.setAlarmCount(1);
-        alarmEvent.setAlarmDate(alarmDate);
-        LocalDateTime now = LocalDateTime.now();
-        alarmEvent.setAlarmTime(now);
-        alarmEvent.setLastAlarmTime(now);
-        alarmEvent.setCreateTime(now);
-        alarmEvent.setUpdateTime(now);
-        alarmEvent.setStatus(0);
-        alarmEventMapper.insert(alarmEvent);
-        log.info("告警抑制-新增 alarmEventId={}, feederId={}, countyId={}, alarmTypes={}",
-                alarmEvent.getId(), alarmEvent.getFeederId(), alarmEvent.getCountyId(), alarmEvent.getAlarmTypes());
-        return true;
-    }
-
-    /**
-     * 发送重构计算任务消息(仅首次插入的告警触发,避免重复告警反复重构)
-     */
-    private void sendReconCalcMessage(AlarmEvent alarmEvent) {
-        try {
-            ReconCalcMessage msg = ReconCalcMessage.builder()
-                    .eventId(alarmEvent.getId())
-                    .countySid(alarmEvent.getCountySid())
-                    .countyName(alarmEvent.getCountyName())
-                    .problemFeederIds(alarmEvent.getFeederId() != null
-                            ? Collections.singletonList(alarmEvent.getFeederId())
-                            : Collections.<String>emptyList())
-                    .build();
-            RocketMQMessageEntity messageEntity = RocketMQMessageEntity.builder()
-                    .topicId(RocketMQConstants.TOPIC_ID_RECONFIGURATION_CALC)
-                    .cacheKey(RedisKeyConstants.ASYNC_QUEUE_RECONFIGURATION_CALC)
-                    .tag(alarmEvent.getBatchId())
-                    .body(msg)
-                    .build();
-            rocketMQTemplate.sendAsyncMessage(messageEntity);
-            log.info("发送重构计算任务 alarmEventId={}, feederId={}, countySid={}",
-                    alarmEvent.getId(), alarmEvent.getFeederId(), alarmEvent.getCountySid());
-        } catch (Exception e) {
-            // MQ 发送失败不影响告警入库主流程,记录错误日志
-            log.error("发送重构计算任务失败 alarmEventId={}, feederId={}",
-                    alarmEvent.getId(), alarmEvent.getFeederId(), e);
-        }
-    }
-
-    /**
-     * 合并告警类型字符串,用 LinkedHashSet 去重后逗号拼接
-     */
-    private String mergeAlarmTypes(String existing, String newTypes) {
-        Set<String> set = new LinkedHashSet<String>();
-        if (existing != null && !existing.isEmpty()) {
-            set.addAll(Arrays.asList(existing.split(",")));
-        }
-        if (newTypes != null && !newTypes.isEmpty()) {
-            set.addAll(Arrays.asList(newTypes.split(",")));
-        }
-        return set.isEmpty() ? null : String.join(",", set);
-    }
-}

+ 0 - 125
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/listener/ReconCalcConsumer.java

@@ -1,125 +0,0 @@
-package com.hdkj.lt.mq.listener;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.aliyun.openservices.ons.api.Action;
-import com.aliyun.openservices.ons.api.ConsumeContext;
-import com.aliyun.openservices.ons.api.Message;
-import com.aliyun.openservices.ons.api.MessageListener;
-import com.hdkj.lt.feign.IReconApiClient;
-import com.hdkj.lt.modle.dto.recon.ReconCalcMessage;
-import com.hdkj.lt.modle.vo.recon.ReconstructionParam;
-import com.hdkj.lt.mq.entity.indicator.AlarmEvent;
-import com.hdkj.lt.mq.mapper.indicator.AlarmEventMapper;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.stereotype.Component;
-
-import java.nio.charset.StandardCharsets;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 重构计算任务消费者(reconfiguration-calc-topic)
- * <p>
- * 消费重构计算任务消息,通过 Feign 调 si 模块的 /recon/trigger 触发重构算法,
- * 根据返回结果更新 t_alarm_event 状态:
- * <ul>
- *   <li>开始:status 0(待处理) → 1(重构中)</li>
- *   <li>成功:status → 2(已完成)</li>
- *   <li>失败:status → 4(重构失败)</li>
- * </ul>
- *
- * @author lsl
- * @since 2026-07-19
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class ReconCalcConsumer implements MessageListener {
-
-    private final IReconApiClient reconApiClient;
-    private final AlarmEventMapper alarmEventMapper;
-
-    @Override
-    public Action consume(Message message, ConsumeContext consumeContext) {
-        try {
-            log.info("===== 重构计算任务-开始消费 =====");
-            String body = new String(message.getBody(), StandardCharsets.UTF_8);
-            ReconCalcMessage calcMessage = JSON.parseObject(body, ReconCalcMessage.class);
-            if (calcMessage == null || calcMessage.getEventId() == null) {
-                log.warn("重构计算任务消息解析失败,body={}", body);
-                return Action.ReconsumeLater;
-            }
-
-            Long eventId = calcMessage.getEventId();
-            AlarmEvent alarmEvent = alarmEventMapper.selectById(eventId);
-            if (alarmEvent == null) {
-                log.warn("告警事件不存在 eventId={}", eventId);
-                return Action.CommitMessage;
-            }
-
-            // 1. 标记重构中
-            updateStatus(eventId, 1);
-
-            // 2. 构造 si 接口入参
-            ReconstructionParam param = new ReconstructionParam();
-            param.setEventId(String.valueOf(eventId));
-            Map<String, Object> params = new HashMap<String, Object>();
-            params.put("county_sid", calcMessage.getCountySid());
-            params.put("county_name", calcMessage.getCountyName());
-            params.put("problem_feeder_ids", calcMessage.getProblemFeederIds());
-            param.setParams(params);
-
-            // 3. Feign 调 si 重构触发接口
-            String apiResponseJson;
-            try {
-                apiResponseJson = reconApiClient.triggerReconfiguration(param);
-            } catch (Exception e) {
-                log.error("调用重构触发接口异常 eventId={}", eventId, e);
-                updateStatus(eventId, 4);
-                return Action.ReconsumeLater;
-            }
-
-            if (StringUtils.isBlank(apiResponseJson)) {
-                log.error("重构触发接口返回空 eventId={}", eventId);
-                updateStatus(eventId, 4);
-                return Action.ReconsumeLater;
-            }
-
-            // 4. 解析 ApiResponse 外壳,取 data.success 判断成功
-            JSONObject apiResp = JSON.parseObject(apiResponseJson);
-            Integer code = apiResp.getInteger("code");
-            JSONObject data = apiResp.getJSONObject("data");
-            Boolean success = data != null ? data.getBoolean("success") : null;
-
-            if (code != null && code == 200 && Boolean.TRUE.equals(success)) {
-                updateStatus(eventId, 2);
-                log.info("重构计算任务完成 eventId={}, reconResult={}", eventId, data);
-            } else {
-                updateStatus(eventId, 4);
-                log.error("重构计算任务失败 eventId={}, apiResponse={}", eventId, apiResponseJson);
-                // 业务失败不重试,避免反复调用算法
-                return Action.CommitMessage;
-            }
-
-            return Action.CommitMessage;
-        } catch (Exception e) {
-            log.error("重构计算任务消费异常 topic={}", message.getTopic(), e);
-            return Action.ReconsumeLater;
-        }
-    }
-
-    /**
-     * 更新告警事件状态
-     */
-    private void updateStatus(Long eventId, Integer status) {
-        AlarmEvent update = new AlarmEvent();
-        update.setId(eventId);
-        update.setStatus(status);
-        update.setUpdateTime(LocalDateTime.now());
-        alarmEventMapper.updateById(update);
-    }
-}

+ 0 - 15
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/mapper/indicator/AlarmEventMapper.java

@@ -1,15 +0,0 @@
-package com.hdkj.lt.mq.mapper.indicator;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.hdkj.lt.mq.entity.indicator.AlarmEvent;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 告警事件 Mapper
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Mapper
-public interface AlarmEventMapper extends BaseMapper<AlarmEvent> {
-}

+ 0 - 15
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/mapper/indicator/IndicatorResultMapper.java

@@ -1,15 +0,0 @@
-package com.hdkj.lt.mq.mapper.indicator;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.hdkj.lt.mq.entity.indicator.IndicatorResult;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 指标计算结果 Mapper
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Mapper
-public interface IndicatorResultMapper extends BaseMapper<IndicatorResult> {
-}

+ 0 - 28
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/AlarmEvaluateService.java

@@ -1,28 +0,0 @@
-package com.hdkj.lt.mq.service.indicator;
-
-import com.hdkj.lt.mq.entity.indicator.AlarmEvent;
-
-import java.util.List;
-
-/**
- * 告警评估服务
- *
- * @author lsl
- * @since 2026-07-17
- */
-public interface AlarmEvaluateService {
-
-    /**
-     * 评估告警事件
-     * <p>
-     * 解析算法返回的 rawResponse,从 over_limit_details 提取超限馈线,
-     * 从 feeder_results 提取馈线级指标值(负载率/电压),构建告警事件列表。
-     *
-     * @param indicatorResultId 指标结果ID
-     * @param batchId           批次ID
-     * @param countyId          区县ID
-     * @param rawResponseJson   算法返回的原始JSON(t_indicator_result.raw_response)
-     * @return 告警事件列表(按馈线粒度),无告警返回空列表
-     */
-    List<AlarmEvent> evaluateAlarms(Long indicatorResultId, String batchId, Integer countyId, String rawResponseJson);
-}

+ 0 - 31
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/IndicatorResultService.java

@@ -1,31 +0,0 @@
-package com.hdkj.lt.mq.service.indicator;
-
-import com.hdkj.lt.mq.entity.indicator.IndicatorResult;
-
-/**
- * 指标计算结果服务
- *
- * @author lsl
- * @since 2026-07-17
- */
-public interface IndicatorResultService {
-
-    /**
-     * 保存指标计算结果(先删后插,幂等)
-     *
-     * @param rawResponseJson 算法返回的原始JSON
-     * @param batchId         批次ID
-     * @param countyId        区县ID
-     * @return 入库后的指标结果
-     */
-    IndicatorResult saveResult(String rawResponseJson, String batchId, Integer countyId);
-
-    /**
-     * 按批次+区县查询指标结果
-     *
-     * @param batchId  批次ID
-     * @param countyId 区县ID
-     * @return 指标结果,不存在返回 null
-     */
-    IndicatorResult getByBatchAndCounty(String batchId, Integer countyId);
-}

+ 0 - 249
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/impl/AlarmEvaluateServiceImpl.java

@@ -1,249 +0,0 @@
-package com.hdkj.lt.mq.service.indicator.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import com.hdkj.lt.mq.entity.indicator.AlarmEvent;
-import com.hdkj.lt.mq.service.indicator.AlarmEvaluateService;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 告警评估服务实现
- * <p>
- * 解析算法返回的 rawResponse,分两步:
- * <ol>
- *   <li>解析 over_limit_details + feeder_results,提取超限馈线和指标值</li>
- *   <li>调用 shouldAlarm() 自定义告警标志判断,满足条件才生成告警事件</li>
- * </ol>
- * <p>
- * alarm_types 值域:
- * <ul>
- *   <li>load_rate     → 线路重过载</li>
- *   <li>voltage_over  → 电压越限</li>
- *   <li>voltage_under → 电压越限</li>
- * </ul>
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Slf4j
-@Service
-public class AlarmEvaluateServiceImpl implements AlarmEvaluateService {
-
-    @Override
-    public List<AlarmEvent> evaluateAlarms(Long indicatorResultId, String batchId, Integer countyId, String rawResponseJson) {
-        log.info("评估告警 indicatorResultId={}, batchId={}, countyId={}, rawResponse={}",
-                indicatorResultId, batchId, countyId, rawResponseJson == null ? "null" : "provided");
-
-        List<AlarmEvent> alarmEvents = new ArrayList<AlarmEvent>();
-        if (StringUtils.isBlank(rawResponseJson)) {
-            log.warn("rawResponse 为空,跳过告警评估 indicatorResultId={}", indicatorResultId);
-            return alarmEvents;
-        }
-
-        JSONObject root = JSON.parseObject(rawResponseJson);
-        JSONObject calculationResult = root.getJSONObject("calculation_result");
-        if (calculationResult == null) {
-            log.warn("calculation_result 为空 indicatorResultId={}", indicatorResultId);
-            return alarmEvents;
-        }
-
-        JSONObject countyResult = calculationResult.getJSONObject("county_result");
-        if (countyResult == null) {
-            log.warn("county_result 为空 indicatorResultId={}", indicatorResultId);
-            return alarmEvents;
-        }
-
-        // ===== 第一步:解析超限数据 =====
-        // 从 over_limit_details 提取三类超限馈线
-        Map<String, List<String>> feederAlarmTypes = parseOverLimitDetails(countyResult);
-        if (feederAlarmTypes.isEmpty()) {
-            log.info("无超限馈线 indicatorResultId={}, batchId={}, countyId={}", indicatorResultId, batchId, countyId);
-            return alarmEvents;
-        }
-
-        // ===== 第二步:自定义告警标志判断 =====
-        if (!shouldAlarm(feederAlarmTypes, countyResult, root)) {
-            log.info("自定义告警判断未通过,不触发告警 indicatorResultId={}, batchId={}, countyId={}",
-                    indicatorResultId, batchId, countyId);
-            return alarmEvents;
-        }
-
-        // ===== 第三步:构建告警事件 =====
-        // 构建 feeder_results 索引(feeder_id → 指标值)
-        Map<String, JSONObject> feederResultMap = buildFeederResultMap(calculationResult);
-
-        // 取县域基本信息
-        JSONObject countyBasicInfo = root.getJSONObject("county_basic_info");
-        String countySid = countyBasicInfo != null ? countyBasicInfo.getString("county_sid") : null;
-        String countyName = countyBasicInfo != null ? countyBasicInfo.getString("county_name") : null;
-
-        // 为每个超限馈线构建 AlarmEvent
-        for (Map.Entry<String, List<String>> entry : feederAlarmTypes.entrySet()) {
-            String feederId = entry.getKey();
-            List<String> types = entry.getValue();
-            String alarmTypes = String.join(",", types);
-
-            // 从 feeder_results 取该馈线的指标值
-            JSONObject feederResult = feederResultMap.get(feederId);
-            BigDecimal maxLoadRatePct = null;
-            BigDecimal maxVoltagePu = null;
-            BigDecimal minVoltagePu = null;
-            if (feederResult != null) {
-                JSONObject loadRate = feederResult.getJSONObject("load_rate");
-                if (loadRate != null) {
-                    maxLoadRatePct = loadRate.getBigDecimal("max_load_rate_pct");
-                }
-                JSONObject voltagePu = feederResult.getJSONObject("voltage_pu");
-                if (voltagePu != null) {
-                    maxVoltagePu = voltagePu.getBigDecimal("max_voltage_pu");
-                    minVoltagePu = voltagePu.getBigDecimal("min_voltage_pu");
-                }
-            }
-
-            AlarmEvent alarmEvent = AlarmEvent.builder()
-                    .batchId(batchId)
-                    .countyId(countyId)
-                    .countySid(countySid)
-                    .countyName(countyName)
-                    .feederId(feederId)
-                    .alarmTypes(alarmTypes)
-                    .maxLoadRatePct(maxLoadRatePct)
-                    .maxVoltagePu(maxVoltagePu)
-                    .minVoltagePu(minVoltagePu)
-                    .indicatorResultId(indicatorResultId)
-                    .alarmDate(LocalDate.now())
-                    .build();
-
-            alarmEvents.add(alarmEvent);
-            log.info("告警评估-生成告警 feederId={}, alarmTypes={}, maxLoadRatePct={}, maxVoltagePu={}, minVoltagePu={}",
-                    feederId, alarmTypes, maxLoadRatePct, maxVoltagePu, minVoltagePu);
-        }
-
-        log.info("告警评估完成 indicatorResultId={}, batchId={}, countyId={}, alarmCount={}",
-                indicatorResultId, batchId, countyId, alarmEvents.size());
-        return alarmEvents;
-    }
-
-    // ============================================================
-    // 自定义告警标志判断(预留 customization point)
-    // ============================================================
-
-    /**
-     * 自定义告警标志判断
-     * <p>
-     * 自主判断指标数据是否满足告警条件。
-     * 满足返回 true → 继续触发下一步(生成告警事件)
-     * 不满足返回 false → 无事发生
-     * <p>
-     * 当前实现:算法返回 over_limit_details 有超限馈线即告警。
-     * 后续可在此自定义判断逻辑,例如:
-     * <ul>
-     *   <li>自定义阈值(不依赖算法的 flags,自行判断 max_load_rate_pct > 阈值)</li>
-     *   <li>白名单/黑名单(某些馈线不告警)</li>
-     *   <li>时间段过滤(某些时段不告警)</li>
-     *   <li>连续超限次数(N 次才告警)</li>
-     * </ul>
-     *
-     * @param feederAlarmTypes 超限馈线及告警类型(feederId → [load_rate / voltage_over / voltage_under])
-     * @param countyResult     县域汇总指标(load_rate / voltage_pu / loss / alarm)
-     * @param root             算法完整返回 JSON(含 county_basic_info / calculation_result)
-     * @return true=满足告警条件,false=不告警
-     */
-    private boolean shouldAlarm(Map<String, List<String>> feederAlarmTypes, JSONObject countyResult, JSONObject root) {
-        // TODO: 自定义告警标志判断逻辑
-        // 当前:算法判定超限即告警
-        return !feederAlarmTypes.isEmpty();
-    }
-
-    // ============================================================
-    // 数据解析
-    // ============================================================
-
-    /**
-     * 从 county_result.alarm.over_limit_details 解析三类超限馈线
-     *
-     * @return feederId → 告警类型列表(去重)
-     */
-    private Map<String, List<String>> parseOverLimitDetails(JSONObject countyResult) {
-        Map<String, List<String>> feederAlarmTypes = new LinkedHashMap<String, List<String>>();
-
-        JSONObject alarm = countyResult.getJSONObject("alarm");
-        if (alarm == null) {
-            return feederAlarmTypes;
-        }
-        JSONObject overLimitDetails = alarm.getJSONObject("over_limit_details");
-        if (overLimitDetails == null) {
-            return feederAlarmTypes;
-        }
-
-        List<String> loadRateFeeders = parseFeederList(overLimitDetails, "load_rate_over_limit");
-        List<String> voltageOverFeeders = parseFeederList(overLimitDetails, "voltage_over_upper_limit");
-        List<String> voltageUnderFeeders = parseFeederList(overLimitDetails, "voltage_under_lower_limit");
-
-        addAlarmTypes(feederAlarmTypes, loadRateFeeders, "load_rate");
-        addAlarmTypes(feederAlarmTypes, voltageOverFeeders, "voltage_over");
-        addAlarmTypes(feederAlarmTypes, voltageUnderFeeders, "voltage_under");
-
-        return feederAlarmTypes;
-    }
-
-    /**
-     * 从 over_limit_details 中解析某类超限馈线ID列表
-     */
-    private List<String> parseFeederList(JSONObject overLimitDetails, String key) {
-        JSONArray arr = overLimitDetails.getJSONArray(key);
-        if (arr == null || arr.isEmpty()) {
-            return new ArrayList<String>();
-        }
-        return arr.toJavaList(String.class);
-    }
-
-    /**
-     * 将馈线ID和告警类型加入 map
-     */
-    private void addAlarmTypes(Map<String, List<String>> feederAlarmTypes, List<String> feederIds, String alarmType) {
-        for (String feederId : feederIds) {
-            if (StringUtils.isBlank(feederId)) {
-                continue;
-            }
-            List<String> types = feederAlarmTypes.get(feederId);
-            if (types == null) {
-                types = new ArrayList<String>();
-                feederAlarmTypes.put(feederId, types);
-            }
-            if (!types.contains(alarmType)) {
-                types.add(alarmType);
-            }
-        }
-    }
-
-    /**
-     * 构建 feeder_results 索引:feeder_id → feederResult JSON 对象
-     */
-    private Map<String, JSONObject> buildFeederResultMap(JSONObject calculationResult) {
-        Map<String, JSONObject> map = new HashMap<String, JSONObject>();
-        JSONArray feederResults = calculationResult.getJSONArray("feeder_results");
-        if (feederResults == null || feederResults.isEmpty()) {
-            return map;
-        }
-        for (int i = 0; i < feederResults.size(); i++) {
-            JSONObject feeder = feederResults.getJSONObject(i);
-            String feederId = feeder.getString("feeder_id");
-            if (StringUtils.isNotBlank(feederId)) {
-                map.put(feederId, feeder);
-            }
-        }
-        return map;
-    }
-}

+ 0 - 175
services/load-transfer-mq/src/main/java/com/hdkj/lt/mq/service/indicator/impl/IndicatorResultServiceImpl.java

@@ -1,175 +0,0 @@
-package com.hdkj.lt.mq.service.indicator.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.hdkj.lt.mq.entity.indicator.IndicatorResult;
-import com.hdkj.lt.mq.mapper.indicator.IndicatorResultMapper;
-import com.hdkj.lt.mq.service.indicator.IndicatorResultService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.List;
-
-/**
- * 指标计算结果服务实现
- *
- * @author lsl
- * @since 2026-07-17
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class IndicatorResultServiceImpl implements IndicatorResultService {
-
-    private final IndicatorResultMapper indicatorResultMapper;
-
-    @Override
-    public IndicatorResult saveResult(String rawResponseJson, String batchId, Integer countyId) {
-        log.info("保存指标计算结果 batchId={}, countyId={}", batchId, countyId);
-        JSONObject root = JSON.parseObject(rawResponseJson, JSONObject.class);
-
-        // county_basic_info
-        JSONObject countyBasicInfo = root.getJSONObject("county_basic_info");
-        String countySid = null;
-        String countyName = null;
-        if (countyBasicInfo != null) {
-            countySid = countyBasicInfo.getString("county_sid");
-            countyName = countyBasicInfo.getString("county_name");
-        }
-
-        // calculation_result
-        JSONObject calculationResult = root.getJSONObject("calculation_result");
-        JSONObject countyResult = calculationResult != null ? calculationResult.getJSONObject("county_result") : null;
-
-        // load_rate
-        JSONObject loadRate = countyResult != null ? countyResult.getJSONObject("load_rate") : null;
-        BigDecimal maxLoadRatePct = getBigDecimal(loadRate, "max_load_rate_pct");
-        BigDecimal minLoadRatePct = getBigDecimal(loadRate, "min_load_rate_pct");
-        BigDecimal avgLoadRatePct = getBigDecimal(loadRate, "avg_load_rate_pct");
-        BigDecimal alarmThresholdPct = getBigDecimal(loadRate, "alarm_threshold_pct");
-
-        // voltage_pu
-        JSONObject voltagePu = countyResult != null ? countyResult.getJSONObject("voltage_pu") : null;
-        BigDecimal maxVoltagePu = getBigDecimal(voltagePu, "max_voltage_pu");
-        BigDecimal minVoltagePu = getBigDecimal(voltagePu, "min_voltage_pu");
-        BigDecimal avgVoltagePu = getBigDecimal(voltagePu, "avg_voltage_pu");
-
-        // loss
-        JSONObject loss = countyResult != null ? countyResult.getJSONObject("loss") : null;
-        BigDecimal totalLossRatePct = getBigDecimal(loss, "total_loss_rate_pct");
-        BigDecimal transformerLossRatePct = getBigDecimal(loss, "transformer_loss_rate_pct");
-        BigDecimal lineLossRatePct = getBigDecimal(loss, "line_loss_rate_pct");
-
-        // alarm
-        JSONObject alarm = countyResult != null ? countyResult.getJSONObject("alarm") : null;
-        JSONObject alarmFlags = alarm != null ? alarm.getJSONObject("flags") : null;
-        Integer loadRateOverLimit = getInteger(alarmFlags, "load_rate_over_limit");
-        Integer voltageOverUpperLimit = getInteger(alarmFlags, "voltage_over_upper_limit");
-        Integer voltageUnderLowerLimit = getInteger(alarmFlags, "voltage_under_lower_limit");
-        Integer overLimitCount = getInteger(alarm, "over_limit_count");
-        String overLimitFeederIds = joinList(alarm, "over_limit_feeder_ids");
-
-        // reconfiguration_feeders.algorithm_trigger
-        JSONObject reconfigurationFeeders = calculationResult != null ? calculationResult.getJSONObject("reconfiguration_feeders") : null;
-        JSONObject algorithmTrigger = reconfigurationFeeders != null ? reconfigurationFeeders.getJSONObject("algorithm_trigger") : null;
-        Integer reconfigurationTriggered = getInteger(algorithmTrigger, "reconfiguration_triggered");
-        String triggerType = algorithmTrigger != null ? algorithmTrigger.getString("trigger_type") : null;
-        String problemFeederIds = joinList(algorithmTrigger, "problem_feeder_ids");
-
-        // 顶层字段
-        String status = root.getString("status");
-        String computeResult = root.getString("compute_result");
-        String generatedAt = root.getString("generated_at");
-
-        // 先删后插(幂等)
-        LambdaQueryWrapper<IndicatorResult> deleteWrapper = new LambdaQueryWrapper<IndicatorResult>()
-                .eq(IndicatorResult::getBatchId, batchId)
-                .eq(IndicatorResult::getCountyId, countyId);
-        indicatorResultMapper.delete(deleteWrapper);
-
-        IndicatorResult result = IndicatorResult.builder()
-                .batchId(batchId)
-                .countyId(countyId)
-                .countySid(countySid)
-                .countyName(countyName)
-                .status(status)
-                .computeResult(computeResult)
-                .generatedAt(generatedAt)
-                .maxLoadRatePct(maxLoadRatePct)
-                .minLoadRatePct(minLoadRatePct)
-                .avgLoadRatePct(avgLoadRatePct)
-                .alarmThresholdPct(alarmThresholdPct)
-                .maxVoltagePu(maxVoltagePu)
-                .minVoltagePu(minVoltagePu)
-                .avgVoltagePu(avgVoltagePu)
-                .totalLossRatePct(totalLossRatePct)
-                .transformerLossRatePct(transformerLossRatePct)
-                .lineLossRatePct(lineLossRatePct)
-                .loadRateOverLimit(loadRateOverLimit)
-                .voltageOverUpperLimit(voltageOverUpperLimit)
-                .voltageUnderLowerLimit(voltageUnderLowerLimit)
-                .overLimitCount(overLimitCount)
-                .overLimitFeederIds(overLimitFeederIds)
-                .reconfigurationTriggered(reconfigurationTriggered)
-                .triggerType(triggerType)
-                .problemFeederIds(problemFeederIds)
-                .rawResponse(rawResponseJson)
-                .createTime(LocalDateTime.now())
-                .build();
-
-        indicatorResultMapper.insert(result);
-        log.info("保存指标计算结果完成 batchId={}, countyId={}, id={}", batchId, countyId, result.getId());
-        return result;
-    }
-
-    @Override
-    public IndicatorResult getByBatchAndCounty(String batchId, Integer countyId) {
-        LambdaQueryWrapper<IndicatorResult> wrapper = new LambdaQueryWrapper<IndicatorResult>()
-                .eq(IndicatorResult::getBatchId, batchId)
-                .eq(IndicatorResult::getCountyId, countyId);
-        return indicatorResultMapper.selectOne(wrapper);
-    }
-
-    /**
-     * 从 JSONObject 安全获取 BigDecimal
-     */
-    private BigDecimal getBigDecimal(JSONObject obj, String key) {
-        if (obj == null || obj.get(key) == null) {
-            return null;
-        }
-        return obj.getBigDecimal(key);
-    }
-
-    /**
-     * 从 JSONObject 安全获取 Integer
-     */
-    private Integer getInteger(JSONObject obj, String key) {
-        if (obj == null || obj.get(key) == null) {
-            return null;
-        }
-        return obj.getInteger(key);
-    }
-
-    /**
-     * 从 JSONObject 获取 List<String> 并用逗号拼接
-     */
-    private String joinList(JSONObject obj, String key) {
-        if (obj == null) {
-            return null;
-        }
-        JSONArray arr = obj.getJSONArray(key);
-        if (arr == null || arr.isEmpty()) {
-            return null;
-        }
-        List<String> list = arr.toJavaList(String.class);
-        if (list.isEmpty()) {
-            return null;
-        }
-        return String.join(",", list);
-    }
-}

+ 17 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/config/ScheduleConfig.java

@@ -0,0 +1,17 @@
+package com.hdkj.lt.si.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 静态网架定时任务配置
+ * <p>
+ * 启用 @Scheduled 支持,用于每月1号凌晨触发静态指标计算。
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@Configuration
+@EnableScheduling
+public class ScheduleConfig {
+}

+ 24 - 2
services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/algorithm/AlgorithmController.java

@@ -45,6 +45,28 @@ public class AlgorithmController extends BaseController {
     public ApiResponse<Long> queryStaticGridIndicators(@RequestBody Map<String, Object> params) {
         String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
         String countyId = params.get("countyId") != null ? String.valueOf(params.get("countyId")) : null;
-        return data(algorithmService.queryStaticGridIndicators(batchId, countyId));
+        String countySid = params.get("countySid") != null ? String.valueOf(params.get("countySid")) : null;
+        String countyName = params.get("countyName") != null ? String.valueOf(params.get("countyName")) : null;
+        String topologyVersion = params.get("topologyVersion") != null ? String.valueOf(params.get("topologyVersion")) : null;
+        return data(algorithmService.queryStaticGridIndicators(batchId, countyId, countySid, countyName, topologyVersion));
     }
-}
+
+    /**
+     * 手动触发静态网架评估(覆盖更新)
+     * <p>
+     * 先删除该区县已有数据,再调算法重新计算并入库。
+     * 当定时任务失败时可通过此端点手动重跑。
+     * <p>
+     * 入参:countyId、countySid、countyName、topologyVersion(可选)
+     *
+     * @return 主表ID
+     */
+    @PostMapping(value = "/static-grid/manual-trigger")
+    public ApiResponse<Long> triggerManualStaticGrid(@RequestBody Map<String, Object> params) {
+        String countyId = params.get("countyId") != null ? String.valueOf(params.get("countyId")) : null;
+        String countySid = params.get("countySid") != null ? String.valueOf(params.get("countySid")) : null;
+        String countyName = params.get("countyName") != null ? String.valueOf(params.get("countyName")) : null;
+        String topologyVersion = params.get("topologyVersion") != null ? String.valueOf(params.get("topologyVersion")) : null;
+        return data(algorithmService.triggerManualStaticGrid(countyId, countySid, countyName, topologyVersion));
+    }
+}

+ 10 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/controller/measurement/MeasurementApiController.java

@@ -76,6 +76,16 @@ public class MeasurementApiController implements IMeasurementApiClient {
         return null;
     }
 
+    @Override
+    @PostMapping("/queryConsumerVoltage")
+    @ApiOperation(value = "查询低压用户电压")
+    public ApiResponse<java.util.Map<String, BigDecimal>> queryConsumerVoltage(@RequestBody java.util.Map<String, Object> request) {
+        java.util.List<String> consNos = (java.util.List<String>) request.get("consNos");
+        String startTime = (String) request.get("startTime");
+        String endTime = (String) request.get("endTime");
+        return ApiResponse.success(measurementApiService.queryConsumerVoltage(consNos, startTime, endTime));
+    }
+
     /**
      * 缓存量测数据
      */

+ 61 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/schedule/StaticGridIndicatorScheduler.java

@@ -0,0 +1,61 @@
+package com.hdkj.lt.si.schedule;
+
+import com.hdkj.lt.si.service.staticgrid.StaticIndicatorResultService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+/**
+ * 静态网架指标月度定时任务
+ * <p>
+ * 每月1号凌晨2点触发,遍历所有区县调用算法服务,解析并入库。
+ * 区县列表见 {@link StaticGridScheduleConstants#COUNTIES}。
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class StaticGridIndicatorScheduler {
+
+    private final StaticIndicatorResultService staticIndicatorResultService;
+
+    /**
+     * 每月1号凌晨2点触发:遍历所有区县,逐个调用静态网架评估接口
+     */
+    @Scheduled(cron = "0 0 2 1 * ?")
+    public void scheduledTrigger() {
+        log.info("静态网架月度定时任务开始, topologyVersion={}", StaticGridScheduleConstants.TOPOLOGY_VERSION);
+
+        if (StaticGridScheduleConstants.COUNTIES.isEmpty()) {
+            log.warn("StaticGridScheduleConstants.COUNTIES 为空,跳过定时任务");
+            return;
+        }
+
+        String batchId = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
+
+        for (Map.Entry<String, String[]> entry : StaticGridScheduleConstants.COUNTIES) {
+            String countyId = entry.getKey();
+            String countySid = entry.getValue()[0];
+            String countyName = entry.getValue()[1];
+
+            try {
+                log.info("开始处理区县: countyId={}, countySid={}, countyName={}", countyId, countySid, countyName);
+                Long resultId = staticIndicatorResultService.saveStaticGridResult(
+                        batchId, countyId, countySid, countyName,
+                        StaticGridScheduleConstants.TOPOLOGY_VERSION);
+                log.info("区县处理完成: countyId={}, resultId={}", countyId, resultId);
+            } catch (Exception e) {
+                log.error("区县处理失败: countyId={}, error={}", countyId, e.getMessage(), e);
+            }
+        }
+
+        log.info("静态网架月度定时任务结束");
+    }
+}

+ 25 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/schedule/StaticGridScheduleConstants.java

@@ -0,0 +1,25 @@
+package com.hdkj.lt.si.schedule;
+
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 静态网架指标定时任务常量
+ *
+ * @author lsl
+ * @since 2026-07-25
+ */
+public interface StaticGridScheduleConstants {
+
+    /** 拓扑版本(算法服务方要求) */
+    String TOPOLOGY_VERSION = "2026-07-01";
+
+    /** 区县列表:countyId → [countySid, countyName] */
+    List<Map.Entry<String, String[]>> COUNTIES = Arrays.asList(
+            new AbstractMap.SimpleEntry<>("BA6481B8F34143FDA7E1C35A76846911", new String[]{"洪湖", "国网洪湖市供电公司"}),
+            new AbstractMap.SimpleEntry<>("54C4BBBB8FE4428E96B6AE12247C4DD4", new String[]{"秭归", "国网秭归县供电公司"}),
+            new AbstractMap.SimpleEntry<>("60FDC66B3FC143DC9C39001215DDCC61", new String[]{"长阳", "国网长阳县供电公司"})
+    );
+}

+ 18 - 4
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/AlgorithmService.java

@@ -17,9 +17,23 @@ public interface AlgorithmService {
     /**
      * 查询静态网架评估结果并入库
      *
-     * @param batchId  批次ID
-     * @param countyId 区县ID(即运维单位ID maintOrgId)
+     * @param batchId   批次ID
+     * @param countyId  区县ID(即运维单位ID maintOrgId)
+     * @param countySid 区县短标识
+     * @param countyName 区县名称
+     * @param topologyVersion 拓扑版本
      * @return 主表ID
      */
-    Long queryStaticGridIndicators(String batchId, String countyId);
-}
+    Long queryStaticGridIndicators(String batchId, String countyId, String countySid, String countyName, String topologyVersion);
+
+    /**
+     * 手动触发静态网架评估(覆盖更新)
+     *
+     * @param countyId  区县ID
+     * @param countySid 区县短标识
+     * @param countyName 区县名称
+     * @param topologyVersion 拓扑版本
+     * @return 主表ID
+     */
+    Long triggerManualStaticGrid(String countyId, String countySid, String countyName, String topologyVersion);
+}

+ 8 - 3
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/algorithm/impl/AlgorithmServiceImpl.java

@@ -29,7 +29,12 @@ public class AlgorithmServiceImpl implements AlgorithmService {
     }
 
     @Override
-    public Long queryStaticGridIndicators(String batchId, String countyId) {
-        return staticIndicatorResultService.saveStaticGridResult(batchId, countyId);
+    public Long queryStaticGridIndicators(String batchId, String countyId, String countySid, String countyName, String topologyVersion) {
+        return staticIndicatorResultService.saveStaticGridResult(batchId, countyId, countySid, countyName, topologyVersion);
     }
-}
+
+    @Override
+    public Long triggerManualStaticGrid(String countyId, String countySid, String countyName, String topologyVersion) {
+        return staticIndicatorResultService.triggerManualStaticGrid(countyId, countySid, countyName, topologyVersion);
+    }
+}

+ 11 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/measurement/IMeasurementApiService.java

@@ -8,6 +8,7 @@ import com.hdkj.hussar.ApiResponse;
 import org.springframework.web.bind.annotation.RequestBody;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -29,4 +30,14 @@ public interface IMeasurementApiService {
     public ApiResponse<StateEstimation> queryByFeederSaveCommonQuery(@RequestBody StateEstimationEquipRequest request);
 
     RecordsVO queryCommonQueryByDay(DistributionTransformerDTO request);
+
+    /**
+     * 查询低压用户电压(A相,.consType=2)
+     *
+     * @param consNos   用户编号列表
+     * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss
+     * @param endTime   结束时间 yyyy-MM-dd HH:mm:ss
+     * @return consNo → measValue(伏特V, pt=1不做变比换算)
+     */
+    Map<String, BigDecimal> queryConsumerVoltage(List<String> consNos, String startTime, String endTime);
 }

+ 72 - 0
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/measurement/impl/MeasurementApiService.java

@@ -483,5 +483,77 @@ public class MeasurementApiService implements IMeasurementApiService {
         return dateTime.format(dateTimeFormatter);
     }
 
+    /**
+     * 查询低压用户电压(A相,consType=2)
+     * 量测中心返回 measValue(伏特V),pt=1不做变比换算
+     */
+    @Override
+    public Map<String, BigDecimal> queryConsumerVoltage(List<String> consNos, String startTime, String endTime) {
+        Map<String, BigDecimal> result = new HashMap<>();
+        if (consNos == null || consNos.isEmpty()) {
+            return result;
+        }
+
+        String url = BASE_URL + "/realMeasCenter/telemetry/commonQuery";
+        String token;
+        try {
+            token = getAccessToken(getMeasurementTokenDTO()).getAccess_token();
+        } catch (Exception e) {
+            log.error("获取量测中心token失败", e);
+            return result;
+        }
+
+        // 分批查询,每批50个
+        int batchSize = 50;
+        for (int i = 0; i < consNos.size(); i += batchSize) {
+            int end = Math.min(i + batchSize, consNos.size());
+            List<String> batch = consNos.subList(i, end);
+
+            DistributionTransformerGetDTO dto = new DistributionTransformerGetDTO();
+            dto.setPage(1);
+            dto.setPerPage(2000);
+            dto.setStartTime(startTime);
+            dto.setEndTime(endTime);
+
+            List<DistributionTransformerGetDTO.FiltersDTO> filtersList = new ArrayList<>();
+            DistributionTransformerGetDTO.FiltersDTO filters = new DistributionTransformerGetDTO.FiltersDTO();
+            filters.setConsType("2");
+            filters.setConsNos(batch);
+            filters.setTelemetryTypes(Arrays.asList("PhV_phsA"));
+            filtersList.add(filters);
+            dto.setFilters(filtersList);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.add("x-token", token);
+            HttpEntity<DistributionTransformerGetDTO> entity = new HttpEntity<>(dto, headers);
+
+            try {
+                DistributionReceiveVO receiveVO = restTemplate.postForObject(url, entity, DistributionReceiveVO.class);
+                if (receiveVO == null || receiveVO.getResult() == null
+                        || receiveVO.getResult().getRecords() == null) {
+                    continue;
+                }
+                for (DistributionReceiveVO.ResultDTO.RecordsDTO record : receiveVO.getResult().getRecords()) {
+                    if (record.getTelemetryValue() == null || record.getTelemetryValue().isEmpty()) {
+                        continue;
+                    }
+                    String consNo = record.getConsNo() != null ? record.getConsNo().toString() : null;
+                    if (consNo == null) {
+                        continue;
+                    }
+                    String measValue = record.getTelemetryValue().get(record.getTelemetryValue().size() - 1).getMeasValue();
+                    if (measValue != null && !measValue.isEmpty()) {
+                        try {
+                            result.put(consNo, new BigDecimal(measValue));
+                        } catch (NumberFormatException ignored) {
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.error("查询用户电压失败 batch={}", batch, e);
+            }
+        }
+        return result;
+    }
 
 }

+ 3 - 2
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/remote/impl/AlgorithmRemoteCallServiceImpl.java

@@ -81,11 +81,12 @@ public class AlgorithmRemoteCallServiceImpl {
      * 算法返回完整 JSON(含 code/message/calculation_result 外壳),此处返回原始字符串,
      * 由 Service 层解析入库。
      *
+     * @param requestBodyJson 请求体 JSON(含 county 信息、scope 等)
      * @return 算法返回的完整 JSON 字符串
      */
-    public String queryStaticGridIndicators() {
+    public String queryStaticGridIndicators(String requestBodyJson) {
         String requestUrl = requestUri.concat(staticGridUri);
-        String requestBody = "{}";
+        String requestBody = requestBodyJson != null ? requestBodyJson : "{}";
         try {
             HttpHeaders headers = new HttpHeaders();
             headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

+ 18 - 4
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/staticgrid/StaticIndicatorResultService.java

@@ -12,11 +12,25 @@ import com.hdkj.lt.si.entity.staticgrid.StaticIndicatorResult;
 public interface StaticIndicatorResultService extends IService<StaticIndicatorResult> {
 
     /**
-     * 调用算法并解析入库
+     * 调用算法并解析入库(定时任务用,不覆盖)
      *
-     * @param batchId  批次ID
-     * @param countyId 区县ID(即运维单位ID maintOrgId)
+     * @param batchId   批次ID
+     * @param countyId  区县ID(即运维单位ID maintOrgId)
+     * @param countySid 区县短标识(如 honghu)
+     * @param countyName 区县名称
+     * @param topologyVersion 拓扑版本(如 2026-07-01)
      * @return 主表ID
      */
-    Long saveStaticGridResult(String batchId, String countyId);
+    Long saveStaticGridResult(String batchId, String countyId, String countySid, String countyName, String topologyVersion);
+
+    /**
+     * 手动触发静态网架评估,先删除该区县旧数据再覆盖写入(覆盖更新)
+     *
+     * @param countyId  区县ID
+     * @param countySid 区县短标识
+     * @param countyName 区县名称
+     * @param topologyVersion 拓扑版本
+     * @return 主表ID
+     */
+    Long triggerManualStaticGrid(String countyId, String countySid, String countyName, String topologyVersion);
 }

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

@@ -3,6 +3,7 @@ package com.hdkj.lt.si.service.staticgrid.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.hdkj.lt.si.dao.StaticIndicatorMetricMapper;
 import com.hdkj.lt.si.dao.StaticIndicatorResultMapper;
@@ -21,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -52,9 +54,20 @@ public class StaticIndicatorResultServiceImpl
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long saveStaticGridResult(String batchId, String countyId) {
-        // 1. 调算法
-        String rawResponse = algorithmRemoteCallService.queryStaticGridIndicators();
+    public Long saveStaticGridResult(String batchId, String countyId, String countySid, String countyName, String topologyVersion) {
+        // 1. 按新API格式构建请求体
+        JSONObject requestBody = new JSONObject();
+        JSONObject county = new JSONObject();
+        county.put("county_id", countyId);
+        county.put("county_sid", countySid != null ? countySid : countyId);
+        county.put("county_name", countyName != null ? countyName : countyId);
+        requestBody.put("county", county);
+        requestBody.put("scope", "county");
+        requestBody.put("topology_version", topologyVersion != null ? topologyVersion : "2026-07-01");
+        requestBody.put("force_recalculate", Boolean.TRUE);
+
+        // 2. 调算法
+        String rawResponse = algorithmRemoteCallService.queryStaticGridIndicators(requestBody.toJSONString());
         if (StringUtils.isBlank(rawResponse)) {
             log.warn("静态网架评估接口返回空 batchId={}, countyId={}", batchId, countyId);
             return null;
@@ -66,8 +79,8 @@ public class StaticIndicatorResultServiceImpl
         String requestId = root.getString("request_id");
 
         JSONObject countyBasicInfo = root.getJSONObject("county_basic_info");
-        String countySid = countyBasicInfo != null ? countyBasicInfo.getString("county_sid") : null;
-        String countyName = countyBasicInfo != null ? countyBasicInfo.getString("county_name") : null;
+        String respCountySid = countyBasicInfo != null ? countyBasicInfo.getString("county_sid") : null;
+        String respCountyName = countyBasicInfo != null ? countyBasicInfo.getString("county_name") : null;
         JSONArray feederIdsArr = countyBasicInfo != null ? countyBasicInfo.getJSONArray("feeder_ids") : null;
         String feederIds = feederIdsArr != null ? String.join(",", feederIdsArr.toJavaList(String.class)) : null;
 
@@ -183,6 +196,44 @@ public class StaticIndicatorResultServiceImpl
         return resultId;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long triggerManualStaticGrid(String countyId, String countySid, String countyName, String topologyVersion) {
+        // 1. 删除该区县已有数据
+        LambdaQueryWrapper<StaticIndicatorResult> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StaticIndicatorResult::getCountyId, countyId);
+        List<StaticIndicatorResult> existing = baseMapper.selectList(wrapper);
+
+        if (!existing.isEmpty()) {
+            List<Long> resultIds = new ArrayList<>();
+            for (StaticIndicatorResult r : existing) {
+                resultIds.add(r.getId());
+            }
+
+            // 删除指标明细
+            LambdaQueryWrapper<StaticIndicatorMetric> metricWrapper = new LambdaQueryWrapper<>();
+            metricWrapper.in(StaticIndicatorMetric::getResultId, resultIds);
+            int metricDel = metricMapper.delete(metricWrapper);
+
+            // 删除问题详情
+            LambdaQueryWrapper<StaticProblemDetail> problemWrapper = new LambdaQueryWrapper<>();
+            problemWrapper.in(StaticProblemDetail::getResultId, resultIds);
+            int problemDel = problemDetailMapper.delete(problemWrapper);
+
+            // 删除主表
+            int resultDel = baseMapper.delete(wrapper);
+
+            log.info("手动触发:已删除区县旧记录 countyId={}, resultCount={}, metricCount={}, problemCount={}",
+                    countyId, resultDel, metricDel, problemDel);
+        }
+
+        // 2. 生成批次ID
+        String batchId = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
+
+        // 3. 调用标准的保存逻辑(调算法 + 插入)
+        return saveStaticGridResult(batchId, countyId, countySid, countyName, topologyVersion);
+    }
+
     /**
      * 解析一个 category 下的 metrics 数组,追加到 metricList
      */
@@ -225,4 +276,4 @@ public class StaticIndicatorResultServiceImpl
             metricList.add(metric);
         }
     }
-}
+}