瀏覽代碼

运行部分修正

lisonglin 1 周之前
父節點
當前提交
afd328e5cd
共有 14 個文件被更改,包括 456 次插入484 次删除
  1. 4 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/constants/OptimizationConstants.java
  2. 2 38
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  3. 30 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsCableSegmentBase.java
  4. 30 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsSpanSegmentBase.java
  5. 5 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsTransformerBase.java
  6. 0 156
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/IndicatorDashboardVO.java
  7. 4 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeCapacityDashboardVO.java
  8. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsCableSegmentBaseMapper.java
  9. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsSpanSegmentBaseMapper.java
  10. 0 24
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/IndicatorDashboardService.java
  11. 0 95
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/IndicatorDashboardServiceImpl.java
  12. 233 121
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  13. 43 27
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  14. 87 23
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java

+ 4 - 0
services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/constants/OptimizationConstants.java

@@ -7,4 +7,8 @@ package com.hdkj.lt.bf.constants;
  */
 public class OptimizationConstants {
     public static final String CACHE_KEY_FEEDER_GROUP_TREE = "feeder_group_tree";
+    /** 网架仪表盘整包缓存前缀(key = grid_dashboard:{type}:{id}:{yyyymm}) */
+    public static final String CACHE_KEY_GRID_DASHBOARD_PREFIX = "grid_dashboard:";
+    /** 网架仪表盘缓存 TTL(分钟):静态月数据 + 嵌运行态今日数据,与断面节奏对齐 */
+    public static final long CACHE_TTL_GRID_DASHBOARD_MINUTES = 15L;
 }

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

@@ -7,7 +7,6 @@ import com.hdkj.lt.bf.adapter.SiStateEstimationClient;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
 import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
-import com.hdkj.lt.bf.entity.vo.IndicatorDashboardVO;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
@@ -15,9 +14,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.entity.vo.SeVoltageIndexVO;
 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;
 import com.hdkj.lt.bf.service.SeSnapshotService;
@@ -38,9 +35,9 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 运行态指标 仪表盘 Controller
+ * 运行态指标 Controller
  * <p>
- * POST /indicator/dashboard — 返回综合态势仪表盘数据
+ * 三个独立接口:/indicator/outage、/indicator/voltage、/indicator/capacity
  *
  * @author lsl
  * @since 2026-07-21
@@ -51,29 +48,12 @@ import java.util.Map;
 @RequestMapping("/indicator")
 public class IndicatorController extends BaseController {
 
-    private final IndicatorDashboardService indicatorDashboardService;
     private final SeIndicatorService seIndicatorService;
     private final SeAlarmService seAlarmService;
     private final SiStateEstimationClient siStateEstimationClient;
     private final SeSnapshotService seSnapshotService;
     private final SeCapacityDailyScheduler seCapacityDailyScheduler;
 
-    /**
-     * 查询运行态仪表盘
-     *
-     * @param params {id: 节点ID, type: 节点类型(2/3/4)}
-     */
-//    @PostMapping("/dashboard")
-//    public ApiResponse<IndicatorDashboardVO> dashboard(@RequestBody Map<String, String> params) {
-//        String id = params.get("id");
-//        String type = params.get("type");
-//        IndicatorDashboardVO vo = indicatorDashboardService.queryDashboard(id, type);
-//        if (vo == null) {
-//            return ApiResponse.fail("参数不合法或无数据");
-//        }
-//        return ApiResponse.success(vo);
-//    }
-
     /**
      * 停电时户数(无时间维度切换)
      *
@@ -105,22 +85,6 @@ public class IndicatorController extends BaseController {
         return ApiResponse.success(seIndicatorService.queryVoltage(id, Integer.parseInt(type), timeScope));
     }
 
-    /**
-     * IEC 用户电压指数(day/month/year)
-     *
-     * @param params {id, type(2/3/4), timeScope(day/month/year)}
-     */
-    @PostMapping("/voltage/index")
-    public ApiResponse<SeVoltageIndexVO> voltageIndex(@RequestBody Map<String, String> params) {
-        String id = params.get("id");
-        String type = params.get("type");
-        String timeScope = params.get("timeScope");
-        if (id == null || type == null || timeScope == null) {
-            return ApiResponse.fail("参数缺失");
-        }
-        return ApiResponse.success(seIndicatorService.queryVoltageIndex(id, Integer.parseInt(type), timeScope));
-    }
-
     /**
      * 供电能力(today/month/year)
      *

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

@@ -0,0 +1,30 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 配电电缆段台账 — 仅映射线损计算需要的字段
+ * @TableName dwd_shb_ds_cable_segment_base
+ */
+@TableName("dwd_shb_ds_cable_segment_base")
+@Data
+public class DwdShbDsCableSegmentBase implements Serializable {
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @TableId
+    private String psrId;
+
+    /** 电缆长度(米) */
+    private BigDecimal length;
+
+    /** 运行状态(20=在运) */
+    private String psrState;
+}

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

@@ -0,0 +1,30 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 配电档距段(导线段)台账 — 仅映射线损计算需要的字段
+ * @TableName dwd_shb_ds_span_segment_base
+ */
+@TableName("dwd_shb_ds_span_segment_base")
+@Data
+public class DwdShbDsSpanSegmentBase implements Serializable {
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @TableId
+    private String psrId;
+
+    /** 档距段长度(米) */
+    private BigDecimal length;
+
+    /** 运行状态(20=在运) */
+    private String psrState;
+}

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

@@ -28,4 +28,9 @@ public class DwdShbDsTransformerBase implements Serializable {
      * 设备名称
      */
     private String name;
+
+    /**
+     * 资源状态(20=在运)— 仅查在运设备时过滤
+     */
+    private String psrState;
 }

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

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

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

@@ -56,6 +56,10 @@ public class SeCapacityDashboardVO implements Serializable {
     @ApiModelProperty(value = "总事件次数")
     private Integer totalEventCount;
 
+    @ApiModelProperty(value = "线路损耗电量(kWh)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal lineLossKwh;
+
     // ============================================================
     // 今日馈线96点时序(仅馈线type=4+today时返回)
     // ============================================================

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

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

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

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

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

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

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

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

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

@@ -4,6 +4,8 @@ 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.FhzgDwmVoltageOverLimitDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
@@ -16,6 +18,8 @@ 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.FhzgDwmVoltageOverLimitDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
@@ -58,6 +62,8 @@ 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 FeederGroupApplication feederGroupApplication;
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
@@ -103,11 +109,21 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             return null;
         }
 
+        SeVoltageDashboardVO vo;
         if ("today".equalsIgnoreCase(timeScope)) {
-            return queryVoltageToday(id, type);
+            vo = queryVoltageToday(id, type);
         } else {
-            return queryVoltageHistory(id, type, timeScope);
+            vo = queryVoltageHistory(id, type, timeScope);
         }
+
+        // IEC 用户电压指数:与 timeScope 一致(today→day, month/year 同名)
+        String indexScope = "today".equalsIgnoreCase(timeScope) ? "day" : timeScope;
+        SeVoltageIndexVO indexVo = queryVoltageIndex(id, type, indexScope);
+        if (indexVo != null) {
+            vo.setIecIndex(indexVo.getVoltageIndex());
+        }
+
+        return vo;
     }
 
     /**
@@ -206,7 +222,6 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
      */
     private SeVoltageDashboardVO queryVoltageHistory(String id, Integer type, String timeScope) {
         LocalDate today = LocalDate.now();
-        LocalDate endDate;
         LocalDate startDate;
 
         if ("month".equalsIgnoreCase(timeScope)) {
@@ -217,86 +232,95 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
 
         String dimLevel = resolveDimLevel(type);
 
-        // 查中台日汇总表最新日期,判断是否需要补昨日
+        // 中台覆盖到哪天(T-2,动态自适应);无数据时 midEnd < startDate → 中台贡献为 0
         String maxOverDate = queryDailyMaxOverDate(dimLevel, id, type);
-        String yesterdayStr = today.minusDays(1).format(DATE_FMT);
-        boolean needFillYesterday = (maxOverDate == null || maxOverDate.compareTo(yesterdayStr) < 0);
-
-        if (needFillYesterday) {
-            endDate = today.minusDays(2); // 中台只到T-2,补昨日T-1
-        } else {
-            endDate = today.minusDays(1); // 中台已有昨天
-        }
-
-        // 如果 endDate < startDate,返回空
-        if (endDate.isBefore(startDate)) {
-            return SeVoltageDashboardVO.builder().build();
+        LocalDate midEnd = startDate.minusDays(1);
+        if (maxOverDate != null) {
+            try {
+                LocalDate mo = LocalDate.parse(maxOverDate, DATE_FMT);
+                midEnd = mo.isAfter(today) ? today : mo;
+            } catch (Exception e) {
+                log.warn("中台 over_date 解析失败: {}, 忽略中台贡献", maxOverDate);
+            }
         }
 
-        // 一条 SQL 聚合 16个count字段 + total_over_limit_hours
-        QueryWrapper<FhzgDwmVoltageOverLimitDaily> qw = new QueryWrapper<>();
-        qw.select(
-                "COALESCE(SUM(over_upper_severe_tg_count),0) AS over_upper_severe_tg_count",
-                "COALESCE(SUM(over_upper_severe_cust_count),0) AS over_upper_severe_cust_count",
-                "COALESCE(SUM(over_upper_severe_feeder_count),0) AS over_upper_severe_feeder_count",
-                "COALESCE(SUM(over_upper_severe_generatrix_count),0) AS over_upper_severe_generatrix_count",
-                "COALESCE(SUM(over_upper_mild_tg_count),0) AS over_upper_mild_tg_count",
-                "COALESCE(SUM(over_upper_mild_cust_count),0) AS over_upper_mild_cust_count",
-                "COALESCE(SUM(over_upper_mild_feeder_count),0) AS over_upper_mild_feeder_count",
-                "COALESCE(SUM(over_upper_mild_generatrix_count),0) AS over_upper_mild_generatrix_count",
-                "COALESCE(SUM(over_lower_severe_tg_count),0) AS over_lower_severe_tg_count",
-                "COALESCE(SUM(over_lower_severe_cust_count),0) AS over_lower_severe_cust_count",
-                "COALESCE(SUM(over_lower_severe_feeder_count),0) AS over_lower_severe_feeder_count",
-                "COALESCE(SUM(over_lower_severe_generatrix_count),0) AS over_lower_severe_generatrix_count",
-                "COALESCE(SUM(over_lower_mild_tg_count),0) AS over_lower_mild_tg_count",
-                "COALESCE(SUM(over_lower_mild_cust_count),0) AS over_lower_mild_cust_count",
-                "COALESCE(SUM(over_lower_mild_feeder_count),0) AS over_lower_mild_feeder_count",
-                "COALESCE(SUM(over_lower_mild_generatrix_count),0) AS over_lower_mild_generatrix_count",
-                "COALESCE(SUM(total_over_limit_hours),0) AS total_over_limit_hours")
-                .eq("dim_level", dimLevel)
-                .ge("over_date", startDate.format(DATE_FMT))
-                .le("over_date", endDate.format(DATE_FMT));
-
-        // 维度过滤
-        if (type == 2) {
-            qw.eq("maintainer_id", id);
-        } else if (type == 3) {
-            qw.eq("substation_id", id);
-        } else if (type == 4) {
-            qw.eq("feeder_id", id);
-        }
+        int overUpperSevereTg = 0, overUpperSevereCust = 0, overUpperSevereFeeder = 0, overUpperSevereGen = 0;
+        int overUpperMildTg = 0, overUpperMildCust = 0, overUpperMildFeeder = 0, overUpperMildGen = 0;
+        int overLowerSevereTg = 0, overLowerSevereCust = 0, overLowerSevereFeeder = 0, overLowerSevereGen = 0;
+        int overLowerMildTg = 0, overLowerMildCust = 0, overLowerMildFeeder = 0, overLowerMildGen = 0;
+        BigDecimal totalHours = BigDecimal.ZERO;
+
+        // ===== 1. 中台贡献区间 [startDate, midEnd](T-2 滞后,月初/年初可能为空)=====
+        if (!midEnd.isBefore(startDate)) {
+            // 一条 SQL 聚合 16个count字段 + total_over_limit_hours
+            QueryWrapper<FhzgDwmVoltageOverLimitDaily> qw = new QueryWrapper<>();
+            qw.select(
+                    "COALESCE(SUM(over_upper_severe_tg_count),0) AS over_upper_severe_tg_count",
+                    "COALESCE(SUM(over_upper_severe_cust_count),0) AS over_upper_severe_cust_count",
+                    "COALESCE(SUM(over_upper_severe_feeder_count),0) AS over_upper_severe_feeder_count",
+                    "COALESCE(SUM(over_upper_severe_generatrix_count),0) AS over_upper_severe_generatrix_count",
+                    "COALESCE(SUM(over_upper_mild_tg_count),0) AS over_upper_mild_tg_count",
+                    "COALESCE(SUM(over_upper_mild_cust_count),0) AS over_upper_mild_cust_count",
+                    "COALESCE(SUM(over_upper_mild_feeder_count),0) AS over_upper_mild_feeder_count",
+                    "COALESCE(SUM(over_upper_mild_generatrix_count),0) AS over_upper_mild_generatrix_count",
+                    "COALESCE(SUM(over_lower_severe_tg_count),0) AS over_lower_severe_tg_count",
+                    "COALESCE(SUM(over_lower_severe_cust_count),0) AS over_lower_severe_cust_count",
+                    "COALESCE(SUM(over_lower_severe_feeder_count),0) AS over_lower_severe_feeder_count",
+                    "COALESCE(SUM(over_lower_severe_generatrix_count),0) AS over_lower_severe_generatrix_count",
+                    "COALESCE(SUM(over_lower_mild_tg_count),0) AS over_lower_mild_tg_count",
+                    "COALESCE(SUM(over_lower_mild_cust_count),0) AS over_lower_mild_cust_count",
+                    "COALESCE(SUM(over_lower_mild_feeder_count),0) AS over_lower_mild_feeder_count",
+                    "COALESCE(SUM(over_lower_mild_generatrix_count),0) AS over_lower_mild_generatrix_count",
+                    "COALESCE(SUM(total_over_limit_hours),0) AS total_over_limit_hours")
+                    .eq("dim_level", dimLevel)
+                    .ge("over_date", startDate.format(DATE_FMT))
+                    .le("over_date", midEnd.format(DATE_FMT));
+
+            // 维度过滤
+            if (type == 2) {
+                qw.eq("maintainer_id", id);
+            } else if (type == 3) {
+                qw.eq("substation_id", id);
+            } else if (type == 4) {
+                qw.eq("feeder_id", id);
+            }
 
-        List<FhzgDwmVoltageOverLimitDaily> records = voltageDailyMapper.selectList(qw);
-        FhzgDwmVoltageOverLimitDaily r = (records != null && !records.isEmpty()) ? records.get(0) : null;
-
-        int overUpperSevereTg = r != null ? safeInt(r.getOverUpperSevereTgCount()) : 0;
-        int overUpperSevereCust = r != null ? safeInt(r.getOverUpperSevereCustCount()) : 0;
-        int overUpperSevereFeeder = r != null ? safeInt(r.getOverUpperSevereFeederCount()) : 0;
-        int overUpperSevereGen = r != null ? safeInt(r.getOverUpperSevereGeneratrixCount()) : 0;
-        int overUpperMildTg = r != null ? safeInt(r.getOverUpperMildTgCount()) : 0;
-        int overUpperMildCust = r != null ? safeInt(r.getOverUpperMildCustCount()) : 0;
-        int overUpperMildFeeder = r != null ? safeInt(r.getOverUpperMildFeederCount()) : 0;
-        int overUpperMildGen = r != null ? safeInt(r.getOverUpperMildGeneratrixCount()) : 0;
-        int overLowerSevereTg = r != null ? safeInt(r.getOverLowerSevereTgCount()) : 0;
-        int overLowerSevereCust = r != null ? safeInt(r.getOverLowerSevereCustCount()) : 0;
-        int overLowerSevereFeeder = r != null ? safeInt(r.getOverLowerSevereFeederCount()) : 0;
-        int overLowerSevereGen = r != null ? safeInt(r.getOverLowerSevereGeneratrixCount()) : 0;
-        int overLowerMildTg = r != null ? safeInt(r.getOverLowerMildTgCount()) : 0;
-        int overLowerMildCust = r != null ? safeInt(r.getOverLowerMildCustCount()) : 0;
-        int overLowerMildFeeder = r != null ? safeInt(r.getOverLowerMildFeederCount()) : 0;
-        int overLowerMildGen = r != null ? safeInt(r.getOverLowerMildGeneratrixCount()) : 0;
-        BigDecimal totalHours = r != null && r.getTotalOverLimitHours() != null
-                ? r.getTotalOverLimitHours() : BigDecimal.ZERO;
-
-        // 补昨日:中台无昨日数据时,从事件表SQL聚合,按 alarm_level 分流
-        if (needFillYesterday) {
-            LocalDateTime yesterdayStart = today.minusDays(1).atStartOfDay();
-            LocalDateTime yesterdayEnd = today.minusDays(1).atTime(23, 59, 59);
+            List<FhzgDwmVoltageOverLimitDaily> records = voltageDailyMapper.selectList(qw);
+            FhzgDwmVoltageOverLimitDaily r = (records != null && !records.isEmpty()) ? records.get(0) : null;
+
+            overUpperSevereTg = r != null ? safeInt(r.getOverUpperSevereTgCount()) : 0;
+            overUpperSevereCust = r != null ? safeInt(r.getOverUpperSevereCustCount()) : 0;
+            overUpperSevereFeeder = r != null ? safeInt(r.getOverUpperSevereFeederCount()) : 0;
+            overUpperSevereGen = r != null ? safeInt(r.getOverUpperSevereGeneratrixCount()) : 0;
+            overUpperMildTg = r != null ? safeInt(r.getOverUpperMildTgCount()) : 0;
+            overUpperMildCust = r != null ? safeInt(r.getOverUpperMildCustCount()) : 0;
+            overUpperMildFeeder = r != null ? safeInt(r.getOverUpperMildFeederCount()) : 0;
+            overUpperMildGen = r != null ? safeInt(r.getOverUpperMildGeneratrixCount()) : 0;
+            overLowerSevereTg = r != null ? safeInt(r.getOverLowerSevereTgCount()) : 0;
+            overLowerSevereCust = r != null ? safeInt(r.getOverLowerSevereCustCount()) : 0;
+            overLowerSevereFeeder = r != null ? safeInt(r.getOverLowerSevereFeederCount()) : 0;
+            overLowerSevereGen = r != null ? safeInt(r.getOverLowerSevereGeneratrixCount()) : 0;
+            overLowerMildTg = r != null ? safeInt(r.getOverLowerMildTgCount()) : 0;
+            overLowerMildCust = r != null ? safeInt(r.getOverLowerMildCustCount()) : 0;
+            overLowerMildFeeder = r != null ? safeInt(r.getOverLowerMildFeederCount()) : 0;
+            overLowerMildGen = r != null ? safeInt(r.getOverLowerMildGeneratrixCount()) : 0;
+            totalHours = r != null && r.getTotalOverLimitHours() != null
+                    ? r.getTotalOverLimitHours() : BigDecimal.ZERO;
+        }
+
+        // ===== 2. 事件表补差区间 [max(midEnd+1, startDate), today](跨月/跨年不越界)=====
+        LocalDate fillStart = midEnd.plusDays(1);
+        if (fillStart.isBefore(startDate)) {
+            fillStart = startDate;
+        }
+        if (!fillStart.isAfter(today)) {
+            LocalDateTime fillStartTime = fillStart.atStartOfDay();
+            LocalDateTime fillEndTime = today.plusDays(1).atStartOfDay();
 
             QueryWrapper<FhzgSeVoltageEvent> agg = new QueryWrapper<>();
             agg.select("alarm_level", "alarm_type", "COUNT(*) AS cnt")
-                    .ge("trigger_time", yesterdayStart)
-                    .le("trigger_time", yesterdayEnd)
+                    .ge("trigger_time", fillStartTime)
+                    .lt("trigger_time", fillEndTime)
                     .eq("status", 2)
                     .groupBy("alarm_level", "alarm_type");
             if (type == 2) agg.eq("county_id", id);
@@ -416,73 +440,113 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             vo.setHeavyLoadRate(BigDecimal.ZERO);
             vo.setOverloadRate(BigDecimal.ZERO);
         }
+
+        // 3. 线损电量:从明细表聚合(今日未刷日汇总,用明细实时算)
+        vo.setLineLossKwh(queryLineLossKwhFromDetail(id, type, startTime, endTime));
+
         return vo;
     }
 
     /**
-     * 历史供电能力:负载率从日汇总表查,重过载次数从事件表统计
-     * month = 月初 ~ 昨天;year = 1.1 ~ 昨天
+     * 历史供电能力(分层口径):
+     * 历史部分(T+1)查日汇总表 [startDate, yesterday] —— 今天尚未入表;
+     * 今日部分实时补:负载率从断面明细聚合、重过载次数从事件表统计、线损从明细 SUM。
+     * month = 月初 ~ 今天;year = 1.1 ~ 今天
      */
     private SeCapacityDashboardVO queryCapacityHistory(String id, Integer type, String timeScope) {
         LocalDate today = LocalDate.now();
         LocalDate startDate;
-        LocalDate endDate;
+        LocalDate endDate = today.minusDays(1);
 
         if ("month".equalsIgnoreCase(timeScope)) {
             startDate = today.withDayOfMonth(1);
         } else {
             startDate = LocalDate.of(today.getYear(), 1, 1);
         }
-        endDate = today.minusDays(1);
 
-        if (endDate.isBefore(startDate)) {
-            return SeCapacityDashboardVO.builder().build();
-        }
+        // 历史部分统计(日汇总表 T+1,只到昨天)
+        BigDecimal histMaxLoadRate = null;
+        BigDecimal histSumAvg = BigDecimal.ZERO;
+        int histDays = 0;
+        long sumHeavy = 0;
+        long sumOverload = 0;
+        BigDecimal histLineLossKwh = BigDecimal.ZERO;
 
-        // 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);
+        if (!endDate.isBefore(startDate)) {
+            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()) {
-            vo = SeCapacityDashboardVO.builder().build();
-        } else {
-            BigDecimal maxLoadRate = BigDecimal.ZERO;
-            BigDecimal sumAvg = BigDecimal.ZERO;
-            long sumHeavy = 0;
-            long sumOverload = 0;
             for (FhzgSeCapacityDaily d : dailyRecords) {
-                if (d.getMaxLoadRate() != null && d.getMaxLoadRate().compareTo(maxLoadRate) > 0) {
-                    maxLoadRate = d.getMaxLoadRate();
+                if (d.getMaxLoadRate() != null
+                        && (histMaxLoadRate == null || d.getMaxLoadRate().compareTo(histMaxLoadRate) > 0)) {
+                    histMaxLoadRate = d.getMaxLoadRate();
                 }
                 if (d.getAvgLoadRate() != null) {
-                    sumAvg = sumAvg.add(d.getAvgLoadRate());
+                    histSumAvg = histSumAvg.add(d.getAvgLoadRate());
+                    histDays++;
                 }
                 // 重过载事件数:日统计表存的就是当日事件表事件数,直接累加
                 if (d.getHeavyCount() != null) sumHeavy += d.getHeavyCount();
                 if (d.getOverloadCount() != null) sumOverload += d.getOverloadCount();
             }
-            BigDecimal avgLoadRate = sumAvg.divide(BigDecimal.valueOf(dailyRecords.size()), 2, RoundingMode.HALF_UP);
-            int totalEvents = (int) (sumHeavy + sumOverload);
-            vo = SeCapacityDashboardVO.builder()
-                    .maxLoadRate(maxLoadRate)
-                    .avgLoadRate(avgLoadRate)
-                    .heavyLoadEventCount((int) sumHeavy)
-                    .overloadEventCount((int) sumOverload)
-                    .totalEventCount(totalEvents)
-                    .build();
-            if (totalEvents > 0) {
-                vo.setHeavyLoadRate(new BigDecimal(sumHeavy * 100)
-                        .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
-                vo.setOverloadRate(new BigDecimal(sumOverload * 100)
-                        .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
-            } else {
-                vo.setHeavyLoadRate(BigDecimal.ZERO);
-                vo.setOverloadRate(BigDecimal.ZERO);
-            }
+            // 线损电量(历史部分)
+            histLineLossKwh = queryLineLossKwhFromDaily(id, type, startDate, endDate);
+        }
+
+        // 今日部分实时聚合(断面明细 + 事件表 + 线损明细)
+        SeCapacityDashboardVO todayVo = queryCapacityToday(id, type);
+        BigDecimal todayMaxLoadRate = todayVo != null ? todayVo.getMaxLoadRate() : null;
+        BigDecimal todayAvgLoadRate = todayVo != null ? todayVo.getAvgLoadRate() : null;
+        int todayHeavy = todayVo != null ? todayVo.getHeavyLoadEventCount() : 0;
+        int todayOverload = todayVo != null ? todayVo.getOverloadEventCount() : 0;
+        BigDecimal todayLineLoss = todayVo != null ? todayVo.getLineLossKwh() : null;
+
+        // 合并
+        BigDecimal maxLoadRate = histMaxLoadRate;
+        if (todayMaxLoadRate != null
+                && (maxLoadRate == null || todayMaxLoadRate.compareTo(maxLoadRate) > 0)) {
+            maxLoadRate = todayMaxLoadRate;
+        }
+
+        // 平均负载率:加权平均(历史每天一个均值 + 今日均值)
+        BigDecimal avgLoadRate = null;
+        if (todayAvgLoadRate != null && histDays > 0) {
+            avgLoadRate = histSumAvg.add(todayAvgLoadRate)
+                    .divide(BigDecimal.valueOf(histDays + 1), 2, RoundingMode.HALF_UP);
+        } else if (todayAvgLoadRate != null) {
+            avgLoadRate = todayAvgLoadRate;
+        } else if (histDays > 0) {
+            avgLoadRate = histSumAvg.divide(BigDecimal.valueOf(histDays), 2, RoundingMode.HALF_UP);
+        }
+
+        long totalHeavy = sumHeavy + todayHeavy;
+        long totalOverload = sumOverload + todayOverload;
+        long totalEvents = totalHeavy + totalOverload;
+
+        BigDecimal lineLossKwh = histLineLossKwh;
+        if (todayLineLoss != null) {
+            lineLossKwh = lineLossKwh.add(todayLineLoss);
+        }
+
+        SeCapacityDashboardVO vo = SeCapacityDashboardVO.builder()
+                .maxLoadRate(maxLoadRate)
+                .avgLoadRate(avgLoadRate)
+                .heavyLoadEventCount((int) totalHeavy)
+                .overloadEventCount((int) totalOverload)
+                .totalEventCount((int) totalEvents)
+                .lineLossKwh(lineLossKwh)
+                .build();
+        if (totalEvents > 0) {
+            vo.setHeavyLoadRate(new BigDecimal(totalHeavy * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+            vo.setOverloadRate(new BigDecimal(totalOverload * 100)
+                    .divide(BigDecimal.valueOf(totalEvents), 2, RoundingMode.HALF_UP));
+        } else {
+            vo.setHeavyLoadRate(BigDecimal.ZERO);
+            vo.setOverloadRate(BigDecimal.ZERO);
         }
 
         return vo;
@@ -652,11 +716,15 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
                 endDate = yesterday;
             }
 
-            // 分子:中台区间 + 事件表补区间
+            // 分子:中台区间 + 事件表补区间(补差起点 = max(中台最后一天+1, 业务起点),跨月/跨年不越界)
             BigDecimal zhongtaiHours = sumZhongtaiOverLimitHours(id, type, startDate, endDate);
-            BigDecimal eventHours = endDate.isBefore(today)
+            LocalDate fillStart = endDate.plusDays(1);
+            if (fillStart.isBefore(startDate)) {
+                fillStart = startDate;
+            }
+            BigDecimal eventHours = fillStart.isBefore(today)
                     ? sumConsumerEventHours(id, type,
-                    endDate.plusDays(1).atStartOfDay(), today.plusDays(1).atStartOfDay())
+                    fillStart.atStartOfDay(), today.plusDays(1).atStartOfDay())
                     : BigDecimal.ZERO;
             overLimitHours = zhongtaiHours.add(eventHours);
 
@@ -779,4 +847,48 @@ 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;
+    }
 }

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

@@ -8,6 +8,8 @@ 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;
@@ -24,6 +26,8 @@ import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
+import com.hdkj.lt.bf.mapper.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;
@@ -56,6 +60,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final ApplicationEventPublisher eventPublisher;
     private final FeederTopoMapper feederTopoMapper;
     private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
+    private final DwdShbDsSpanSegmentBaseMapper spanSegmentMapper;
+    private final DwdShbDsCableSegmentBaseMapper cableSegmentMapper;
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
@@ -461,32 +467,41 @@ 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
-     * 段长度从 t_feeder_topo.sb_length 取(单位:米)
+     * 段长度从 dwd_shb_ds_span_segment_base(导线段)/ dwd_shb_ds_cable_segment_base(电缆段)的 length 取(单位:米)
      * SE 返回的 seoIs 为安培,先 /1000 换算成 kA
      */
     private void processSegmentsForLineLoss(JSONArray segments, LocalDateTime snapTime, Map<String, String[]> feederMap) {
-        // 1. 收集所有 psrId
-        List<String> psrIds = new ArrayList<>();
-        for (int i = 0; i < segments.size(); i++) {
-            String psrId = segments.getJSONObject(i).getString("psrId");
-            if (psrId != null) psrIds.add(psrId);
-        }
-        if (psrIds.isEmpty()) return;
-
-        // 2. 批量查 topo 表取 sbLength
-        Map<String, Double> lengthMap = new HashMap<>();
+        // 1. 按类型分组查段长度(档距段→span表,电缆段→cable表),过滤在运设备
+        Map<String, BigDecimal> lengthMap = new HashMap<>();
         try {
-            List<FeederTopo> topoList = feederTopoMapper.selectList(
-                    new QueryWrapper<FeederTopo>()
-                            .in("psr_id", psrIds)
-                            .select("psr_id", "sb_length"));
-            for (FeederTopo t : topoList) {
-                if (t.getSbLength() != null) {
-                    lengthMap.put(t.getPsrId(), t.getSbLength());
-                }
+            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()); });
+            }
+            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()); });
             }
         } catch (Exception e) {
-            log.warn("查询线路线损段长度失败", e);
+            log.warn("查询段长度失败", e);
         }
 
         // 3. 逐段计算线损
@@ -502,8 +517,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
             BigDecimal current = firstValue(seg.getJSONArray("seoIs"));
             if (current == null || current.compareTo(BigDecimal.ZERO) <= 0) continue;
 
-            Double sbLength = lengthMap.get(psrId);
-            if (sbLength == null || sbLength <= 0) continue;
+            BigDecimal sbLength = lengthMap.get(psrId);
+            if (sbLength == null || sbLength.compareTo(BigDecimal.ZERO) <= 0) continue;
 
             // 电阻率
             BigDecimal resistivity;
@@ -520,7 +535,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
             BigDecimal currentKa = current.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
 
             // 3000 × I_kA² × r × (L_m / 1000) = 3 × I_kA² × r × L_m
-            BigDecimal lengthKm = BigDecimal.valueOf(sbLength / 1000.0);
+            BigDecimal lengthKm = sbLength.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
             BigDecimal loss = BigDecimal.valueOf(3000)
                     .multiply(currentKa.pow(2))
                     .multiply(resistivity)
@@ -535,7 +550,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                     .countyId(loc.length > 1 ? loc[1] : "")
                     .subsId(loc.length > 0 ? loc[0] : "")
                     .currentKa(currentKa)
-                    .sbLengthM(BigDecimal.valueOf(sbLength))
+                    .sbLengthM(sbLength)
                     .resistivity(resistivity)
                     .lineLossKw(loss)
                     .createTime(now)
@@ -739,9 +754,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         Map<String, String> map = new HashMap<>();
         try {
             List<DwdShbDsTransformerBase> rows = transformerBaseMapper.selectList(
-                    new LambdaQueryWrapper<DwdShbDsTransformerBase>()
-                            .in(DwdShbDsTransformerBase::getPsrId, ids)
-                            .isNotNull(DwdShbDsTransformerBase::getName));
+                                new LambdaQueryWrapper<DwdShbDsTransformerBase>()
+                                        .in(DwdShbDsTransformerBase::getPsrId, ids)
+                                        .eq(DwdShbDsTransformerBase::getPsrState, "20")
+                                        .isNotNull(DwdShbDsTransformerBase::getName));
             for (DwdShbDsTransformerBase row : rows) {
                 map.put(row.getPsrId(), row.getName());
             }

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

@@ -2,19 +2,23 @@ package com.hdkj.lt.bf.service.staticgrid.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.hdkj.lt.bf.constants.OptimizationConstants;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
 import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
 import com.hdkj.lt.bf.service.SeIndicatorService;
+import com.hdkj.lt.bf.entity.dto.TodayOutageData;
 import com.hdkj.lt.bf.service.SjztPwwyyqxtPrmPdrOutageMvDsService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
+import com.hdkj.lt.utils.cache.RedisRepository;
 import com.alibaba.fastjson.JSON;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -57,6 +61,7 @@ public class StaticGridDashboardServiceImpl
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
     private final StaticProblemDetailMapper problemDetailMapper;
     private final SeIndicatorService seIndicatorService;
+    private final RedisRepository redisRepository;
 
     // ============================================================
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
@@ -117,15 +122,30 @@ public class StaticGridDashboardServiceImpl
         }
 
         YearMonth ym = YearMonth.now();
+        String cacheKey = OptimizationConstants.CACHE_KEY_GRID_DASHBOARD_PREFIX
+                + nodeType + ":" + id + ":" + ym;
+        // 整包缓存:静态月数据 + 嵌运行态,15min TTL(与断面节奏对齐)
+        Object cached = redisRepository.get(cacheKey);
+        if (cached instanceof StaticGridDashboardVO) {
+            return (StaticGridDashboardVO) cached;
+        }
+
+        StaticGridDashboardVO vo;
         if (nodeType == 2) {
-            return queryByCounty(id, ym);
+            vo = queryByCounty(id, ym);
         } else if (nodeType == 3) {
-            return queryBySubstation(id, ym);
+            vo = queryBySubstation(id, ym);
         } else if (nodeType == 4) {
-            return queryByFeeder(id, ym);
+            vo = queryByFeeder(id, ym);
+        } else {
+            log.warn("不支持的节点类型: {}", nodeType);
+            return null;
+        }
+        if (vo != null) {
+            redisRepository.setExpire(cacheKey, vo,
+                    OptimizationConstants.CACHE_TTL_GRID_DASHBOARD_MINUTES, java.util.concurrent.TimeUnit.MINUTES);
         }
-        log.warn("不支持的节点类型: {}", nodeType);
-        return null;
+        return vo;
     }
 
     // ============================================================
@@ -144,11 +164,8 @@ public class StaticGridDashboardServiceImpl
         // county 级指标
         List<StaticIndicatorMetric> countyMetrics = getMetrics(result.getId(), "county", null, null);
         Map<String, String> metricMap = metricsToMap(countyMetrics);
-        // 停电时户数
-        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
-                maintOrgId, 2,
-                ym.atDay(1).atStartOfDay(),
-                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        // 停电时户数(历史T-2 + 今日实时,分层口径)
+        BigDecimal outage = queryOutageForMonth(maintOrgId, 2, ym);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=2 区县维度)
@@ -178,10 +195,8 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
-        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
-                substationId, 3,
-                ym.atDay(1).atStartOfDay(),
-                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        // 停电时户数(历史T-2 + 今日实时,分层口径)
+        BigDecimal outage = queryOutageForMonth(substationId, 3, ym);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, substationId, null, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=3 变电站维度)
@@ -211,10 +226,8 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
-        BigDecimal outage = outageDurationService.getOutageDurationHoursHouseholds(
-                feederId, 4,
-                ym.atDay(1).atStartOfDay(),
-                ym.atEndOfMonth().atTime(23, 59, 59)).getTotalHouseholds();
+        // 停电时户数(历史T-2 + 今日实时,分层口径)
+        BigDecimal outage = queryOutageForMonth(feederId, 4, ym);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, null, feederId, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=4 馈线维度)
@@ -236,6 +249,33 @@ public class StaticGridDashboardServiceImpl
         }
     }
 
+    /**
+     * 网架停电时户数(分层口径,与运行态同源):
+     * 历史 T-2 查 [月初, 昨天] + 今日从实时停电表补。
+     * T-2 表滞后且不含当日,直接查整月会在每月 1 号/跨月边界为空。
+     */
+    private BigDecimal queryOutageForMonth(String id, int type, YearMonth ym) {
+        LocalDate today = LocalDate.now();
+        LocalDate monthStart = ym.atDay(1);
+        LocalDate histEnd = ym.atEndOfMonth().isBefore(today)
+                ? ym.atEndOfMonth() : today.minusDays(1);
+
+        BigDecimal hist = BigDecimal.ZERO;
+        if (!histEnd.isBefore(monthStart)) {
+            TodayOutageData histData = outageDurationService.getOutageDurationHoursHouseholds(
+                    id, type, monthStart.atStartOfDay(), histEnd.atTime(23, 59, 59));
+            hist = histData != null ? histData.getTotalHouseholds() : BigDecimal.ZERO;
+        }
+
+        // 今日实时(仅当查询月包含今天)
+        BigDecimal todayHc = BigDecimal.ZERO;
+        if (ym.getYear() == today.getYear() && ym.getMonthValue() == today.getMonthValue()) {
+            TodayOutageData todayData = outageDurationService.getTodayOutageData(id, type);
+            todayHc = todayData != null ? todayData.getTotalHouseholds() : BigDecimal.ZERO;
+        }
+        return hist.add(todayHc);
+    }
+
     // ============================================================
     // 聚合逻辑:feeder 级指标 → county 级格式指标 Map
     // ============================================================
@@ -543,7 +583,6 @@ public class StaticGridDashboardServiceImpl
         }
 
         List<FhzgSeCapacityDaily> list = capacityDailyMapper.selectList(wrapper);
-        if (list.isEmpty()) return;
 
         // 区域最大负载率
         BigDecimal maxLoad = list.stream()
@@ -551,7 +590,6 @@ public class StaticGridDashboardServiceImpl
                 .filter(Objects::nonNull)
                 .max(BigDecimal::compareTo)
                 .orElse(null);
-        vo.setRegionalMaxLoadRate(maxLoad);
 
         // 区域平均负载率:对每日 avg_load_rate 取均值
         double avg = list.stream()
@@ -560,9 +598,8 @@ public class StaticGridDashboardServiceImpl
                 .mapToDouble(BigDecimal::doubleValue)
                 .average()
                 .orElse(-1);
-        if (avg >= 0) {
-            vo.setRegionalAvgLoadRate(BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP));
-        }
+        BigDecimal avgLoad = avg >= 0
+                ? BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP) : null;
 
         // 线路重过载事件数(与运行态口径一致:日统计表存当日事件数,按月累加)
         long sumHeavy = list.stream()
@@ -573,6 +610,33 @@ public class StaticGridDashboardServiceImpl
                 .filter(d -> d.getOverloadCount() != null)
                 .mapToLong(FhzgSeCapacityDaily::getOverloadCount)
                 .sum();
+
+        // 今日实时补(T+1 日统计表不含今天):复用运行态 today 查询口径
+        String id = feederId != null ? feederId : (subsId != null ? subsId : countyId);
+        int type = feederId != null ? 4 : (subsId != null ? 3 : 2);
+        if (id != null) {
+            try {
+                SeCapacityDashboardVO todayVo = seIndicatorService.queryCapacity(id, type, "today");
+                if (todayVo != null) {
+                    if (todayVo.getMaxLoadRate() != null
+                            && (maxLoad == null || todayVo.getMaxLoadRate().compareTo(maxLoad) > 0)) {
+                        maxLoad = todayVo.getMaxLoadRate();
+                    }
+                    if (todayVo.getAvgLoadRate() != null) {
+                        avgLoad = todayVo.getAvgLoadRate();
+                    }
+                    sumHeavy += todayVo.getHeavyLoadEventCount() != null
+                            ? todayVo.getHeavyLoadEventCount() : 0;
+                    sumOverload += todayVo.getOverloadEventCount() != null
+                            ? todayVo.getOverloadEventCount() : 0;
+                }
+            } catch (Exception e) {
+                log.warn("网架容量今日实时补失败 id={} type={}", id, type, e);
+            }
+        }
+
+        vo.setRegionalMaxLoadRate(maxLoad);
+        vo.setRegionalAvgLoadRate(avgLoad);
         long totalEvents = sumHeavy + sumOverload;
         if (totalEvents > 0) {
             vo.setLineOverloadCount((int) totalEvents);