lisonglin пре 2 недеља
родитељ
комит
98e5d46a06

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

@@ -13,6 +13,7 @@ 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.scheduler.SeCapacityDailyScheduler;
 import com.hdkj.lt.bf.service.IndicatorDashboardService;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.hdkj.lt.bf.service.SeIndicatorService;
@@ -27,6 +28,8 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -50,6 +53,7 @@ public class IndicatorController extends BaseController {
     private final SeAlarmService seAlarmService;
     private final SiStateEstimationClient siStateEstimationClient;
     private final SeSnapshotService seSnapshotService;
+    private final SeCapacityDailyScheduler seCapacityDailyScheduler;
 
     /**
      * 查询运行态仪表盘
@@ -140,7 +144,7 @@ public class IndicatorController extends BaseController {
     /**
      * 异常告警-电压越限列表
      *
-     * @param params {id, type(2/3/4), feederName(可选), startDate(可选), endDate(可选), orderBy(asc/desc,可选)}
+     * @param params {id, type(2/3/4), feederName(可选), startDate(可选), endDate(可选), orderBy(asc/desc,可选), alarmLevel(feeder/mvtrans)}
      */
     @PostMapping("/alarm/voltage")
     public ApiResponse<PageResult<SeAlarmListVO>> alarmVoltage(@RequestBody Map<String, String> params) {
@@ -152,23 +156,28 @@ public class IndicatorController extends BaseController {
         String orderBy = params.get("orderBy");
         String pageStr = params.get("page");
         String pageSizeStr = params.get("pageSize");
+        String alarmLevel = params.get("alarmLevel");
         if (id == null || type == null) {
             return ApiResponse.fail("参数缺失");
         }
         Integer page = pageStr != null ? Integer.parseInt(pageStr) : 1;
         Integer pageSize = pageSizeStr != null ? Integer.parseInt(pageSizeStr) : 20;
-        return ApiResponse.success(seAlarmService.queryVoltageAlarmList(id, Integer.parseInt(type), feederName, startDate, endDate, orderBy, page, pageSize));
+        return ApiResponse.success(seAlarmService.queryVoltageAlarmList(id, Integer.parseInt(type), feederName, startDate, endDate, orderBy, page, pageSize, alarmLevel));
     }
 
     /**
      * 运行异常信息详情
+     * <p>
+     * 两种查询方式:
+     * - 传 alarmEventId -> 查重过载事件(老逻辑兼容)
+     * - 传 feederId + firstOverTime -> 查电压越限详情
      *
-     * @param params {alarmEventId}
+     * @param params {alarmEventId} 或 {feederId, firstOverTime}
      */
     @PostMapping("/alarm/detail")
     public ApiResponse<SeAlarmDetailVO> alarmDetail(@RequestBody Map<String, String> params) {
         String alarmEventId = params.get("alarmEventId");
-        if (alarmEventId == null) {
+        if (alarmEventId == null || alarmEventId.isEmpty()) {
             return ApiResponse.fail("参数缺失");
         }
         return ApiResponse.success(seAlarmService.queryAlarmDetail(Long.parseLong(alarmEventId)));
@@ -248,6 +257,37 @@ public class IndicatorController extends BaseController {
         return ApiResponse.success(msg.toString());
     }
 
+    /**
+     * 手动触发供电能力日汇总刷数
+     * <p>
+     * 默认重跑今天的数据(覆盖更新:先删再插)。传 statDate 时重跑指定日期。
+     *
+     * @param params {statDate(可选, yyyy-MM-dd),默认当天}
+     */
+    @PostMapping("/capacity/manual")
+    public ApiResponse<String> manualCapacityDaily(@RequestBody Map<String, String> params) {
+        String statDateStr = params.get("statDate");
+        LocalDate date;
+        if (StringUtils.isNotBlank(statDateStr)) {
+            try {
+                date = LocalDate.parse(statDateStr);
+            } catch (Exception e) {
+                return ApiResponse.fail("日期格式非法,期望 yyyy-MM-dd");
+            }
+        } else {
+            date = LocalDate.now();
+        }
+
+        log.info("[供电能力日汇总] 手动触发刷数 statDate={}", date);
+        try {
+            String result = seCapacityDailyScheduler.aggregateForDate(date);
+            return ApiResponse.success(result);
+        } catch (Exception e) {
+            log.error("[供电能力日汇总] 手动触发异常", e);
+            return ApiResponse.fail("执行异常: " + e.getMessage());
+        }
+    }
+
     private StateEstimation mergeCountyResults(List<StateEstimation> list) {
         if (list == null || list.isEmpty()) return null;
 

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

@@ -43,21 +43,21 @@ public class SeCapacityDailyScheduler {
         log.info("[供电能力日汇总] 开始处理 {} 数据", yesterday);
 
         try {
-            aggregateForDate(yesterday);
-            log.info("[供电能力日汇总] {} 处理完成", yesterday);
+            String result = aggregateForDate(yesterday);
+            log.info("[供电能力日汇总] {} {}", yesterday, result);
         } catch (Exception e) {
             log.error("[供电能力日汇总] {} 处理失败", yesterday, e);
         }
     }
 
     /**
-     * 聚合指定日期的断面数据到日汇总表
+     * 聚合指定日期的断面数据到日汇总表(手动触发时调用)
      */
-    private void aggregateForDate(LocalDate date) {
+    public String 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)
@@ -68,7 +68,7 @@ public class SeCapacityDailyScheduler {
 
         if (snapshots.isEmpty()) {
             log.info("[供电能力日汇总] {} 无断面数据,跳过", date);
-            return;
+            return date + " 无断面数据";
         }
 
         // 按馈线分组聚合
@@ -80,6 +80,7 @@ public class SeCapacityDailyScheduler {
             DailyAccumulator acc = accumulatorMap.computeIfAbsent(feederId, k -> new DailyAccumulator());
             acc.countyId = s.getCountyId();
             acc.subsId = s.getSubsId();
+            if (acc.feederName == null) acc.feederName = s.getFeederName();
 
             BigDecimal loadRate = s.getLoadRate();
             if (loadRate != null) {
@@ -115,7 +116,7 @@ public class SeCapacityDailyScheduler {
             FhzgSeCapacityDaily daily = FhzgSeCapacityDaily.builder()
                     .statDate(date)
                     .feederId(feederId)
-                    .feederName(null)
+                    .feederName(acc.feederName)
                     .countyId(acc.countyId)
                     .subsId(acc.subsId)
                     .maxLoadRate(acc.maxLoadRate)
@@ -134,7 +135,9 @@ public class SeCapacityDailyScheduler {
             capacityDailyMapper.insert(record);
         }
 
-        log.info("[供电能力日汇总] {} 处理完成: {} 条馈线日汇总", date, dailyRecords.size());
+        String result = date + " 处理完成: " + dailyRecords.size() + " 条馈线日汇总";
+        log.info("[供电能力日汇总] {}", result);
+        return result;
     }
 
     /**
@@ -143,6 +146,7 @@ public class SeCapacityDailyScheduler {
     private static class DailyAccumulator {
         String countyId;
         String subsId;
+        String feederName;
         BigDecimal maxLoadRate = BigDecimal.ZERO;
         BigDecimal sumLoadRate = BigDecimal.ZERO;
         int heavyCount = 0;

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

@@ -41,7 +41,7 @@ public interface SeAlarmService {
      */
     PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
                                                      String startDate, String endDate, String orderDir,
-                                                     Integer page, Integer pageSize);
+                                                     Integer page, Integer pageSize, String alarmLevel);
 
     /**
      * 查询运行异常信息详情

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

@@ -4,12 +4,14 @@ 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.FhzgSeSnapshotDetail;
 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.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -42,6 +44,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     private final FeederGroupApplication feederGroupApplication;
     private final FhzgSeMonitorCurrentMapper monitorCurrentMapper;
     private final FhzgSeMonitorVoltageMapper monitorVoltageMapper;
+    private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
@@ -72,7 +75,6 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
                 .ge(FhzgSeCurrentEvent::getFirstOverTime, rangeStart)
                 .lt(FhzgSeCurrentEvent::getFirstOverTime, rangeEnd)
-                .eq(FhzgSeCurrentEvent::getStatus, 1)
                 .in(FhzgSeCurrentEvent::getAlarmType, OVERLOAD_TYPES);
 
         applyCurrentFilter(wrapper, id, type);
@@ -108,7 +110,7 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     @Override
     public PageResult<SeAlarmListVO> queryVoltageAlarmList(String id, Integer type, String feederName,
                                                             String startDate, String endDate, String orderDir,
-                                                            Integer page, Integer pageSize) {
+                                                            Integer page, Integer pageSize, String alarmLevel) {
         if (page == null || page < 1) page = 1;
         if (pageSize == null || pageSize < 1) pageSize = 20;
 
@@ -119,12 +121,10 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         LocalDateTime rangeEnd = end.plusDays(1).atStartOfDay();
 
         Page<FhzgSeVoltageEvent> mpPage = new Page<>(page, pageSize);
-
         LambdaQueryWrapper<FhzgSeVoltageEvent> wrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
                 .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
                 .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
-                .eq(FhzgSeVoltageEvent::getStatus, 1)
-                .in(FhzgSeVoltageEvent::getAlarmLevel, "feeder", "mvtrans");
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder".equals(alarmLevel) ? "feeder" : "mvtrans");
 
         applyVoltageFilter(wrapper, id, type);
         if (StringUtils.isNotBlank(feederName)) {
@@ -139,14 +139,9 @@ public class SeAlarmServiceImpl implements SeAlarmService {
 
     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)
+                .feederName(e.getFeederName())
                 .feederId(e.getFeederId())
                 .alarmDisplayValue(displayValue)
                 .alarmTime(e.getFirstOverTime() != null ? e.getFirstOverTime().format(DT_FMT) : null)
@@ -155,12 +150,21 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                 .build();
     }
 
+    /**
+     * 展示标签:按 alarmType + alarmLevel 拼线路/台区电压越限
+     */
     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 + "电压越限";
+        boolean isOver = alarmType != null && alarmType.contains("over");
+        boolean isUnder = alarmType != null && alarmType.contains("under");
+        if ("mvtrans".equals(alarmLevel)) {
+            if (isOver) return "台区电压越上限";
+            if (isUnder) return "台区电压越下限";
+            return "电压越限";
+        }
+        // 默认 feeder
+        if (isOver) return "线路电压越上限";
+        if (isUnder) return "线路电压越下限";
+        return "电压越限";
     }
 
     // ============================================================
@@ -171,56 +175,99 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     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);
+        if (voltageEvent != null && "feeder".equals(voltageEvent.getAlarmLevel())) {
+            return buildFeederDetail(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())
+    /**
+     * 线路事件详情:展示电压范围 + 窗口内配变统计
+     */
+    private SeAlarmDetailVO buildFeederDetail(FhzgSeVoltageEvent feederEvent) {
+        String feederId = feederEvent.getFeederId();
+        // 持续窗口
+        LocalDateTime endTime = feederEvent.getRecoverTime();
+        if (endTime == null && feederEvent.getStatus() == 1) {
+            FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
+                    new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                            .eq(FhzgSeMonitorVoltage::getFeederId, feederId)
+                            .eq(FhzgSeMonitorVoltage::getDeviceType, "feeder")
                             .last("LIMIT 1"));
             if (state != null) endTime = state.getLastSnapTime();
         }
-        if (endTime == null) endTime = event.getTriggerTime();
+        if (endTime == null) endTime = feederEvent.getTriggerTime();
+        LocalDateTime rangeEnd = endTime != null ? endTime : LocalDateTime.now();
+
+        // 实时查该馈线在越限时段内的电压范围
+        BigDecimal maxVoltage = null, minVoltage = null;
+        List<FhzgSeSnapshotDetail> snaps = snapshotDetailMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
+                        .eq(FhzgSeSnapshotDetail::getFeederId, feederId)
+                        .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
+                        .isNotNull(FhzgSeSnapshotDetail::getVoltageValue)
+                        .ge(FhzgSeSnapshotDetail::getSnapshotTime, feederEvent.getFirstOverTime())
+                        .le(FhzgSeSnapshotDetail::getSnapshotTime, rangeEnd));
+        for (FhzgSeSnapshotDetail s : snaps) {
+            BigDecimal v = s.getVoltageValue();
+            if (v == null) continue;
+            if (maxVoltage == null || v.compareTo(maxVoltage) > 0) maxVoltage = v;
+            if (minVoltage == null || v.compareTo(minVoltage) < 0) minVoltage = v;
+        }
+
+        // 窗口内配变统计(实时查,不依赖触发快照值)
+        int[] mc = countMvtransInWindow(feederId, feederEvent.getFirstOverTime(), rangeEnd);
 
         return SeAlarmDetailVO.builder()
-                .alarmEventId(event.getId())
-                .alarmType(event.getAlarmType())
-                .alarmCategory("overload")
-                .countyName(event.getCountyName())
-                .feederName(event.getFeederName())
-                .alarmStartTime(event.getFirstOverTime())
+                .alarmEventId(feederEvent.getId())
+                .alarmType(feederEvent.getAlarmType())
+                .alarmCategory("voltage")
+                .countyName(feederEvent.getCountyName())
+                .feederName(feederEvent.getFeederName())
+                .alarmStartTime(feederEvent.getFirstOverTime())
                 .alarmEndTime(endTime)
-                .maxLoadRate(event.getMaxLoadRate())
-                .reconStatus(event.getReconStatus())
+                .abnormalIndicator(resolveVoltageDisplay(feederEvent.getAlarmType(), feederEvent.getAlarmLevel()))
+                .maxVoltage(maxVoltage)
+                .minVoltage(minVoltage)
+                .overUpperMvtransCount(mc[0])
+                .overLowerMvtransCount(mc[1])
+                .reconStatus(feederEvent.getReconStatus())
                 .build();
     }
 
-    private SeAlarmDetailVO buildVoltageDetail(FhzgSeVoltageEvent event) {
-        String indicator = resolveVoltageDisplay(event.getAlarmType(), event.getAlarmLevel());
-        String feederName = event.getFeederName();
+//    /**
+//     * 单配变事件详情:展示该配变自身信息
+//     */
+//    private SeAlarmDetailVO buildMvtransDetail(FhzgSeVoltageEvent event) {
+//        LocalDateTime endTime = event.getRecoverTime() != null ? event.getRecoverTime()
+//                : (event.getTriggerTime() != null ? event.getTriggerTime() : event.getFirstOverTime());
+//        return SeAlarmDetailVO.builder()
+//                .alarmEventId(event.getId())
+//                .alarmType(event.getAlarmType())
+//                .alarmCategory("voltage")
+//                .countyName(event.getCountyName())
+//                .feederName(event.getFeederName())
+//                .alarmStartTime(event.getFirstOverTime())
+//                .alarmEndTime(endTime)
+//                .abnormalIndicator(resolveVoltageDisplay(event.getAlarmType(), event.getAlarmLevel()))
+//                .build();
+//    }
 
+    private SeAlarmDetailVO buildOverloadDetail(FhzgSeCurrentEvent event) {
         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())
+            FhzgSeMonitorCurrent state = monitorCurrentMapper.selectOne(
+                    new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
+                            .eq(FhzgSeMonitorCurrent::getFeederId, event.getFeederId())
                             .last("LIMIT 1"));
             if (state != null) endTime = state.getLastSnapTime();
         }
@@ -229,20 +276,37 @@ public class SeAlarmServiceImpl implements SeAlarmService {
         return SeAlarmDetailVO.builder()
                 .alarmEventId(event.getId())
                 .alarmType(event.getAlarmType())
-                .alarmCategory("voltage")
+                .alarmCategory("overload")
                 .countyName(event.getCountyName())
-                .feederName(feederName)
+                .feederName(event.getFeederName())
                 .alarmStartTime(event.getFirstOverTime())
                 .alarmEndTime(endTime)
-                .abnormalIndicator(indicator)
-                .maxVoltage(event.getVoltageRangeMax())
-                .minVoltage(event.getVoltageRangeMin())
-                .overUpperMvtransCount(event.getOverUpperMvtrans())
-                .overLowerMvtransCount(event.getOverLowerMvtrans())
+                .maxLoadRate(event.getMaxLoadRate())
                 .reconStatus(event.getReconStatus())
                 .build();
     }
 
+    /**
+     * 查 feeder 事件发生在窗口 [firstOverTime, endTime] 内的 mvtrans 事件数
+     */
+    private int[] countMvtransInWindow(String feederId, LocalDateTime firstOverTime, LocalDateTime endTime) {
+        if (firstOverTime == null) return new int[]{0, 0};
+        LocalDateTime rangeEnd = endTime != null ? endTime : LocalDateTime.now();
+        List<FhzgSeVoltageEvent> list = voltageEventMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                        .eq(FhzgSeVoltageEvent::getFeederId, feederId)
+                        .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
+                        .eq(FhzgSeVoltageEvent::getStatus, 1)
+                        .ge(FhzgSeVoltageEvent::getFirstOverTime, firstOverTime)
+                        .le(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd));
+        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};
+    }
+
     // ============================================================
     // 辅助
     // ============================================================

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

@@ -3,6 +3,7 @@ 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.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgDwmVoltageOverLimitDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
@@ -11,6 +12,7 @@ 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.FhzgSeCurrentEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgDwmVoltageOverLimitDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
@@ -47,6 +49,7 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     private final FhzgDwmVoltageOverLimitDailyMapper voltageDailyMapper;
     private final FhzgSeSnapshotDetailMapper snapshotDetailMapper;
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
+    private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FeederGroupApplication feederGroupApplication;
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
@@ -97,17 +100,45 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     }
 
     /**
-     * 今日电压:COUNT 聚合到 SQL,按 alarmLevel + alarmType 分组
-     * 返回4个计数:台区越上限/下限 + 馈线越上限/下限
+     * 今日电压:线路越限数从 feeder 事件 (alarm_level='feeder') 统计,
+     * 台区越限数从 mvtrans 事件 (alarm_level='mvtrans') 直接 COUNT。
+     * 按 firstOverTime 今日范围 + status=1(活跃)过滤。
      */
     private SeVoltageDashboardVO queryVoltageToday(String id, Integer type) {
         LocalDateTime todayStart = LocalDate.now().atStartOfDay();
-        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay();
+
+        // == 线路越限事件统计(alarm_level='feeder')==
+        LambdaQueryWrapper<FhzgSeVoltageEvent> feederWrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, "feeder")
+                .ge(FhzgSeVoltageEvent::getFirstOverTime, todayStart)
+                .lt(FhzgSeVoltageEvent::getFirstOverTime, tomorrowStart);
+        applyNodeFilter(feederWrapper, id, type);
+        List<FhzgSeVoltageEvent> feederEvents = voltageEventMapper.selectList(feederWrapper);
+
+        int overUpperFeeder = 0, overLowerFeeder = 0;
+        for (FhzgSeVoltageEvent e : feederEvents) {
+            String t = e.getAlarmType();
+            if (t == null) continue;
+            if (t.contains("over")) overUpperFeeder++;
+            if (t.contains("under")) overLowerFeeder++;
+        }
 
-        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");
+        // == 配变越限事件统计(alarm_level='mvtrans')==
+        LambdaQueryWrapper<FhzgSeVoltageEvent> mvtransWrapper = new LambdaQueryWrapper<FhzgSeVoltageEvent>()
+                .eq(FhzgSeVoltageEvent::getAlarmLevel, "mvtrans")
+                .ge(FhzgSeVoltageEvent::getFirstOverTime, todayStart)
+                .lt(FhzgSeVoltageEvent::getFirstOverTime, tomorrowStart);
+        applyNodeFilter(mvtransWrapper, id, type);
+        List<FhzgSeVoltageEvent> mvtransEvents = voltageEventMapper.selectList(mvtransWrapper);
+
+        int overUpperArea = 0, overLowerArea = 0;
+        for (FhzgSeVoltageEvent e : mvtransEvents) {
+            String t = e.getAlarmType();
+            if (t == null) continue;
+            if (t.contains("over")) overUpperArea++;
+            if (t.contains("under")) overLowerArea++;
+        }
 
         return SeVoltageDashboardVO.builder()
                 .todayOverUpperAreaCount(overUpperArea)
@@ -118,20 +149,30 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     }
 
     /**
-     * 按节点+时段+类型统计电压越限事件数
+     * 按节点+时段统计重过载事件数
+     * 返回 int[2]: [0]=重载事件数, [1]=过载事件数
      */
-    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;
+    private int[] countCurrentEvents(String id, Integer type,
+                                      LocalDateTime start, LocalDateTime end) {
+        LambdaQueryWrapper<FhzgSeCurrentEvent> wrapper = new LambdaQueryWrapper<FhzgSeCurrentEvent>()
+                .ge(FhzgSeCurrentEvent::getFirstOverTime, start)
+                .lt(FhzgSeCurrentEvent::getFirstOverTime, end)
+                .in(FhzgSeCurrentEvent::getAlarmType, "current_heavy", "current_overload");
+        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);
+        }
+
+        List<FhzgSeCurrentEvent> events = currentEventMapper.selectList(wrapper);
+        int heavy = 0, overload = 0;
+        for (FhzgSeCurrentEvent e : events) {
+            if ("current_heavy".equals(e.getAlarmType())) heavy++;
+            else if ("current_overload".equals(e.getAlarmType())) overload++;
+        }
+        return new int[]{heavy, overload};
     }
 
     /**
@@ -280,40 +321,54 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     }
 
     /**
-     * 今日供电能力:实时查 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>()
+        // 1. 负载率:从断面明细聚合(保持馈线96点时序)
+        LambdaQueryWrapper<FhzgSeSnapshotDetail> snapshotWrapper = new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
                 .eq(FhzgSeSnapshotDetail::getDeviceType, "feeder")
                 .isNotNull(FhzgSeSnapshotDetail::getLoadRate)
                 .ge(FhzgSeSnapshotDetail::getSnapshotTime, startTime)
                 .le(FhzgSeSnapshotDetail::getSnapshotTime, endTime);
+        applySnapshotFilter(snapshotWrapper, id, type);
+        List<FhzgSeSnapshotDetail> snapshots = snapshotDetailMapper.selectList(snapshotWrapper);
 
-        applySnapshotFilter(wrapper, id, type);
-
-        List<FhzgSeSnapshotDetail> snapshots = snapshotDetailMapper.selectList(wrapper);
-
+        SeCapacityDashboardVO vo;
         if (snapshots.isEmpty()) {
-            return SeCapacityDashboardVO.builder().build();
+            vo = SeCapacityDashboardVO.builder().build();
+        } else {
+            vo = aggregateSnapshots(snapshots);
+            // 馈线 → 附加96点时序
+            if (type == 4) {
+                vo.setLoadRateSeries(buildFeederTimeSeries(snapshots));
+            }
         }
 
-        SeCapacityDashboardVO vo = aggregateSnapshots(snapshots);
-
-        // 馈线 → 附加96点时序
-        if (type == 4) {
-            vo.setLoadRateSeries(buildFeederTimeSeries(snapshots));
+        // 2. 重过载事件次数:从事件表统计
+        int[] eventCounts = countCurrentEvents(id, type, startTime, endTime);
+        vo.setHeavyLoadEventCount(eventCounts[0]);
+        vo.setOverloadEventCount(eventCounts[1]);
+        int totalEvents = eventCounts[0] + eventCounts[1];
+        vo.setTotalEventCount(totalEvents);
+        if (totalEvents > 0) {
+            vo.setHeavyLoadRate(new BigDecimal(eventCounts[0] * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+            vo.setOverloadRate(new BigDecimal(eventCounts[1] * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+        } else {
+            vo.setHeavyLoadRate(BigDecimal.ZERO);
+            vo.setOverloadRate(BigDecimal.ZERO);
         }
         return vo;
     }
 
     /**
-     * 历史供电能力:查 capacity_daily 日汇总表(每馈线每天一行,量极小)
-     * month = 月初~昨天;year = 1.1~昨天
+     * 历史供电能力:负载率从日汇总表查,重过载次数从事件表统计
+     * month = 月初 ~ 昨天;year = 1.1 ~ 昨天
      */
     private SeCapacityDashboardVO queryCapacityHistory(String id, Integer type, String timeScope) {
         LocalDate today = LocalDate.now();
@@ -331,59 +386,58 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             return SeCapacityDashboardVO.builder().build();
         }
 
+        // 1. 负载率:从日汇总表聚合
         LambdaQueryWrapper<FhzgSeCapacityDaily> wrapper = new LambdaQueryWrapper<FhzgSeCapacityDaily>()
                 .ge(FhzgSeCapacityDaily::getStatDate, startDate)
                 .le(FhzgSeCapacityDaily::getStatDate, endDate);
-
         applyCapacityDailyFilter(wrapper, id, type);
-
         List<FhzgSeCapacityDaily> dailyRecords = capacityDailyMapper.selectList(wrapper);
 
+        SeCapacityDashboardVO vo;
         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());
+            vo = SeCapacityDashboardVO.builder().build();
+        } else {
+            BigDecimal maxLoadRate = BigDecimal.ZERO;
+            BigDecimal sumAvg = BigDecimal.ZERO;
+            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;
+            BigDecimal avgLoadRate = !dailyRecords.isEmpty()
+                    ? sumAvg.divide(BigDecimal.valueOf(dailyRecords.size()), 2, RoundingMode.HALF_UP)
+                    : BigDecimal.ZERO;
+            vo = SeCapacityDashboardVO.builder()
+                    .maxLoadRate(maxLoadRate)
+                    .avgLoadRate(avgLoadRate)
+                    .build();
         }
 
-        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();
+        // 2. 重过载事件次数:从事件表统计
+        LocalDateTime rangeStart = startDate.atStartOfDay();
+        LocalDateTime rangeEnd = endDate.atTime(23, 59, 59);
+        int[] eventCounts = countCurrentEvents(id, type, rangeStart, rangeEnd);
+        vo.setHeavyLoadEventCount(eventCounts[0]);
+        vo.setOverloadEventCount(eventCounts[1]);
+        int totalEvents = eventCounts[0] + eventCounts[1];
+        vo.setTotalEventCount(totalEvents);
+        if (totalEvents > 0) {
+            vo.setHeavyLoadRate(new BigDecimal(eventCounts[0] * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+            vo.setOverloadRate(new BigDecimal(eventCounts[1] * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+        } else {
+            vo.setHeavyLoadRate(BigDecimal.ZERO);
+            vo.setOverloadRate(BigDecimal.ZERO);
+        }
+        return vo;
     }
 
     /**
-     * 内存聚合断面列表 → VO(today用)
+     * 内存聚合断面列表 → VO(today用,只算负载率,事件次数由 queryCapacityToday 从事件表统计
      */
     private SeCapacityDashboardVO aggregateSnapshots(List<FhzgSeSnapshotDetail> snapshots) {
         BigDecimal maxLoadRate = snapshots.stream()
@@ -398,31 +452,9 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
                 .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();
     }
 

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

@@ -6,6 +6,7 @@ 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.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
@@ -298,43 +299,62 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                 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()));
-                    }
-                }
+                            String countyName = lookupCountyName(countyId, null);
+
+                            if ("feeder".equals(deviceType)) {
+                                // === 馈线自身越限 → 创建 feeder 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                BigDecimal[] vr = queryVoltageRangeFromSnap(feederId, state.getFirstOverTime(), snapTime);
+                                int[] mc = queryMvtransCounts(feederId, state.getFirstOverTime(), snapTime);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio)
+                                        .voltageRangeMax(vr[0]).voltageRangeMin(vr[1])
+                                        .overUpperMvtrans(mc[0]).overLowerMvtrans(mc[1])
+                                        .lastValue(voltageRatio).status(1).build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1); state.setAlarmEventId(event.getId());
+                                // 馈线越下限 → 异步触发重构算法
+                                if ("feeder".equals(deviceType) && "voltage_under".equals(alarmType)) {
+                                    eventPublisher.publishEvent(new ReconTriggerEvent(
+                                            event.getId(), feederId, feederName, countyId, subsId,
+                                            alarmType, snapTime, state.getFirstOverTime()));
+                                }
+                            } else if ("mvtrans".equals(deviceType)) {
+                                // === 台区越限 → 创建 mvtrans 级独立事件 ===
+                                String alarmType = resolveVoltageAlarmType(deviceType, voltageStatus);
+                                BigDecimal threshold = resolveVoltageThreshold(deviceType, voltageStatus);
+                                FhzgSeVoltageEvent event = FhzgSeVoltageEvent.builder()
+                                        .deviceId(deviceId).deviceName(feederName)
+                                        .deviceType(deviceType).alarmLevel(deviceType).alarmType(alarmType)
+                                        .feederId(feederId).feederName(feederName)
+                                        .subsId(subsId).countyId(countyId).countyName(countyName)
+                                        .mvtransId(mvtransId)
+                                        .threshold(threshold)
+                                        .firstOverTime(state.getFirstOverTime()).triggerTime(snapTime)
+                                        .triggerValue(voltageRatio).lastValue(voltageRatio).status(1)
+                                        .build();
+                                voltageEventMapper.insert(event);
+                                state.setAlarmTriggered(1);
+                                state.setAlarmEventId(event.getId());
+                            }
+                            // consumer 级别暂走旧逻辑(如有可后续优化)
+                        }
             } else {
                 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);
+                    // 只有 feeder 自身恢复才标聚合事件 status=2;mvtrans 恢复不动聚合事件
+                    if ("feeder".equals(deviceType)) {
+                        FhzgSeVoltageEvent update = new FhzgSeVoltageEvent();
+                        update.setId(state.getAlarmEventId());
+                        update.setStatus(2); update.setRecoverTime(snapTime); update.setLastValue(voltageRatio);
+                        voltageEventMapper.updateById(update);
+                    }
                 }
                 state.setFirstOverTime(null); state.setAlarmTriggered(0); state.setAlarmEventId(null);
             }
@@ -387,22 +407,6 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         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%)
      */
@@ -442,6 +446,18 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         return 0;
     }
 
+    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 String resolveVoltageAlarmType(String deviceType, int status) {
         if ("consumer".equals(deviceType)) {
             switch (status) {
@@ -455,16 +471,23 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         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;
+    /**
+     * 查窗口内已有 mvtrans 越限事件数(用于 feeder 触发时预填 overUpper/overLowerMvtrans)
+     */
+    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};
     }
 
     // ============================================================
@@ -478,7 +501,17 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         try { return new BigDecimal(v); } catch (NumberFormatException e) { return null; }
     }
 
+    /**
+     * 按 countyId 从 SeSnapshotConstants.COUNTIES 查 countyName。
+     * COUNTIES 的 key=countyId(=maintOrg UUID),value=[countySid, countyName]
+     */
     private String lookupCountyName(String countyId, Map<String, String[]> feederMap) {
+        if (countyId == null) return null;
+        for (Map.Entry<String, String[]> entry : SeSnapshotConstants.COUNTIES) {
+            if (countyId.equals(entry.getKey())) {
+                return entry.getValue()[1];
+            }
+        }
         return null;
     }