Sunlit 12 hodín pred
rodič
commit
9731d4fbbe
16 zmenil súbory, kde vykonal 667 pridanie a 78 odobranie
  1. 28 0
      api/load-transfer-bf-api/src/main/java/com/hdkj/lt/feign/IBlockVoltageReconClient.java
  2. 56 0
      common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeBlockReconLog.java
  3. 120 0
      common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeBlockVoltageEvent.java
  4. 7 0
      common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeBlockReconLogMapper.java
  5. 7 0
      common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeBlockVoltageEventMapper.java
  6. 1 1
      common/ruoyi-common-swagger/src/main/java/com/ovit/config/SwaggerConfig.java
  7. 20 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  8. 1 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/ReconQueueMsg.java
  9. 20 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeAlarmDetailVO.java
  10. 15 40
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/listener/ReconTriggerListener.java
  11. 128 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeBlockVoltageEventService.java
  12. 50 22
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/FhzgSeEventStateEstimationServiceImpl.java
  13. 165 14
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java
  14. 6 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  15. 8 0
      services/load-transfer-bf/src/main/resources/mapper/SjztPwwyyqxtPrmPdrOutageMvDsMapper.xml
  16. 35 0
      services/ruoyi-job/src/main/java/com/hdkj/lt/job/task/SeBlockVoltageReconTask.java

+ 28 - 0
api/load-transfer-bf-api/src/main/java/com/hdkj/lt/feign/IBlockVoltageReconClient.java

@@ -0,0 +1,28 @@
+package com.hdkj.lt.feign;
+
+import com.hdkj.hussar.ApiResponse;
+import com.hdkj.lt.base.constants.SystemGlobalConstant;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+
+/**
+ * 成片电压越限事件重构触发 Feign 客户端
+ * <p>
+ * 供 pwfhzg-job 定时任务通过 Feign 调用 bf 服务的 /indicator/block-voltage/recon-scan 端点,
+ * 扫描中台推送的成片数据并触发未处理的重构(幂等:按 event_id 记录 recon_log)。
+ *
+ * @author lsl
+ * @since 2026-08-13
+ */
+@FeignClient(path = "/indicator", value = SystemGlobalConstant.SERVICE_NAME_LOAD_TRANSFER_BF,
+        contextId = "IBlockVoltageReconClient")
+public interface IBlockVoltageReconClient {
+
+    /**
+     * 自动扫描成片电压事件重构
+     *
+     * @return 扫描结果消息
+     */
+    @PostMapping("/block-voltage/recon-scan")
+    ApiResponse<String> triggerBlockVoltageRecon();
+}

+ 56 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeBlockReconLog.java

@@ -0,0 +1,56 @@
+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;
+
+/**
+ * 成片电压越限事件重构状态(本地自维护,中台不碰)
+ * <p>
+ * 中台成片表 fhzg_se_block_voltage_event 是"先删后推"全量快照(DI 同步),
+ * 本地无法在其上维护重构状态(每次推送都被清空)。
+ * 本表以中台生成的 event_id 为幂等键,记录重构触发/结果,中台重推多少次都不重复触发。
+ * <p>
+ * 注意:id 使用自增补充列,建表时 AUTO_INCREMENT 从 1 亿起步——现有
+ * fhzg_se_current_event / fhzg_se_voltage_event 均为 AUTO 自增小值,若本表也用
+ * 小自增 id 会与事件表撞车:前端按列表返回的 alarmEventId(=本表 id) 调
+ * queryAlarmDetail 时按 current→voltage→recon_log 顺序查表,撞号会命中错误事件详情。
+ *
+ * @author lsl
+ * @since 2026-08-13
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@TableName("fhzg_se_block_recon_log")
+public class FhzgSeBlockReconLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键(自增补充列,起点 1 亿防与事件表撞车;同时作为重构队列 eventId 传给 Listener) */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 成片事件ID(中台生成,幂等键) */
+    private String eventId;
+
+    /** 馈线ID */
+    private String feederId;
+
+    /** 重构状态: 0未触发 1运行中 2已完成 3无方案 -1失败 */
+    private Integer reconStatus;
+
+    /** 重构断面时刻(成片开始时间) */
+    private LocalDateTime pointTime;
+
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 120 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeBlockVoltageEvent.java

@@ -0,0 +1,120 @@
+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;
+
+/**
+ * 馈线级成片电压越限事件(中台 T-2 聚合结果,DI 同步全量快照)
+ * <p>
+ * 数据来源:中台 ODPS 任务 un4064_02_..._feeder_over_limit_event(ds=T-2 分区)
+ * 语义:同馈线 + 同日 + 越限时间区间有交集的配变(≥2台) → 一次成片事件
+ * 与实时事件表 fhzg_se_voltage_event 物理隔离(实时=状估监测,成片=中台台区聚合,理论不重合)
+ * <p>
+ * 同步方式:中台 DI 同步(ODPS 表结构原样同步到 MySQL,不带 id 列)。
+ * 本地 id 为自增补充列,建表时 AUTO_INCREMENT 从 1 亿起步——
+ * 防与 fhzg_se_current_event / fhzg_se_voltage_event(AUTO 自增小值)撞车:
+ * 前端按 alarmEventId 调 queryAlarmDetail 时按 current→voltage→recon_log 顺序查表,
+ * 若本表/recon_log 用小自增 id 会命中错误事件详情。
+ *
+ * @author lsl
+ * @since 2026-08-13
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@TableName("fhzg_se_block_voltage_event")
+public class FhzgSeBlockVoltageEvent implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键(本地自增补充列,AUTO_INCREMENT 起点 1 亿防与事件表撞车;中台 DI 不带此列) */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 成片事件ID(中台生成,唯一键:maintainer_feeder_date_group) */
+    private String eventId;
+
+    private String provinceId;
+    private String provinceName;
+    private String cityOrgId;
+    private String cityOrgName;
+
+    /** 运维单位ID(即 county_id) */
+    private String maintainerId;
+    private String maintainerName;
+
+    private String substationId;
+    private String substationName;
+
+    private String feederId;
+    private String feederName;
+
+    /** 越限日期 yyyymmdd */
+    private String overLimitDate;
+
+    /** 成片开始时间(片内最早越限) */
+    private LocalDateTime eventStartTime;
+
+    /** 成片结束时间(片内最晚越限) */
+    private LocalDateTime eventEndTime;
+
+    /** 成片持续分钟 */
+    private Integer eventDurationMinutes;
+
+    /** 成片配变数量 */
+    private Integer memberTransCount;
+
+    /** 片内配变越限时长合计(分钟) */
+    private Integer totalOverLimitMinutes;
+
+    /** 片内明细事件条数 */
+    private Integer memberEventCount;
+
+    /** 成员配变名称列表(; 分隔) */
+    private String memberTransformerList;
+
+    /** 成员明细事件ID列表(; 分隔) */
+    private String memberEventIds;
+
+    /** 成员配变越限时段列表 */
+    private String memberTimeRanges;
+
+    /** 严重标识: 1=片内存在严重越限配变(voltage_under_index_severe>0) 0=一般 */
+    private Integer severeFlag;
+
+    /** 片内严重越限配变数量 */
+    private Integer severeTransCount;
+
+    /** 越限成因 */
+    private String overLimitCause;
+
+    /** 治理措施 */
+    private String controlMeasures;
+
+    /** 影响用户数 */
+    private Integer effectCustNum;
+
+    /** 越限类型(01=越下限 02=越上限) */
+    private String overLimitType;
+
+    /** 溯源层级(02=馈线级成片) */
+    private String traceabilityLevel;
+
+    /** 数据质量标识(00=正常) */
+    private String isDataQualityIssue;
+
+    /** 中台创建时间(ODPS ctime) */
+    private LocalDateTime ctime;
+
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 7 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeBlockReconLogMapper.java

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

+ 7 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeBlockVoltageEventMapper.java

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

+ 1 - 1
common/ruoyi-common-swagger/src/main/java/com/ovit/config/SwaggerConfig.java

@@ -31,7 +31,7 @@ public class SwaggerConfig {
         return new Docket(DocumentationType.SWAGGER_2)
                 .apiInfo(getApiInfo())
                 .select()
-                .apis(RequestHandlerSelectors.basePackage("com.ruoyi")
+                .apis(RequestHandlerSelectors.basePackage("com.hdkj")
                 )
                 .paths(PathSelectors.any())
                 .build();

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

@@ -18,6 +18,7 @@ import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.scheduler.SeCapacityDailyScheduler;
 import com.hdkj.lt.bf.scheduler.SeConsumerCountDailyScheduler;
 import com.hdkj.lt.bf.scheduler.SeLineLossDailyScheduler;
+import com.hdkj.lt.bf.service.SeBlockVoltageEventService;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.hdkj.lt.bf.service.SeIndicatorService;
 import com.hdkj.lt.bf.service.SeSnapshotService;
@@ -57,6 +58,7 @@ public class IndicatorController extends BaseController {
     private final SeCapacityDailyScheduler seCapacityDailyScheduler;
     private final SeConsumerCountDailyScheduler seConsumerCountDailyScheduler;
     private final SeLineLossDailyScheduler seLineLossDailyScheduler;
+    private final SeBlockVoltageEventService seBlockVoltageEventService;
 
     /**
      * 停电时户数(无时间维度切换)
@@ -328,6 +330,24 @@ public class IndicatorController extends BaseController {
         }
     }
 
+    /**
+     * 自动扫描成片电压事件并触发重构(供 pwfhzg-job 定时任务调用)
+     * <p>
+     * 增量自动触发:扫描中台推送的成片事件,recon_log 无记录的登记并发布入队。
+     * 手动触发不走这里——前端直接调 SI /recon/trigger(eventId=recon_log.id + feeder_id + point_time)。
+     */
+    @PostMapping("/block-voltage/recon-scan")
+    public ApiResponse<String> scanBlockVoltageRecon() {
+        log.info("[成片电压重构] 自动扫描触发");
+        try {
+            int triggered = seBlockVoltageEventService.scanAndTrigger();
+            return ApiResponse.success("扫描完成, 触发重构 " + triggered + " 条");
+        } catch (Exception e) {
+            log.error("[成片电压重构] 自动扫描异常", e);
+            return ApiResponse.fail("执行异常: " + e.getMessage());
+        }
+    }
+
     /**
      * 手动触发线路线损日汇总刷数
      * <p>

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

@@ -34,7 +34,7 @@ public class ReconQueueMsg implements Serializable {
     /** 变电站ID */
     private String subsId;
 
-    /** 告警类型(current_overload / current_heavy / voltage_over / voltage_under) */
+    /** 告警类型(current_overload / current_heavy / voltage_over / voltage_under / block_voltage_under) */
     private String alarmType;
 
     /** 持续1小时的那个起始断面时刻(yyyy-MM-dd HH:mm:ss) */

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

@@ -76,4 +76,24 @@ public class SeAlarmDetailVO implements Serializable {
 
     @ApiModelProperty(value = "重构方案状态:0=未触发 1=生成中 2=有方案 3=无方案 -1=失败")
     private Integer reconStatus;
+
+    // === 成片电压越限专用(中台T-2聚合)===
+
+    @ApiModelProperty(value = "成片配变数量")
+    private Integer memberTransCount;
+
+    @ApiModelProperty(value = "成员配变名称列表(; 分隔)")
+    private String memberTransformerList;
+
+    @ApiModelProperty(value = "成员配变越限时段列表")
+    private String memberTimeRanges;
+
+    @ApiModelProperty(value = "影响用户数")
+    private Integer effectCustNum;
+
+    @ApiModelProperty(value = "越限成因")
+    private String overLimitCause;
+
+    @ApiModelProperty(value = "治理措施")
+    private String controlMeasures;
 }

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

@@ -1,34 +1,25 @@
 package com.hdkj.lt.bf.listener;
 
-import cn.hutool.core.lang.Snowflake;
-import cn.hutool.core.util.IdUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.hdkj.hussar.ApiResponse;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
-import com.hdkj.lt.bf.entity.FhzgSeEventStateEstimation;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.entity.FhzgSeBlockReconLog;
 import com.hdkj.lt.bf.entity.dto.ReconQueueMsg;
 import com.hdkj.lt.bf.entity.vo.XlRelVO;
 import com.hdkj.lt.bf.event.ReconTriggerEvent;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
-import com.hdkj.lt.bf.mapper.FhzgSeEventStateEstimationMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeBlockReconLogMapper;
 import com.hdkj.lt.bf.mapper.XlRelMapper;
 import com.hdkj.lt.bf.entity.dto.XlRelDTO;
-import com.hdkj.lt.bf.service.FhzgSeEventStateEstimationService;
-import com.hdkj.lt.core.bizms.modle.dto.StateEstimation;
 import com.hdkj.lt.core.bizms.modle.po.DwdShbDsFeederBase;
 import com.hdkj.lt.core.bizms.modle.po.Jxz;
-import com.hdkj.lt.core.bizms.modle.request.StateEstimationRequest;
 import com.hdkj.lt.core.sys.dao.DwdShbDsFeederBaseMapper;
 import com.hdkj.lt.core.sys.dao.JxzMapper;
 import com.hdkj.lt.feign.IReconApiClient;
 import com.hdkj.lt.feign.ISimulationPlatformApiClient;
-import com.hdkj.lt.feign.IStateEstimationApiClient;
 import com.hdkj.lt.modle.dto.simulation.EacFeederDTO;
 import com.hdkj.lt.modle.dto.simulation.OperationModeDTO;
 import com.hdkj.lt.modle.dto.simulation.OperationModeStartDTO;
@@ -36,7 +27,6 @@ import com.hdkj.lt.modle.dto.simulation.SwitchFeederDTO;
 import com.hdkj.lt.modle.vo.recon.ReconstructionParam;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.compress.utils.Lists;
 import org.springframework.context.event.EventListener;
 import org.springframework.data.redis.connection.stream.Consumer;
 import org.springframework.data.redis.connection.stream.MapRecord;
@@ -91,12 +81,11 @@ public class ReconTriggerListener {
     private final DwdShbDsFeederBaseMapper feederBaseMapper;
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final FhzgSeBlockReconLogMapper blockReconLogMapper;
     private final RedisTemplate<String, Object> redisTemplate;
-    private final FhzgSeEventStateEstimationMapper stateEstimationMapper;
-    private final FhzgSeEventStateEstimationService seEventStateEstimationService;
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-    private final ObjectMapper writeMapper = new ObjectMapper();
+
     private static final int RECON_RUNNING   = 1;
     private static final int RECON_COMPLETED = 2;
     private static final int RECON_NO_PLAN   = 3;
@@ -290,8 +279,6 @@ public class ReconTriggerListener {
     // ============================================================
 
     private void handleReconTrigger(ReconTriggerEvent event) {
-        // 保存状估数据
-        saveStateEstimation(event);
         // 幂等保护:事件已完成重构(有方案/无方案)则跳过,防重投/重复触发重复调用重构接口
         Integer doneStatus = queryReconStatus(event.getEventId(), event.getAlarmType());
         if (doneStatus != null && (doneStatus == RECON_COMPLETED || doneStatus == RECON_NO_PLAN)) {
@@ -313,7 +300,6 @@ public class ReconTriggerListener {
         log.info("重构触发: eventId={}, feederId={}, alarmType={}, pointTime={}",
                 event.getEventId(), event.getFeederId(), event.getAlarmType(), event.getPointTime());
 
-
         // 异步调用求解器,不阻塞后续重构请求
         CompletableFuture.runAsync(() -> callSolver(event));
 
@@ -445,7 +431,10 @@ public class ReconTriggerListener {
     private Integer queryReconStatus(Long eventId, String alarmType) {
         if (eventId == null) return null;
         try {
-            if (alarmType != null && alarmType.startsWith("current_")) {
+            if (alarmType != null && alarmType.startsWith("block_")) {
+                FhzgSeBlockReconLog cur = blockReconLogMapper.selectById(eventId);
+                return cur != null ? cur.getReconStatus() : null;
+            } else if (alarmType != null && alarmType.startsWith("current_")) {
                 FhzgSeCurrentEvent cur = currentEventMapper.selectById(eventId);
                 return cur != null ? cur.getReconStatus() : null;
             } else if (alarmType != null && alarmType.startsWith("voltage_")) {
@@ -461,7 +450,13 @@ public class ReconTriggerListener {
     private void updateReconStatus(Long eventId, String alarmType, int reconStatus) {
         if (eventId == null) return;
         try {
-            if (alarmType != null && alarmType.startsWith("current_")) {
+            if (alarmType != null && alarmType.startsWith("block_")) {
+                FhzgSeBlockReconLog update = new FhzgSeBlockReconLog();
+                update.setId(eventId);
+                update.setReconStatus(reconStatus);
+                update.setUpdateTime(LocalDateTime.now());
+                blockReconLogMapper.updateById(update);
+            } else if (alarmType != null && alarmType.startsWith("current_")) {
                 FhzgSeCurrentEvent update = new FhzgSeCurrentEvent();
                 update.setId(eventId);
                 update.setReconStatus(reconStatus);
@@ -505,24 +500,4 @@ public class ReconTriggerListener {
                 msg.getCountyId(), msg.getSubsId(), msg.getAlarmType(),
                 pointTime, triggerTime);
     }
-    /**
-     * 状估数据保存
-     * @param event
-     */
-    private void saveStateEstimation(ReconTriggerEvent event){
-        //
-        log.info("=========状估数据=========");
-        Long selectCount = stateEstimationMapper.selectCount(new LambdaQueryWrapper<FhzgSeEventStateEstimation>()
-                .eq(FhzgSeEventStateEstimation::getEventId, event.getEventId())
-                .eq(FhzgSeEventStateEstimation::getAlarmType, event.getAlarmType()));
-        log.info("eventId={}, 状估数据有【{}】条", event.getEventId(), selectCount);
-        if(selectCount == null || selectCount <= 0) {
-            CompletableFuture.runAsync(() -> {
-                FhzgSeEventStateEstimation estimation = new FhzgSeEventStateEstimation();
-                estimation.setEventId(event.getEventId()).setFeederId(event.getFeederId())
-                        .setTriggerTime(event.getTriggerTime().format(DT_FMT)).setAlarmType(event.getAlarmType());
-                seEventStateEstimationService.saveStateEstimation(estimation);
-            });
-        }
-    }
 }

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

@@ -0,0 +1,128 @@
+package com.hdkj.lt.bf.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.hdkj.lt.bf.entity.FhzgSeBlockReconLog;
+import com.hdkj.lt.bf.entity.FhzgSeBlockVoltageEvent;
+import com.hdkj.lt.bf.event.ReconTriggerEvent;
+import com.hdkj.lt.bf.mapper.FhzgSeBlockReconLogMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeBlockVoltageEventMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 成片电压越限事件:扫描 + 触发重构
+ * <p>
+ * 中台全量快照先删后推落到 fhzg_se_block_voltage_event(本地只读展示)。
+ * 重构状态记录在 fhzg_se_block_recon_log(本地自维护,中台不碰)。
+ * <p>
+ * 触发入口:
+ * - 自动:pwfhzg-job 定时任务通过 Feign 调 bf /indicator/block-voltage/recon-scan(本方法 scanAndTrigger)
+ * - 手动:前端直接调 SI /recon/trigger(eventId=recon_log.id + feeder_id + point_time),不走本服务
+ * <p>
+ * 幂等:recon_log 以中台生成的 event_id 为唯一键,中台重推全量不重复触发。
+ * 首次刷入的历史成片由脚本置 recon_status=4(存档不自动触发),本扫描同样跳过(recon_log 已存在)。
+ *
+ * @author lsl
+ * @since 2026-08-13
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SeBlockVoltageEventService {
+
+    private final FhzgSeBlockVoltageEventMapper blockVoltageEventMapper;
+    private final FhzgSeBlockReconLogMapper blockReconLogMapper;
+    private final ApplicationEventPublisher eventPublisher;
+
+    /** 成片事件告警类型(block_ 前缀,Listener 据此分流到成片重构状态表) */
+    public static final String ALARM_TYPE_BLOCK_UNDER = "block_voltage_under";
+    /** 重构状态:0=未触发 1=运行中 4=历史存档不自动触发(首次刷入脚本标记,可手动触发) */
+    private static final int RECON_NOT_TRIGGERED = 0;
+    private static final int RECON_RUNNING = 1;
+
+    /**
+     * 扫描中台推送的成片数据并触发未处理的重构
+     *
+     * @return 本次触发条数
+     */
+    public int scanAndTrigger() {
+        List<FhzgSeBlockVoltageEvent> all = blockVoltageEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeBlockVoltageEvent>()
+                        .isNotNull(FhzgSeBlockVoltageEvent::getEventId));
+        if (all == null || all.isEmpty()) {
+            return 0;
+        }
+        int triggered = 0;
+        for (FhzgSeBlockVoltageEvent e : all) {
+            if (triggerRecon(e)) {
+                triggered++;
+            }
+        }
+        log.info("成片事件扫描完成: 业务表 {} 条, 触发重构 {} 条", all.size(), triggered);
+        return triggered;
+    }
+
+    /**
+     * 单条成片事件触发重构(按 event_id 幂等,中台重推全量不重复触发)
+     */
+    private boolean triggerRecon(FhzgSeBlockVoltageEvent e) {
+        try {
+            // 1. 幂等:recon_log 已存在该 event_id → 已处理过(含处理中/存档/失败),跳过
+            FhzgSeBlockReconLog existing = blockReconLogMapper.selectOne(
+                    new LambdaQueryWrapper<FhzgSeBlockReconLog>()
+                            .eq(FhzgSeBlockReconLog::getEventId, e.getEventId()));
+            if (existing != null) {
+                return false;
+            }
+
+            // 2. 登记 recon_log(状态0=未触发,pointTime=成片开始时间)
+            FhzgSeBlockReconLog logRecord = new FhzgSeBlockReconLog();
+            logRecord.setEventId(e.getEventId());
+            logRecord.setFeederId(e.getFeederId());
+            logRecord.setReconStatus(RECON_NOT_TRIGGERED);
+            logRecord.setPointTime(e.getEventStartTime());
+            logRecord.setCreateTime(LocalDateTime.now());
+            logRecord.setUpdateTime(LocalDateTime.now());
+            blockReconLogMapper.insert(logRecord);
+
+            // 3. 发布重构触发(含防重置1)
+            return publishRecon(logRecord, e);
+        } catch (Exception ex) {
+            log.error("成片事件触发重构失败: eventId={}, feederId={}, error={}",
+                    e.getEventId(), e.getFeederId(), ex.getMessage(), ex);
+            return false;
+        }
+    }
+
+    /**
+     * 发布重构触发事件(bf 进程内 @EventListener 接收后 XADD 入队)
+     * 防重:先置运行中,避免扫描与 Listener 竞态重复触发
+     */
+    private boolean publishRecon(FhzgSeBlockReconLog logRecord, FhzgSeBlockVoltageEvent e) {
+        try {
+            FhzgSeBlockReconLog mark = new FhzgSeBlockReconLog();
+            mark.setId(logRecord.getId());
+            mark.setReconStatus(RECON_RUNNING);
+            mark.setUpdateTime(LocalDateTime.now());
+            blockReconLogMapper.updateById(mark);
+
+            eventPublisher.publishEvent(new ReconTriggerEvent(
+                    logRecord.getId(), e.getFeederId(), e.getFeederName(),
+                    e.getMaintainerId(), e.getSubstationId(),
+                    ALARM_TYPE_BLOCK_UNDER, e.getEventStartTime(), LocalDateTime.now()));
+
+            log.info("成片事件触发重构: eventId={}, reconLogId={}, feederId={}, pointTime={}",
+                    e.getEventId(), logRecord.getId(), e.getFeederId(), e.getEventStartTime());
+            return true;
+        } catch (Exception ex) {
+            log.error("成片事件触发重构失败: reconLogId={}, eventId={}, error={}",
+                    logRecord.getId(), e.getEventId(), ex.getMessage(), ex);
+            return false;
+        }
+    }
+}

+ 50 - 22
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/FhzgSeEventStateEstimationServiceImpl.java

@@ -12,8 +12,10 @@ import com.hdkj.hussar.ApiResponse;
 import com.hdkj.lt.base.constants.CommonConstant;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeEventStateEstimation;
+import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeEventStateEstimationMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
 import com.hdkj.lt.bf.service.FhzgSeEventStateEstimationService;
 import com.hdkj.lt.core.bizms.modle.dto.StateEstimation;
 import com.hdkj.lt.core.bizms.modle.po.Jxz;
@@ -31,10 +33,7 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -51,6 +50,7 @@ public class FhzgSeEventStateEstimationServiceImpl extends ServiceImpl<FhzgSeEve
     private final JxzMapper jxzMapper;
     private final IStateEstimationApiClient stateEstimationApiClient;
     private final FhzgSeCurrentEventMapper currentEventMapper;
+    private final FhzgSeVoltageEventMapper voltageEventMapper;
     private static final int RETRY_COUNT = 2;
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     private final ObjectMapper writeMapper = new ObjectMapper();
@@ -76,18 +76,8 @@ public class FhzgSeEventStateEstimationServiceImpl extends ServiceImpl<FhzgSeEve
         List<FhzgSeEventStateEstimation> stateEstimationList = Lists.newArrayList();
         for (String feeder : groupFeederIds) {
             StateEstimationRequest req = buildStateEstimationReq(feeder,estimation.getTriggerTime());
-//            StateEstimation stateEstimation = callWithRetry(feeder, req);
             StateEstimation stateEstimation = stateEstimationApiClient.queryByFeeder(req).getData();
-//            try{
-//                String str = StreamUtils.copyToString(new ClassPathResource("15DKX-11729.txt").getInputStream(), StandardCharsets.UTF_8);
-//                JSON.parseObject(str,StateEstimation.class);
-//                JsonNode root = writeMapper.readTree(str);
-//                JsonNode data = root.get("data");
-//                stateEstimation = writeMapper.treeToValue(data,StateEstimation.class);
-//            }catch (IOException e) {
-//                throw new RuntimeException(e);
-//            }
-            log.info("response = {}", JSON.toJSONString(stateEstimation));
+//            log.info("response = {}", JSON.toJSONString(stateEstimation));
             if(Objects.nonNull(stateEstimation)){
                 FhzgSeEventStateEstimation entity = new FhzgSeEventStateEstimation();
                 entity.setId(snowflake.nextId()).setEventId(eventId).setFeederId(feeder).setAlarmType(estimation.getAlarmType())
@@ -102,7 +92,7 @@ public class FhzgSeEventStateEstimationServiceImpl extends ServiceImpl<FhzgSeEve
                 stateEstimationList.add(entity);
                 try {
                     this.save(entity);
-                    log.info("馈线={} ,事件={} ,状估数据 保存成功!", feeder, eventId);
+//                    log.info("馈线={} ,事件={} ,状估数据 保存成功!", feeder, eventId);
                 } catch (Exception e) {
                     log.error("馈线={} ,事件={} ,落库失败: {}", feeder, eventId, e.getMessage(), e);
                 }
@@ -113,14 +103,52 @@ public class FhzgSeEventStateEstimationServiceImpl extends ServiceImpl<FhzgSeEve
 
     @Override
     public void saveByList() {
+        // 1. 查询近7天的重过载事件(电流 + 电压)
         LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
-        List<FhzgSeCurrentEvent> list = currentEventMapper.selectList(new LambdaQueryWrapper<FhzgSeCurrentEvent>().ge(FhzgSeCurrentEvent::getTriggerTime, sevenDaysAgo));
-        for (FhzgSeCurrentEvent currentEvent : list) {
-            FhzgSeEventStateEstimation estimation = new FhzgSeEventStateEstimation();
-            estimation.setEventId(currentEvent.getId()).setFeederId(currentEvent.getFeederId())
-                    .setTriggerTime(currentEvent.getTriggerTime().format(DT_FMT)).setAlarmType(currentEvent.getAlarmType());
-            saveStateEstimation(estimation);
+        List<FhzgSeCurrentEvent> currentEvents = currentEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeCurrentEvent>().ge(FhzgSeCurrentEvent::getTriggerTime, sevenDaysAgo));
+        List<FhzgSeVoltageEvent> voltageEvents = voltageEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeVoltageEvent>().ge(FhzgSeVoltageEvent::getTriggerTime, sevenDaysAgo));
+
+        // 2. 汇总事件ID;为空则直接返回(同时避免 IN () 空集合导致的SQL异常)
+        List<Long> eventIdList = new ArrayList<>(currentEvents.size() + voltageEvents.size());
+        currentEvents.forEach(e -> eventIdList.add(e.getId()));
+        voltageEvents.forEach(e -> eventIdList.add(e.getId()));
+        if (eventIdList.isEmpty()) {
+            log.info("近7天无重过载事件,跳过状估数据保存");
+            return;
+        }
+
+        // 3. 查询已存在状估记录的事件ID。该集合 ⊆ eventIdList,可直接用于 O(1) 判定,无需再求交集
+        Set<Long> existingEventIds = this.list(new LambdaQueryWrapper<FhzgSeEventStateEstimation>()
+                        .in(FhzgSeEventStateEstimation::getEventId, eventIdList)).stream()
+                .map(FhzgSeEventStateEstimation::getEventId)
+                .collect(Collectors.toSet());
+
+        // 4. 对已存在状估记录的事件重新保存状估数据
+        for (FhzgSeCurrentEvent event : currentEvents) {
+            if (existingEventIds.contains(event.getId())) {
+                saveStateEstimation(buildEstimation(event.getId(), event.getFeederId(),
+                        event.getTriggerTime(), event.getAlarmType()));
+            }
         }
+        for (FhzgSeVoltageEvent event : voltageEvents) {
+            if (existingEventIds.contains(event.getId())) {
+                saveStateEstimation(buildEstimation(event.getId(), event.getFeederId(),
+                        event.getTriggerTime(), event.getAlarmType()));
+            }
+        }
+    }
+
+    /**
+     * 构建状估保存参数(triggerTime 统一格式化为字符串)
+     */
+    private FhzgSeEventStateEstimation buildEstimation(Long eventId, String feederId,
+                                                       LocalDateTime triggerTime, String alarmType) {
+        FhzgSeEventStateEstimation estimation = new FhzgSeEventStateEstimation();
+        estimation.setEventId(eventId).setFeederId(feederId)
+                .setTriggerTime(triggerTime.format(DT_FMT)).setAlarmType(alarmType);
+        return estimation;
     }
 
     /**

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

@@ -8,6 +8,8 @@ import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
+import com.hdkj.lt.bf.entity.FhzgSeBlockVoltageEvent;
+import com.hdkj.lt.bf.entity.FhzgSeBlockReconLog;
 import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
 import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
@@ -17,6 +19,8 @@ import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeBlockVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeBlockReconLogMapper;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
@@ -45,6 +49,8 @@ public class SeAlarmServiceImpl implements SeAlarmService {
 
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeVoltageEventMapper voltageEventMapper;
+    private final FhzgSeBlockVoltageEventMapper blockVoltageEventMapper;
+    private final FhzgSeBlockReconLogMapper blockReconLogMapper;
     private final FeederGroupApplication feederGroupApplication;
     private final FhzgSeMonitorCurrentMapper monitorCurrentMapper;
     private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
@@ -124,23 +130,57 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         LocalDateTime rangeStart = start.atStartOfDay();
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
 
-        Page<FhzgSeVoltageEvent> mpPage = new Page<>(req.getPage(), req.getPageSize());
-        LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
-                .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
-                .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
-                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder".equals(req.getAlarmLevel()) ? "feeder" : "mvtrans");
+        String alarmLevel = "feeder".equals(req.getAlarmLevel()) ? "feeder" : "mvtrans";
 
-        applyVoltageFilter(wrapper, req.getId(), req.getType());
-        if (StringUtils.isNotBlank(req.getFeederName())) {
-            wrapper.like(FhzgSeVoltageEvent::getFeederName, req.getFeederName());
-        }
-        applyOrderBy(wrapper, req.getOrderByTime(), FhzgSeVoltageEvent::getFirstOverTime);
-
-        Page<FhzgSeVoltageEvent> result = voltageEventMapper.selectPage(mpPage, wrapper);
-        List<SeAlarmListVO> voList = result.getRecords().stream()
+        // 1. 实时电压事件(事件表,不分页全量拉取时间窗)
+        List<FhzgSeVoltageEvent> volEvents = voltageEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                        .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
+                        .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
+                        .eq(FhzgSeVoltageEvent::getAlarmLevel, alarmLevel)
+                        .and(w -> applyVoltageFilter(w, req.getId(), req.getType()))
+                        .like(StringUtils.isNotBlank(req.getFeederName()),
+                                FhzgSeVoltageEvent::getFeederName, req.getFeederName()));
+        List<SeAlarmListVO> voList = volEvents.stream()
                 .map(this::toVoltageVO)
                 .collect(Collectors.toList());
-        return PageResult.of(result, voList);
+
+        // 2. 成片电压事件(仅 feeder 级时合并,中台T-2聚合结果)
+        if ("feeder".equals(alarmLevel)) {
+            List<FhzgSeBlockVoltageEvent> blockEvents = blockVoltageEventMapper.selectList(
+                    new LambdaQueryWrapper<FhzgSeBlockVoltageEvent>()
+                            .ge(FhzgSeBlockVoltageEvent::getEventStartTime, rangeStart)
+                            .lt(FhzgSeBlockVoltageEvent::getEventStartTime, rangeEnd)
+                            .and(w -> applyBlockVoltageFilter(w, req.getId(), req.getType()))
+                            .like(StringUtils.isNotBlank(req.getFeederName()),
+                                    FhzgSeBlockVoltageEvent::getFeederName, req.getFeederName()));
+            // 批量关联 recon_log:alarmEventId 用稳定 log id(业务表 id 随中台重推变化,不能作跳转键)
+            Map<String, Long> logIdByEventId = queryBlockReconLogIds(blockEvents);
+            voList.addAll(blockEvents.stream()
+                    .map(e -> toBlockVoltageVO(e, logIdByEventId))
+                    .collect(Collectors.toList()));
+        }
+
+        // 3. 合并后统一排序(时间字段均为 yyyy-MM-dd HH:mm:ss,字典序=时间序)
+        Comparator<SeAlarmListVO> byTime = Comparator.comparing(SeAlarmListVO::getAlarmTime,
+                Comparator.nullsLast(String::compareTo));
+        voList.sort("asc".equalsIgnoreCase(req.getOrderByTime()) ? byTime : byTime.reversed());
+
+        // 4. 手工分页
+        long total = voList.size();
+        int fromIndex = (int) ((req.getPage() - 1) * req.getPageSize());
+        int toIndex = (int) Math.min(total, (long) req.getPage() * req.getPageSize());
+        List<SeAlarmListVO> pageList = fromIndex >= total
+                ? Collections.emptyList()
+                : new ArrayList<>(voList.subList(fromIndex, toIndex));
+
+        return PageResult.<SeAlarmListVO>builder()
+                .records(pageList)
+                .total(total)
+                .pages((total + req.getPageSize() - 1) / req.getPageSize())
+                .current(req.getPage())
+                .pageSize(req.getPageSize())
+                .build();
     }
 
     private SeAlarmListVO toVoltageVO(FhzgSeVoltageEvent e) {
@@ -219,9 +259,49 @@ public class SeAlarmServiceImpl implements SeAlarmService {
             return buildFeederDetail(voltageEvent);
         }
 
+        // 成片电压事件(alarmEventId 为 recon_log.id——中台重推业务表 id 会变,前端跳转用稳定 log id)
+        FhzgSeBlockReconLog blockLog = blockReconLogMapper.selectById(alarmEventId);
+        if (blockLog != null) {
+            FhzgSeBlockVoltageEvent blockEvent = blockVoltageEventMapper.selectOne(
+                    new LambdaQueryWrapper<FhzgSeBlockVoltageEvent>()
+                            .eq(FhzgSeBlockVoltageEvent::getEventId, blockLog.getEventId())
+                            .last("LIMIT 1"));
+            if (blockEvent != null) {
+                return buildBlockDetail(blockEvent, blockLog);
+            }
+        }
+
         return null;
     }
 
+    /**
+     * 成片电压事件详情:直接使用中台聚合结果(T-2 历史,无断面快照)
+     * reconStatus 从传入的 recon_log 取(中台快照表无此字段)
+     * abnormalIndicator/alarmType 按 over_limit_type 映射成与实时电压事件一致(不做特化)
+     */
+    private SeAlarmDetailVO buildBlockDetail(FhzgSeBlockVoltageEvent e, FhzgSeBlockReconLog logRecord) {
+        Integer reconStatus = logRecord != null ? logRecord.getReconStatus() : 0;
+        String alarmType = resolveBlockVoltageAlarmType(e);
+        return SeAlarmDetailVO.builder()
+                .alarmEventId(logRecord != null ? logRecord.getId() : e.getId())
+                .alarmType(alarmType)
+                .alarmCategory("voltage")
+                .countyName(e.getMaintainerName())
+                .feederName(e.getFeederName())
+                .alarmStartTime(e.getEventStartTime())
+                .alarmEndTime(e.getEventEndTime())
+                .abnormalIndicator(resolveVoltageDisplay(alarmType, "feeder"))
+                .overLowerMvtransCount(e.getMemberTransCount())
+                .reconStatus(reconStatus)
+                .memberTransCount(e.getMemberTransCount())
+                .memberTransformerList(e.getMemberTransformerList())
+                .memberTimeRanges(e.getMemberTimeRanges())
+                .effectCustNum(e.getEffectCustNum())
+                .overLimitCause(e.getOverLimitCause())
+                .controlMeasures(e.getControlMeasures())
+                .build();
+    }
+
     /**
      * 线路事件详情:展示电压范围 + 窗口内配变统计
      */
@@ -346,6 +426,77 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         else if (type == 4) wrapper.eq(FhzgSeVoltageEvent::getFeederId, id);
     }
 
+    /**
+     * 成片事件节点过滤(与事件表对应:county=maintainer, subs=substation, feeder=feeder)
+     */
+    private void applyBlockVoltageFilter(LambdaQueryWrapper<FhzgSeBlockVoltageEvent> wrapper,
+                                          String id, Integer type) {
+        if (type == 2) wrapper.eq(FhzgSeBlockVoltageEvent::getMaintainerId, id);
+        else if (type == 3) wrapper.eq(FhzgSeBlockVoltageEvent::getSubstationId, id);
+        else if (type == 4) wrapper.eq(FhzgSeBlockVoltageEvent::getFeederId, id);
+    }
+
+    /**
+     * 成片事件列表行:展示方向(越上限/越下限)+ 影响用户数
+     * 实体字段无 alarmType/alarmLevel/status(中台快照表),按 over_limit_type 映射成
+     * 与实时电压事件一致的 alarmType(voltage_under/voltage_over),前端无需特化处理。
+     * alarmEventId 用 recon_log.id(稳定,中台重推业务表 id 会变,不能用)
+     */
+    private SeAlarmListVO toBlockVoltageVO(FhzgSeBlockVoltageEvent e, Map<String, Long> logIdByEventId) {
+        Long stableId = e.getEventId() != null ? logIdByEventId.get(e.getEventId()) : null;
+        String alarmType = resolveBlockVoltageAlarmType(e);
+        return SeAlarmListVO.builder()
+                .alarmEventId(stableId != null ? stableId : e.getId())
+                .deviceName(e.getFeederName())
+                .feederName(e.getFeederName())
+                .feederId(e.getFeederId())
+                .alarmDisplayValue(resolveVoltageDirection(alarmType))
+                .alarmTime(e.getEventStartTime() != null ? e.getEventStartTime().format(DT_FMT) : null)
+                .alarmDuration(calcDuration(e.getEventStartTime(), e.getEventEndTime(), STATUS_RECOVERED))
+                .alarmType(alarmType)
+                .alarmLevel("feeder")
+                .affectedConsumerCount(e.getEffectCustNum())
+                .build();
+    }
+
+    /**
+     * 成片事件 alarmType:按 over_limit_type 映射成与实时电压事件一致的值
+     * (01=越下限 → voltage_under,02=越上限 → voltage_over),方向展示直接复用
+     * resolveVoltageDirection/resolveVoltageDisplay,不做特化。
+     */
+    private String resolveBlockVoltageAlarmType(FhzgSeBlockVoltageEvent e) {
+        if (e != null && "02".equals(e.getOverLimitType())) {
+            return "voltage_over";
+        }
+        return "voltage_under";
+    }
+
+    /**
+     * 批量查成片 recon_log id 映射(eventId → log.id),未登记(未触发)的成片不在映射中
+     */
+    private Map<String, Long> queryBlockReconLogIds(List<FhzgSeBlockVoltageEvent> blockEvents) {
+        Map<String, Long> map = new HashMap<>();
+        if (blockEvents == null || blockEvents.isEmpty()) return map;
+        List<String> eventIds = blockEvents.stream()
+                .map(FhzgSeBlockVoltageEvent::getEventId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (eventIds.isEmpty()) return map;
+        List<FhzgSeBlockReconLog> logs = blockReconLogMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeBlockReconLog>()
+                        .in(FhzgSeBlockReconLog::getEventId, eventIds));
+        if (logs != null) {
+            for (FhzgSeBlockReconLog logRecord : logs) {
+                map.put(logRecord.getEventId(), logRecord.getId());
+            }
+        }
+        return map;
+    }
+
+    /** 成片事件状态:中台快照均为已恢复历史数据 */
+    private static final int STATUS_RECOVERED = 2;
+
     // ============================================================
     // 线路下拉列表
     // ============================================================

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

@@ -360,6 +360,12 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             }
         }
 
+        // ===== 3. 成片电压事件不并入统计 =====
+        // 口径说明(用户复核确认 2026-08-13):
+        // 历史统计查中台用户越限事件表(ads_mrt_voltage_over_limit_daily_result),与我们的事件表完全无关;
+        // 成片电压事件并入的是"事件"(列表/详情/重构),不补历史统计(历史统计不走我们的事件表),
+        // 也进不了今日统计(中台 T-2 滞后)。故统计保持原样,成片只进事件链路。
+
         return SeVoltageDashboardVO.builder()
                 .overUpperSevereTgCount(overUpperSevereTg)
                 .overUpperSevereCustCount(overUpperSevereCust)

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

@@ -115,6 +115,10 @@
         ) AS tmp ON td.outage_feeder_name = tmp.c
         where td.outage_stime &gt; #{startOfMonth}
         and td.outage_stime &lt; #{endOfToday}
+        and td.data_version = 2
+        and td.operate_flag in ('0','1','2')
+        and td.is_confirmed = '1'
+        and td.report_status = '1'
     </select>
     <select id="selectOutageAggregation" resultType="com.hdkj.lt.bf.entity.dto.OutageAggregationDTO">
         SELECT
@@ -128,5 +132,9 @@
         ) AS tmp ON td.outage_feeder_name = tmp.c
         where td.outage_stime &gt;= #{startTime}
         and td.outage_stime &lt; #{endTime}
+        and td.data_version = 2
+        and td.operate_flag in ('0','1','2')
+        and td.is_confirmed = '1'
+        and td.report_status = '1'
     </select>
 </mapper>

+ 35 - 0
services/ruoyi-job/src/main/java/com/hdkj/lt/job/task/SeBlockVoltageReconTask.java

@@ -0,0 +1,35 @@
+package com.hdkj.lt.job.task;
+
+import com.hdkj.hussar.ApiResponse;
+import com.hdkj.lt.feign.IBlockVoltageReconClient;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 成片电压越限事件触发重构定时任务(每天早晨扫描中台推送的成片数据)
+ * <p>
+ * sys_job 配置:invokeTarget = seBlockVoltageRecon.noParams(),cron 按中台推送时间配置(如 0 30 6 * * ?)
+ * 通过 Feign 调 bf 服务的 /indicator/block-voltage/recon-scan 端点(跨服务,bf 进程内幂等触发)。
+ *
+ * @author lsl
+ * @since 2026-08-13
+ */
+@Slf4j
+@Component("seBlockVoltageRecon")
+@RequiredArgsConstructor
+public class SeBlockVoltageReconTask {
+
+    private final IBlockVoltageReconClient blockVoltageReconClient;
+
+    public void noParams() {
+        log.info("[成片电压重构] 定时触发 bf 扫描中台推送的成片数据");
+        try {
+            ApiResponse<String> resp = blockVoltageReconClient.triggerBlockVoltageRecon();
+            log.info("[成片电压重构] bf 响应: code={}, msg={}", resp == null ? null : resp.getCode(),
+                    resp == null ? null : resp.getMsg());
+        } catch (Exception e) {
+            log.error("[成片电压重构] 调用 bf 失败", e);
+        }
+    }
+}