2
0

2 Commits f24a084b2b ... 79500839ff

Autor SHA1 Nachricht Datum
  lisonglin 79500839ff 线损更改逻辑 vor 10 Stunden
  lisonglin a67acbacf9 线损更改逻辑 vor 10 Stunden
16 geänderte Dateien mit 224 neuen und 523 gelöschten Zeilen
  1. 0 64
      common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeLineLossDaily.java
  2. 21 18
      common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeLineLossRate.java
  3. 0 7
      common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeLineLossDailyMapper.java
  4. 0 15
      common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeLineLossDetailMapper.java
  5. 15 0
      common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeLineLossRateMapper.java
  6. 0 114
      common/common-se/src/main/java/com/hdkj/lt/bf/scheduler/SeLineLossDailyScheduler.java
  7. 0 19
      common/common-se/src/main/resources/mapper/FhzgSeLineLossDetailMapper.xml
  8. 2 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/common/SeMonitorThreshold.java
  9. 5 36
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  10. 3 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/dto/SeVoltageAlarmListReq.java
  11. 7 7
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeLineLossRateVO.java
  12. 2 2
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/SeIndicatorService.java
  13. 16 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeAlarmServiceImpl.java
  14. 37 79
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  15. 116 127
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  16. 0 35
      services/ruoyi-job/src/main/java/com/hdkj/lt/job/task/SeLineLossDailyTask.java

+ 0 - 64
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeLineLossDaily.java

@@ -1,64 +0,0 @@
-package com.hdkj.lt.bf.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-
-/**
- * 线路线损日汇总
- *
- * @author lsl
- * @since 2026-07-30
- */
-@TableName("fhzg_se_line_loss_daily")
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class FhzgSeLineLossDaily implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /** 统计日期 */
-    private LocalDate statDate;
-
-    /** 馈线ID */
-    private String feederId;
-
-    /** 馈线名称 */
-    private String feederName;
-
-    /** 区县ID */
-    private String countyId;
-
-    /** 变电站ID */
-    private String subsId;
-
-    /** 日线损电量 kWh */
-    private BigDecimal lineLossKwh;
-
-    /** 日最大线损功率 kW */
-    private BigDecimal maxLineLossKw;
-
-    /** 日平均线损功率 kW */
-    private BigDecimal avgLineLossKw;
-
-    /** 总断面数 */
-    private Integer totalSnapshots;
-
-    private LocalDateTime createTime;
-
-    private LocalDateTime updateTime;
-}

+ 21 - 18
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeLineLossDetail.java → common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeLineLossRate.java

@@ -13,17 +13,20 @@ import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
- * 线路线损明细(逐段逐断面)
+ * 馈线线损率(每断面每馈线一条)
+ * <p>
+ * 口径:线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
+ * 总功率取状估馈线块 seoPs(≈出线开关功率);配变/中压用户接入点有功同样取状估 seoPs。
  *
  * @author lsl
- * @since 2026-07-30
+ * @since 2026-08
  */
-@TableName("fhzg_se_line_loss_detail")
+@TableName("fhzg_se_line_loss_rate")
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class FhzgSeLineLossDetail implements Serializable {
+public class FhzgSeLineLossRate implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
@@ -36,29 +39,29 @@ public class FhzgSeLineLossDetail implements Serializable {
     /** 馈线ID */
     private String feederId;
 
-    /** 段ID */
-    private String psrId;
+    /** 馈线名称 */
+    private String feederName;
 
-    /** 段类型: dxd-导线段, 0201-电缆段 */
-    private String psrType;
+    /** 变电站ID */
+    private String subsId;
 
     /** 区县ID */
     private String countyId;
 
-    /** 变电站ID */
-    private String subsId;
+    /** 总功率(馈线状估有功, MW) */
+    private BigDecimal totalPower;
 
-    /** 状估电流 kA */
-    private BigDecimal currentKa;
+    /** 配变有功(MW) */
+    private BigDecimal transPower;
 
-    /** 段长度 米 */
-    private BigDecimal sbLengthM;
+    /** 中压用户接入点有功(MW) */
+    private BigDecimal consumerPower;
 
-    /** 电阻率 Ω/km */
-    private BigDecimal resistivity;
+    /** 线损功率 = 总功率-配变-用户 (MW) */
+    private BigDecimal lineLossPower;
 
-    /** 线损功率 kW */
-    private BigDecimal lineLossKw;
+    /** 线损率(%) */
+    private BigDecimal lineLossRate;
 
     private LocalDateTime createTime;
 }

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

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

+ 0 - 15
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeLineLossDetailMapper.java

@@ -1,15 +0,0 @@
-package com.hdkj.lt.bf.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-
-import java.util.List;
-
-@Mapper
-public interface FhzgSeLineLossDetailMapper extends BaseMapper<FhzgSeLineLossDetail> {
-
-    /** 批量插入线损明细 */
-    int insertBatch(@Param("list") List<FhzgSeLineLossDetail> list);
-}

+ 15 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeLineLossRateMapper.java

@@ -0,0 +1,15 @@
+package com.hdkj.lt.bf.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 馈线线损率 Mapper
+ *
+ * @author lsl
+ * @since 2026-08
+ */
+@Mapper
+public interface FhzgSeLineLossRateMapper extends BaseMapper<FhzgSeLineLossRate> {
+}

+ 0 - 114
common/common-se/src/main/java/com/hdkj/lt/bf/scheduler/SeLineLossDailyScheduler.java

@@ -1,114 +0,0 @@
-package com.hdkj.lt.bf.scheduler;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDaily;
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDailyMapper;
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDetailMapper;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 线路线损日汇总逻辑(公共模块)
- * <p>
- * 定时调度由 pwfhzg-job 通过 sys_job 配置触发(凌晨1点聚合昨天),
- * 也可由 bf 手动端点 /indicator/line-loss/manual 触发。
- *
- * @author lsl
- * @since 2026-07-30
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class SeLineLossDailyScheduler {
-
-    private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
-    private final FhzgSeLineLossDailyMapper lineLossDailyMapper;
-
-    /**
-     * 聚合指定日期的线损明细到日汇总表(手动触发时调用)
-     */
-    @Transactional(rollbackFor = Exception.class)
-    public String aggregateForDate(LocalDate date) {
-        LocalDateTime startTime = date.atStartOfDay();
-        LocalDateTime endTime = date.atTime(23, 59, 59);
-
-        // 按馈线 SQL 聚合
-        // lineLossKwh = SUM(line_loss_kw) × 0.25(15min 断面间隔换算 kWh)
-        QueryWrapper<FhzgSeLineLossDetail> wrapper = new QueryWrapper<FhzgSeLineLossDetail>()
-                .select("feeder_id",
-                        "MAX(county_id) AS county_id",
-                        "MAX(subs_id) AS subs_id",
-                        "COALESCE(SUM(line_loss_kw), 0) AS sum_kw",
-                        "COALESCE(MAX(line_loss_kw), 0) AS max_kw",
-                        "COUNT(line_loss_kw) AS cnt")
-                .ge("snap_time", startTime)
-                .le("snap_time", endTime)
-                .isNotNull("feeder_id")
-                .groupBy("feeder_id");
-
-        List<Map<String, Object>> rows = lineLossDetailMapper.selectMaps(wrapper);
-
-        if (rows == null || rows.isEmpty()) {
-            log.info("[线路线损日汇总] {} 无线损明细数据,跳过", date);
-            return date + " 无线损明细数据";
-        }
-
-        // 构建日汇总记录
-        List<FhzgSeLineLossDaily> dailyRecords = new ArrayList<>();
-        for (Map<String, Object> row : rows) {
-            String feederId = (String) row.get("feeder_id");
-            if (feederId == null) continue;
-
-            BigDecimal sumKw = (BigDecimal) row.get("sum_kw");
-            BigDecimal maxKw = (BigDecimal) row.get("max_kw");
-            int cnt = ((Number) row.get("cnt")).intValue();
-
-            // 线损电量 kWh = 线损功率 kW × 断面间隔(15min = 0.25h)
-            BigDecimal sumKwh = sumKw.multiply(BigDecimal.valueOf(0.25));
-
-            BigDecimal avgKw = cnt > 0
-                    ? sumKw.divide(BigDecimal.valueOf(cnt), 4, RoundingMode.HALF_UP)
-                    : BigDecimal.ZERO;
-
-            FhzgSeLineLossDaily daily = FhzgSeLineLossDaily.builder()
-                    .statDate(date)
-                    .feederId(feederId)
-                    .feederName(null)
-                    .countyId((String) row.get("county_id"))
-                    .subsId((String) row.get("subs_id"))
-                    .lineLossKwh(sumKwh.setScale(2, RoundingMode.HALF_UP))
-                    .maxLineLossKw(maxKw.compareTo(BigDecimal.ZERO) > 0 ? maxKw.setScale(4, RoundingMode.HALF_UP) : BigDecimal.ZERO)
-                    .avgLineLossKw(avgKw.setScale(4, RoundingMode.HALF_UP))
-                    .totalSnapshots(cnt)
-                    .createTime(LocalDateTime.now())
-                    .updateTime(LocalDateTime.now())
-                    .build();
-            dailyRecords.add(daily);
-        }
-
-        // 先删当天已有数据(避免重复运行时叠加)
-        LambdaQueryWrapper<FhzgSeLineLossDaily> deleteWrapper = new LambdaQueryWrapper<FhzgSeLineLossDaily>()
-                .eq(FhzgSeLineLossDaily::getStatDate, date);
-        lineLossDailyMapper.delete(deleteWrapper);
-
-        for (FhzgSeLineLossDaily record : dailyRecords) {
-            lineLossDailyMapper.insert(record);
-        }
-
-        String result = date + " 处理完成: " + dailyRecords.size() + " 条馈线线损日汇总";
-        log.info("[线路线损日汇总] {}", result);
-        return result;
-    }
-}

+ 0 - 19
common/common-se/src/main/resources/mapper/FhzgSeLineLossDetailMapper.xml

@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.hdkj.lt.bf.mapper.FhzgSeLineLossDetailMapper">
-
-    <!-- 批量插入线损明细 -->
-    <insert id="insertBatch">
-        INSERT INTO fhzg_se_line_loss_detail (
-            snap_time, feeder_id, psr_id, psr_type, county_id, subs_id,
-            current_ka, sb_length_m, resistivity, line_loss_kw, create_time
-        ) VALUES
-        <foreach collection="list" item="item" separator=",">
-            (
-            #{item.snapTime}, #{item.feederId}, #{item.psrId}, #{item.psrType}, #{item.countyId}, #{item.subsId},
-            #{item.currentKa}, #{item.sbLengthM}, #{item.resistivity}, #{item.lineLossKw}, #{item.createTime}
-            )
-        </foreach>
-    </insert>
-
-</mapper>

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

@@ -36,6 +36,8 @@ public final class SeMonitorThreshold {
     // === 负载率 ===
     public static final BigDecimal LOAD_RATE_HEAVY = new BigDecimal("75");
     public static final BigDecimal LOAD_RATE_OVERLOAD = new BigDecimal("100");
+    /** 负载率异常阈值:超过该值视为异常数据(状估量测异常),不计入计算 */
+    public static final BigDecimal LOAD_RATE_ABNORMAL = new BigDecimal("200");
 
     // === 电压越限持续触发: 时间跨度 ≥ 60分钟 ===
     public static final long TRIGGER_DURATION_MINUTES = 60L;

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

@@ -9,7 +9,7 @@ import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
 import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
-import com.hdkj.lt.bf.entity.vo.SeLineLossVO;
+import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
 import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
 import com.hdkj.lt.bf.entity.vo.SeAlarmListVO;
@@ -17,7 +17,6 @@ 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.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;
@@ -57,7 +56,6 @@ public class IndicatorController extends BaseController {
     private final SeSnapshotService seSnapshotService;
     private final SeCapacityDailyScheduler seCapacityDailyScheduler;
     private final SeConsumerCountDailyScheduler seConsumerCountDailyScheduler;
-    private final SeLineLossDailyScheduler seLineLossDailyScheduler;
     private final SeBlockVoltageEventService seBlockVoltageEventService;
 
     /**
@@ -108,12 +106,14 @@ public class IndicatorController extends BaseController {
     }
 
     /**
-     * 线损电量(独立接口,从供电能力拆出;today/month/year)
+     * 线损率(独立接口,从供电能力拆出;today/month/year)
+     * <p>
+     * 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%,区间加权平均。
      *
      * @param params {id, type(2/3/4), timeScope(today/month/year)}
      */
     @PostMapping("/line-loss")
-    public ApiResponse<SeLineLossVO> lineLoss(@RequestBody Map<String, String> params) {
+    public ApiResponse<SeLineLossRateVO> lineLoss(@RequestBody Map<String, String> params) {
         String id = params.get("id");
         String type = params.get("type");
         String timeScope = params.get("timeScope");
@@ -348,37 +348,6 @@ public class IndicatorController extends BaseController {
         }
     }
 
-    /**
-     * 手动触发线路线损日汇总刷数
-     * <p>
-     * 默认重跑今天的数据(覆盖更新:先删再插)。传 statDate 时重跑指定日期。
-     *
-     * @param params {statDate(可选, yyyy-MM-dd),默认当天}
-     */
-    @PostMapping("/line-loss/manual")
-    public ApiResponse<String> manualLineLossDaily(@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 = seLineLossDailyScheduler.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;
 

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

@@ -32,6 +32,9 @@ public class SeVoltageAlarmListReq {
     @ApiModelProperty(value = "时间排序 asc/desc")
     private String orderByTime;
 
+    @ApiModelProperty(value = "越限方向筛选: 1=越上限 2=越下限 0/不传=全量")
+    private Integer orderByOvertype;
+
     @ApiModelProperty(value = "告警级别 feeder/mvtrans", required = true)
     private String alarmLevel;
 

+ 7 - 7
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeLineLossVO.java → services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeLineLossRateVO.java

@@ -13,10 +13,10 @@ import java.io.Serializable;
 import java.math.BigDecimal;
 
 /**
- * 线损电量 VO(独立接口,从供电能力拆出
+ * 线损率 VO(馈线级线损率,状估口径
  * <p>
- * 供电能力 /indicator/capacity 只返回负载率/重过载,线损单独 /indicator/line-loss 查询,
- * 避免线损明细聚合拖慢主接口
+ * 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
+ * 查询结果按区间加权平均(Σ线损功率 / Σ总功率)
  *
  * @author lsl
  * @since 2026-08
@@ -25,12 +25,12 @@ import java.math.BigDecimal;
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
-@ApiModel(description = "线损电量")
-public class SeLineLossVO implements Serializable {
+@ApiModel(description = "线损")
+public class SeLineLossRateVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @ApiModelProperty(value = "线路损耗电量(kWh)")
+    @ApiModelProperty(value = "线损率(%)")
     @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
-    private BigDecimal lineLossKwh;
+    private BigDecimal lineLossRate;
 }

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

@@ -1,7 +1,7 @@
 package com.hdkj.lt.bf.service;
 
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
-import com.hdkj.lt.bf.entity.vo.SeLineLossVO;
+import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
@@ -56,5 +56,5 @@ public interface SeIndicatorService {
      * @param type     节点类型 2=区县 3=变电站 4=馈线
      * @param timeScope today/month/year
      */
-    SeLineLossVO queryLineLoss(String id, Integer type, String timeScope);
+    SeLineLossRateVO queryLineLoss(String id, Integer type, String timeScope);
 }

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

@@ -62,6 +62,10 @@ public class SeAlarmServiceImpl implements SeAlarmService {
     private static final List<String> VOLTAGE_TYPES = Arrays.asList(
             "voltage_over", "voltage_under", "voltage_over_general", "voltage_over_severe",
             "voltage_under_general", "voltage_under_severe");
+    private static final List<String> VOLTAGE_OVER_TYPES = Arrays.asList(
+            "voltage_over", "voltage_over_general", "voltage_over_severe");
+    private static final List<String> VOLTAGE_UNDER_TYPES = Arrays.asList(
+            "voltage_under", "voltage_under_general", "voltage_under_severe");
 
     // ============================================================
     // 1. 重过载列表
@@ -132,12 +136,22 @@ public class SeAlarmServiceImpl implements SeAlarmService {
 
         String alarmLevel = "feeder".equals(req.getAlarmLevel()) ? "feeder" : "mvtrans";
 
+        // 越限方向筛选: 1=越上限 2=越下限 0/不传=全量
+        Integer overtype = req.getOrderByOvertype();
+        List<String> overtypeFilter = null;
+        if (overtype != null && overtype == 1) {
+            overtypeFilter = VOLTAGE_OVER_TYPES;
+        } else if (overtype != null && overtype == 2) {
+            overtypeFilter = VOLTAGE_UNDER_TYPES;
+        }
+
         // 1. 实时电压事件(事件表,不分页全量拉取时间窗)
         List<FhzgSeVoltageEvent> volEvents = voltageEventMapper.selectList(
                 new LambdaQueryWrapper<FhzgSeVoltageEvent>()
                         .ge(FhzgSeVoltageEvent::getFirstOverTime, rangeStart)
                         .lt(FhzgSeVoltageEvent::getFirstOverTime, rangeEnd)
                         .eq(FhzgSeVoltageEvent::getAlarmLevel, alarmLevel)
+                        .in(overtypeFilter != null, FhzgSeVoltageEvent::getAlarmType, overtypeFilter)
                         .and(w -> applyVoltageFilter(w, req.getId(), req.getType()))
                         .like(StringUtils.isNotBlank(req.getFeederName()),
                                 FhzgSeVoltageEvent::getFeederName, req.getFeederName()));
@@ -151,6 +165,8 @@ public class SeAlarmServiceImpl implements SeAlarmService {
                     new LambdaQueryWrapper<FhzgSeBlockVoltageEvent>()
                             .ge(FhzgSeBlockVoltageEvent::getEventStartTime, rangeStart)
                             .lt(FhzgSeBlockVoltageEvent::getEventStartTime, rangeEnd)
+                            .eq(overtypeFilter != null, FhzgSeBlockVoltageEvent::getOverLimitType,
+                                    overtypeFilter == VOLTAGE_OVER_TYPES ? "02" : "01")
                             .and(w -> applyBlockVoltageFilter(w, req.getId(), req.getType()))
                             .like(StringUtils.isNotBlank(req.getFeederName()),
                                     FhzgSeBlockVoltageEvent::getFeederName, req.getFeederName()));

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

@@ -4,8 +4,7 @@ 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.FhzgSeLineLossDaily;
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
+import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
 import com.hdkj.lt.bf.entity.FhzgDwmVoltageOverLimitDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
@@ -14,13 +13,12 @@ import com.hdkj.lt.bf.entity.FhzgSeConsumerCountDaily;
 import com.hdkj.lt.bf.entity.FhzgSeConsumerRelation;
 import com.hdkj.lt.bf.entity.dto.TodayOutageData;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
-import com.hdkj.lt.bf.entity.vo.SeLineLossVO;
+import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDailyMapper;
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDetailMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeLineLossRateMapper;
 import com.hdkj.lt.bf.mapper.FhzgDwmVoltageOverLimitDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
@@ -63,8 +61,7 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeConsumerCountDailyMapper consumerCountDailyMapper;
     private final FhzgSeConsumerRelationMapper consumerRelationMapper;
-    private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
-    private final FhzgSeLineLossDailyMapper lineLossDailyMapper;
+    private final FhzgSeLineLossRateMapper lineLossRateMapper;
     private final FeederGroupApplication feederGroupApplication;
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
@@ -562,42 +559,47 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     }
 
     /**
-     * 线损电量(独立接口,从供电能力拆出):
-     * today → 明细表实时 SUM;
-     * month/year → 历史查日汇总表 [startDate, yesterday] + 今日明细实时补
+     * 线损(独立接口,从供电能力拆出):
+     * 按区间从 fhzg_se_line_loss_rate 加权平均(Σ线损功率 / Σ总功率 × 100)。
+     * today → 今日 0 点至今;month → 月初至今;year → 年初至今
      */
     @Override
-    public SeLineLossVO queryLineLoss(String id, Integer type, String timeScope) {
+    public SeLineLossRateVO queryLineLoss(String id, Integer type, String timeScope) {
         if (StringUtils.isBlank(id) || type == null || StringUtils.isBlank(timeScope)) {
             return null;
         }
 
-        BigDecimal lineLossKwh;
-        if ("today".equalsIgnoreCase(timeScope)) {
-            lineLossKwh = queryLineLossKwhFromDetail(id, type,
-                    LocalDate.now().atStartOfDay(), LocalDateTime.now());
-        } else {
-            // month/year:历史(日汇总 T+1,只到昨天)+ 今日(明细实时)
-            LocalDate today = LocalDate.now();
-            LocalDate startDate;
-            LocalDate endDate = today.minusDays(1);
-            if ("month".equalsIgnoreCase(timeScope)) {
-                startDate = today.withDayOfMonth(1);
-            } else {
-                startDate = LocalDate.of(today.getYear(), 1, 1);
-            }
-            BigDecimal histKwh = BigDecimal.ZERO;
-            if (!endDate.isBefore(startDate)) {
-                histKwh = queryLineLossKwhFromDaily(id, type, startDate, endDate);
-            }
-            BigDecimal todayKwh = queryLineLossKwhFromDetail(id, type,
-                    today.atStartOfDay(), LocalDateTime.now());
-            lineLossKwh = histKwh.add(todayKwh);
+        // TODO 历史线损(month/year):取中台线损日汇总表(建设中,未接入),暂不返回
+        if (!"today".equalsIgnoreCase(timeScope)) {
+            return null;
         }
 
-        return SeLineLossVO.builder()
-                .lineLossKwh(lineLossKwh.setScale(2, RoundingMode.HALF_UP))
-                .build();
+        LocalDateTime startTime = LocalDate.now().atStartOfDay();
+        LocalDateTime endTime = LocalDateTime.now();
+
+        QueryWrapper<FhzgSeLineLossRate> qw = new QueryWrapper<>();
+        qw.select("COALESCE(SUM(line_loss_power), 0) AS total_loss",
+                "COALESCE(SUM(total_power), 0) AS total_power")
+                .ge("snap_time", startTime)
+                .le("snap_time", endTime);
+        if (type == 2) {
+            qw.eq("county_id", id);
+        } else if (type == 3) {
+            qw.eq("subs_id", id);
+        } else if (type == 4) {
+            qw.eq("feeder_id", id);
+        }
+        List<Map<String, Object>> rows = lineLossRateMapper.selectMaps(qw);
+        if (rows == null || rows.isEmpty() || rows.get(0).get("total_power") == null
+                || new BigDecimal(rows.get(0).get("total_power").toString()).compareTo(BigDecimal.ZERO) <= 0) {
+            return SeLineLossRateVO.builder().lineLossRate(BigDecimal.ZERO).build();
+        }
+        BigDecimal totalLoss = new BigDecimal(rows.get(0).get("total_loss").toString());
+        BigDecimal totalPower = new BigDecimal(rows.get(0).get("total_power").toString());
+        BigDecimal rate = totalLoss.multiply(BigDecimal.valueOf(100))
+                .divide(totalPower, 2, RoundingMode.HALF_UP);
+
+        return SeLineLossRateVO.builder().lineLossRate(rate).build();
     }
 
     /**
@@ -880,48 +882,4 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
         }
         return 0;
     }
-
-    // ============================================================
-    // 8. 线损电量查询
-    // ============================================================
-
-    /**
-     * 今日线损电量:从明细表聚合 SUM(line_loss_kw) × 0.25h
-     */
-    private BigDecimal queryLineLossKwhFromDetail(String id, Integer type,
-                                                   LocalDateTime startTime, LocalDateTime endTime) {
-        QueryWrapper<FhzgSeLineLossDetail> qw = new QueryWrapper<>();
-        qw.select("COALESCE(SUM(line_loss_kw), 0) AS total_loss");
-        if (type == 2) qw.eq("county_id", id);
-        else if (type == 3) qw.eq("subs_id", id);
-        else if (type == 4) qw.eq("feeder_id", id);
-        qw.ge("snap_time", startTime).lt("snap_time", endTime);
-        List<Map<String, Object>> rows = lineLossDetailMapper.selectMaps(qw);
-        if (rows != null && !rows.isEmpty() && rows.get(0).get("total_loss") != null) {
-            BigDecimal sumKw = ((Number) rows.get(0).get("total_loss")).longValue() == 0
-                    ? BigDecimal.ZERO
-                    : new BigDecimal(rows.get(0).get("total_loss").toString());
-            // 每个断面间隔15min = 0.25h,线损功率 × 时间 = 电量
-            return sumKw.multiply(new BigDecimal("0.25")).setScale(2, RoundingMode.HALF_UP);
-        }
-        return BigDecimal.ZERO;
-    }
-
-    /**
-     * 历史线损电量:从日汇总表聚合 SUM(line_loss_kwh)
-     */
-    private BigDecimal queryLineLossKwhFromDaily(String id, Integer type,
-                                                  LocalDate startDate, LocalDate endDate) {
-        QueryWrapper<FhzgSeLineLossDaily> qw = new QueryWrapper<>();
-        qw.select("COALESCE(SUM(line_loss_kwh), 0) AS total_kwh");
-        if (type == 2) qw.eq("county_id", id);
-        else if (type == 3) qw.eq("subs_id", id);
-        else if (type == 4) qw.eq("feeder_id", id);
-        qw.ge("stat_date", startDate).le("stat_date", endDate);
-        List<Map<String, Object>> rows = lineLossDailyMapper.selectMaps(qw);
-        if (rows != null && !rows.isEmpty() && rows.get(0).get("total_kwh") != null) {
-            return new BigDecimal(rows.get(0).get("total_kwh").toString()).setScale(2, RoundingMode.HALF_UP);
-        }
-        return BigDecimal.ZERO;
-    }
 }

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

@@ -8,11 +8,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 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.DwdShbDsCableSegmentBase;
-import com.hdkj.lt.bf.entity.DwdShbDsSpanSegmentBase;
 import com.hdkj.lt.bf.entity.DwdShbDsTransformerBase;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
+import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
@@ -21,13 +19,11 @@ import com.hdkj.lt.bf.event.MvtransOverVoltageEvent;
 import com.hdkj.lt.bf.event.ReconTriggerEvent;
 import com.hdkj.lt.bf.mapper.FeederTopoMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDetailMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeLineLossRateMapper;
 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.DwdShbDsCableSegmentBaseMapper;
-import com.hdkj.lt.bf.mapper.DwdShbDsSpanSegmentBaseMapper;
 import com.hdkj.lt.bf.mapper.DwdShbDsTransformerBaseMapper;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.lt.core.bizms.modle.po.FeederTopo;
@@ -59,9 +55,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final FeederGroupApplication feederGroupApplication;
     private final ApplicationEventPublisher eventPublisher;
     private final FeederTopoMapper feederTopoMapper;
-    private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
-    private final DwdShbDsSpanSegmentBaseMapper spanSegmentMapper;
-    private final DwdShbDsCableSegmentBaseMapper cableSegmentMapper;
+    private final FhzgSeLineLossRateMapper lineLossRateMapper;
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
@@ -170,11 +164,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
             }
         }
 
-        // === 3. 线路线段 → 线损计算 ===
-        JSONArray segments = root.getJSONArray("periodSegmentSeResult");
-        if (segments != null && !segments.isEmpty()) {
-            processSegmentsForLineLoss(segments, snapTime, feederMap);
-        }
+        // === 3. 馈线线损率(状估口径)===
+        // 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
+        // 总功率=馈线块 seoPs(≈出线开关功率);配变块=0302+0110;中压用户接入点块=370000
+        processLineLossRate(root, snapTime, feederMap, advanceState);
 
         if (!batch.isEmpty()) {
             // 防重:查该断面时刻已有的所有 device_id(精确到秒,无需再 IN 过滤)
@@ -209,6 +202,13 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         if (feederId == null || feederId.isEmpty()) return null;  // 缺 psrId 直接丢弃,防 NULL 脏写
         String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
         BigDecimal loadRate = firstValue(f.getJSONArray("loadRates"));
+        // 负载率超过 200% 视为异常数据(状估量测异常),不计入计算:
+        // loadRate 置 null → 不触发电流事件、不参与负载率聚合统计
+        if (loadRate != null && loadRate.compareTo(SeMonitorThreshold.LOAD_RATE_ABNORMAL) > 0) {
+            log.warn("馈线负载率异常(>{}),不计入计算: feederId={}, loadRate={}",
+                    SeMonitorThreshold.LOAD_RATE_ABNORMAL, feederId, loadRate);
+            loadRate = null;
+        }
         BigDecimal voltage = firstValue(f.getJSONArray("seoUs"));
         BigDecimal voltageRatio = voltage != null
                 ? voltage.divide(SeMonitorThreshold.FEEDER_BASE_KV, 4, RoundingMode.HALF_UP) : null;
@@ -522,129 +522,118 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
 
     // ============================================================
-    // 线线损计算
+    // 线线损计算(状估口径)
     // ============================================================
 
     /**
-     * 处理线路线段断面数据,计算逐段线损功率并写入明细表
+     * 馈线线损率计算(新算法,替代旧的逐段电流法)
      * <p>
-     * line_loss_kW = 3000 × I_kA² × r_Ω_per_km × (L_m / 1000)
-     * 电阻率: dxd(导线段)=0.4132Ω/km, 0201(电缆段)=0.313Ω/km
-     * 段长度从 dwd_shb_ds_span_segment_base(导线段)/ dwd_shb_ds_cable_segment_base(电缆段)的 length 取(单位:米)
-     * SE 返回的 seoIs 为安培,先 /1000 换算成 kA
+     * 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
+     * <ul>
+     *   <li>总功率 = periodFeederSeResult[].seoPs(馈线状估有功 Σ,≈出线开关功率)</li>
+     *   <li>配变有功 = periodMVTransSeResult[].seoPs(0302+0110 全部配变 Σ)</li>
+     *   <li>中压用户接入点有功 = periodEnergyConsumerSeResult[].seoPs(370000 Σ)</li>
+     * </ul>
+     * 每断面每馈线一条写入 fhzg_se_line_loss_rate;总功率缺失/为 0 的馈线跳过(仅 log)。
+     * backfill(advanceState=false)同样计算写库,仅跳过状态机推进。
      */
-    private void processSegmentsForLineLoss(JSONArray segments, LocalDateTime snapTime, Map<String, String[]> feederMap) {
-        // 1. 按类型分组查段长度(档距段→span表,电缆段→cable表),过滤在运设备
-        Map<String, BigDecimal> lengthMap = new HashMap<>();
+    private void processLineLossRate(JSONObject root, LocalDateTime snapTime,
+                                     Map<String, String[]> feederMap, boolean advanceState) {
         try {
-            List<String> spanIds = new ArrayList<>();
-            List<String> cableIds = new ArrayList<>();
-            for (int i = 0; i < segments.size(); i++) {
-                JSONObject seg = segments.getJSONObject(i);
-                String pid = seg.getString("psrId");
-                String pt = seg.getString("psrType");
-                if (pid == null) continue;
-                if ("dxd".equals(pt)) spanIds.add(pid);
-                else if ("0201".equals(pt)) cableIds.add(pid);
-            }
-            if (!spanIds.isEmpty()) {
-                spanSegmentMapper.selectList(
-                        new LambdaQueryWrapper<DwdShbDsSpanSegmentBase>()
-                                .select(DwdShbDsSpanSegmentBase::getPsrId, DwdShbDsSpanSegmentBase::getLength)
-                                .in(DwdShbDsSpanSegmentBase::getPsrId, spanIds)
-                                .eq(DwdShbDsSpanSegmentBase::getPsrState, "20"))
-                        .forEach(r -> { if (r.getLength() != null) lengthMap.put(r.getPsrId(), r.getLength()); });
+            // 1. 配变有功:periodMVTransSeResult 按 feederId 汇总 seoPs
+            Map<String, BigDecimal> transPowerMap = new HashMap<>();
+            JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
+            if (mvtrans != null) {
+                for (int i = 0; i < mvtrans.size(); i++) {
+                    JSONObject m = mvtrans.getJSONObject(i);
+                    String feederId = m.getString("feederId");
+                    BigDecimal p = firstValue(m.getJSONArray("seoPs"));
+                    if (feederId == null || p == null) continue;
+                    transPowerMap.merge(feederId, p, BigDecimal::add);
+                }
             }
-            if (!cableIds.isEmpty()) {
-                cableSegmentMapper.selectList(
-                        new LambdaQueryWrapper<DwdShbDsCableSegmentBase>()
-                                .select(DwdShbDsCableSegmentBase::getPsrId, DwdShbDsCableSegmentBase::getLength)
-                                .in(DwdShbDsCableSegmentBase::getPsrId, cableIds)
-                                .eq(DwdShbDsCableSegmentBase::getPsrState, "20"))
-                        .forEach(r -> { if (r.getLength() != null) lengthMap.put(r.getPsrId(), r.getLength()); });
+
+            // 2. 中压用户接入点有功:periodEnergyConsumerSeResult 按 feederId 汇总 seoPs
+            Map<String, BigDecimal> consumerPowerMap = new HashMap<>();
+            JSONArray consumers = root.getJSONArray("periodEnergyConsumerSeResult");
+            if (consumers != null) {
+                for (int i = 0; i < consumers.size(); i++) {
+                    JSONObject c = consumers.getJSONObject(i);
+                    String feederId = c.getString("feederId");
+                    BigDecimal p = firstValue(c.getJSONArray("seoPs"));
+                    if (feederId == null || p == null) continue;
+                    consumerPowerMap.merge(feederId, p, BigDecimal::add);
+                }
             }
-        } catch (Exception e) {
-            log.warn("查询段长度失败", e);
-        }
 
-        // 3. 逐段计算线损
-        List<FhzgSeLineLossDetail> batch = new ArrayList<>();
-        LocalDateTime now = LocalDateTime.now();
-        for (int i = 0; i < segments.size(); i++) {
-            JSONObject seg = segments.getJSONObject(i);
-            String psrId = seg.getString("psrId");
-            String psrType = seg.getString("psrType");
-            String feederId = seg.getString("feederId");
-            String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
-
-            BigDecimal current = firstValue(seg.getJSONArray("seoIs"));
-            if (current == null || current.compareTo(BigDecimal.ZERO) <= 0) continue;
-
-            BigDecimal sbLength = lengthMap.get(psrId);
-            if (sbLength == null || sbLength.compareTo(BigDecimal.ZERO) <= 0) continue;
-
-            // 电阻率
-            BigDecimal resistivity;
-            if ("0201".equals(psrType)) {
-                resistivity = new BigDecimal("0.313");      // 电缆段
-            } else if ("dxd".equals(psrType)) {
-                resistivity = new BigDecimal("0.4132");     // 导线段
-            } else {
-                log.debug("未知段类型跳过 psrType={}, psrId={}", psrType, psrId);
-                continue;
+            // 3. 馈线块:总功率 + 逐馈线计算线损率
+            JSONArray feeders = root.getJSONArray("periodFeederSeResult");
+            if (feeders == null || feeders.isEmpty()) {
+                return;
             }
+            List<FhzgSeLineLossRate> batch = new ArrayList<>();
+            LocalDateTime now = LocalDateTime.now();
+            for (int i = 0; i < feeders.size(); i++) {
+                JSONObject f = feeders.getJSONObject(i);
+                String feederId = f.getString("psrId");
+                if (feederId == null || feederId.isEmpty()) continue;
 
-            // SE 返回电流为安培,换算成 kA:I_kA = I_A / 1000
-//            BigDecimal currentKa = current.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
-            BigDecimal currentKa = current.setScale(8, RoundingMode.HALF_UP);
-
-            // 3000 × I_kA² × r × (L_m / 1000) = 3 × I_kA² × r × L_m
-            BigDecimal lengthKm = sbLength.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
-            BigDecimal loss = BigDecimal.valueOf(3000)
-                    .multiply(currentKa.pow(2))
-                    .multiply(resistivity)
-                    .multiply(lengthKm)
-                    .setScale(4, RoundingMode.HALF_UP);
-
-            FhzgSeLineLossDetail detail = FhzgSeLineLossDetail.builder()
-                    .snapTime(snapTime)
-                    .feederId(feederId)
-                    .psrId(psrId)
-                    .psrType(psrType)
-                    .countyId(loc.length > 1 ? loc[1] : "")
-                    .subsId(loc.length > 0 ? loc[0] : "")
-                    .currentKa(currentKa)
-                    .sbLengthM(sbLength)
-                    .resistivity(resistivity)
-                    .lineLossKw(loss)
-                    .createTime(now)
-                    .build();
-            batch.add(detail);
-        }
+                BigDecimal totalPower = firstValue(f.getJSONArray("seoPs"));
+                if (totalPower == null || totalPower.compareTo(BigDecimal.ZERO) <= 0) {
+                    log.debug("馈线总功率缺失/为0, 跳过线损率: feederId={}", feederId);
+                    continue;
+                }
 
-        if (!batch.isEmpty()) {
-            // 防重:查该断面时刻已有的段(避免重复跑同一断面时叠加)
-            Set<String> existingPsrIds = new HashSet<>();
-            lineLossDetailMapper.selectList(
-                    new LambdaQueryWrapper<FhzgSeLineLossDetail>()
-                            .select(FhzgSeLineLossDetail::getPsrId)
-                            .eq(FhzgSeLineLossDetail::getSnapTime, snapTime)
-            ).forEach(e -> existingPsrIds.add(e.getPsrId()));
-
-            List<FhzgSeLineLossDetail> toInsert = new ArrayList<>();
-            for (FhzgSeLineLossDetail d : batch) {
-                if (existingPsrIds.contains(d.getPsrId())) continue;
-                toInsert.add(d);
+                BigDecimal transPower = transPowerMap.getOrDefault(feederId, BigDecimal.ZERO);
+                BigDecimal consumerPower = consumerPowerMap.getOrDefault(feederId, BigDecimal.ZERO);
+                BigDecimal lineLossPower = totalPower.subtract(transPower).subtract(consumerPower);
+                BigDecimal lineLossRate = lineLossPower.multiply(BigDecimal.valueOf(100))
+                        .divide(totalPower, 4, RoundingMode.HALF_UP);
+
+                String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
+                batch.add(FhzgSeLineLossRate.builder()
+                        .snapTime(snapTime)
+                        .feederId(feederId)
+                        .feederName(loc.length > 2 ? loc[2] : null)
+                        .subsId(loc.length > 0 ? loc[0] : "")
+                        .countyId(loc.length > 1 ? loc[1] : "")
+                        .totalPower(totalPower)
+                        .transPower(transPower)
+                        .consumerPower(consumerPower)
+                        .lineLossPower(lineLossPower)
+                        .lineLossRate(lineLossRate)
+                        .createTime(now)
+                        .build());
             }
 
-            int inserted = 0;
-            if (!toInsert.isEmpty()) {
-                inserted = insertBatchSharded(rows -> lineLossDetailMapper.insertBatch(rows), toInsert);
+            if (!batch.isEmpty()) {
+                // 防重:查该断面时刻已有的馈线(避免重复跑同一断面时叠加)
+                Set<String> existingFeederIds = new HashSet<>();
+                lineLossRateMapper.selectList(
+                        new LambdaQueryWrapper<FhzgSeLineLossRate>()
+                                .select(FhzgSeLineLossRate::getFeederId)
+                                .eq(FhzgSeLineLossRate::getSnapTime, snapTime)
+                ).forEach(e -> existingFeederIds.add(e.getFeederId()));
+
+                List<FhzgSeLineLossRate> toInsert = new ArrayList<>();
+                for (FhzgSeLineLossRate d : batch) {
+                    if (existingFeederIds.contains(d.getFeederId())) continue;
+                    toInsert.add(d);
+                }
+
+                int inserted = 0;
+                if (!toInsert.isEmpty()) {
+                    inserted = insertBatchSharded(rows -> {
+                        toInsert.forEach(lineLossRateMapper::insert);
+                        return toInsert.size();
+                    }, toInsert);
+                }
+                log.info("馈线线损率计算完成 snapTime={}, 写入{}条, 跳过{}条(无功率{}条/重复{}条)",
+                        snapTime, inserted, batch.size() - inserted,
+                        batch.size() - toInsert.size(), existingFeederIds.size());
             }
-            int skippedNoData = segments.size() - batch.size();
-            int skippedDup = batch.size() - toInsert.size();
-            log.info("线路线损计算完成 snapTime={}, 写入{}条, 跳过{}条(无数据{}条/重复{}条)",
-                    snapTime, inserted, skippedNoData + skippedDup, skippedNoData, skippedDup);
+        } catch (Exception e) {
+            log.warn("馈线线损率计算失败 snapTime={}", snapTime, e);
         }
     }
 
@@ -914,11 +903,11 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         int deleted = snapshotDetailMapper.delete(
                 new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
                         .eq(FhzgSeSnapshotDetail::getSnapshotTime, snapTime));
-        // 线损明细同步删除(否则防重会跳过插入,保留旧值导致与断面不一致)
-        int deletedLoss = lineLossDetailMapper.delete(
-                new LambdaQueryWrapper<FhzgSeLineLossDetail>()
-                        .eq(FhzgSeLineLossDetail::getSnapTime, snapTime));
-        log.info("状估覆盖更新 snapTime={}, 删除断面{}条, 删除线损{}条", snapTime, deleted, deletedLoss);
+        // 线损同步删除(否则防重会跳过插入,保留旧值导致与断面不一致)
+        int deletedLoss = lineLossRateMapper.delete(
+                new LambdaQueryWrapper<FhzgSeLineLossRate>()
+                        .eq(FhzgSeLineLossRate::getSnapTime, snapTime));
+        log.info("状估覆盖更新 snapTime={}, 删除断面{}条, 删除线损{}条", snapTime, deleted, deletedLoss);
 
         // 重新走补数链路写入(此时防重逻辑不会跳过任何记录)
         // 注意:覆盖更新用于补历史断面,只写数据不推状态机——防止状态机时间倒退、

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

@@ -1,35 +0,0 @@
-package com.hdkj.lt.job.task;
-
-import com.hdkj.lt.bf.scheduler.SeLineLossDailyScheduler;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDate;
-
-/**
- * 线路线损日汇总定时任务(凌晨1点聚合昨天)
- * <p>
- * sys_job 配置:invokeTarget = seLineLossDaily.noParams(),cron = 0 0 1 * * ?
- *
- * @author lsl
- * @since 2026-08-05
- */
-@Slf4j
-@Component("seLineLossDaily")
-@RequiredArgsConstructor
-public class SeLineLossDailyTask {
-
-    private final SeLineLossDailyScheduler seLineLossDailyScheduler;
-
-    public void noParams() {
-        LocalDate yesterday = LocalDate.now().minusDays(1);
-        log.info("[线路线损日汇总] 开始处理 {} 数据", yesterday);
-        try {
-            String result = seLineLossDailyScheduler.aggregateForDate(yesterday);
-            log.info("[线路线损日汇总] {} {}", yesterday, result);
-        } catch (Exception e) {
-            log.error("[线路线损日汇总] {} 处理失败", yesterday, e);
-        }
-    }
-}