Browse Source

运行部分修正

lisonglin 1 week ago
parent
commit
d8fa2f0e67
19 changed files with 559 additions and 520 deletions
  1. 2 0
      api/load-transfer-bf-api/src/main/java/com/hdkj/fhzg/optimization/request/MaintOrgRequest.java
  2. 4 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/constants/OptimizationConstants.java
  3. 12 42
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/controller/optimization/IndicatorController.java
  4. 30 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsCableSegmentBase.java
  5. 30 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsSpanSegmentBase.java
  6. 5 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/DwdShbDsTransformerBase.java
  7. 0 156
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/IndicatorDashboardVO.java
  8. 4 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeCapacityDashboardVO.java
  9. 1 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeVoltageDashboardVO.java
  10. 5 4
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/SeVoltageIndexVO.java
  11. 1 1
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/entity/vo/StaticGridDashboardVO.java
  12. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsCableSegmentBaseMapper.java
  13. 9 0
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/mapper/DwdShbDsSpanSegmentBaseMapper.java
  14. 0 24
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/IndicatorDashboardService.java
  15. 0 95
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/IndicatorDashboardServiceImpl.java
  16. 242 123
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeIndicatorServiceImpl.java
  17. 111 48
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/impl/SeSnapshotServiceImpl.java
  18. 87 23
      services/load-transfer-bf/src/main/java/com/hdkj/lt/bf/service/staticgrid/impl/StaticGridDashboardServiceImpl.java
  19. 7 3
      services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/estimation/impl/StateEstimationServiceImpl.java

+ 2 - 0
api/load-transfer-bf-api/src/main/java/com/hdkj/fhzg/optimization/request/MaintOrgRequest.java

@@ -10,4 +10,6 @@ import lombok.Data;
 @Data
 @Data
 public class MaintOrgRequest {
 public class MaintOrgRequest {
     private String maintOrg;
     private String maintOrg;
+    /** 可选:指定补断面时刻(yyyy-MM-dd HH:mm:ss),传值时按该时刻拉取;不传按当前最新时刻 */
+    private String pointTime;
 }
 }

+ 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 class OptimizationConstants {
     public static final String CACHE_KEY_FEEDER_GROUP_TREE = "feeder_group_tree";
     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;
 }
 }

+ 12 - 42
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.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
 import com.hdkj.lt.bf.entity.dto.SeOverloadAlarmListReq;
 import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 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.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
 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.SeAlarmListVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 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.scheduler.SeCapacityDailyScheduler;
-import com.hdkj.lt.bf.service.IndicatorDashboardService;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.hdkj.lt.bf.service.SeAlarmService;
 import com.hdkj.lt.bf.service.SeIndicatorService;
 import com.hdkj.lt.bf.service.SeIndicatorService;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.lt.bf.service.SeSnapshotService;
@@ -38,9 +35,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
- * 运行态指标 仪表盘 Controller
+ * 运行态指标 Controller
  * <p>
  * <p>
- * POST /indicator/dashboard — 返回综合态势仪表盘数据
+ * 三个独立接口:/indicator/outage、/indicator/voltage、/indicator/capacity
  *
  *
  * @author lsl
  * @author lsl
  * @since 2026-07-21
  * @since 2026-07-21
@@ -51,29 +48,12 @@ import java.util.Map;
 @RequestMapping("/indicator")
 @RequestMapping("/indicator")
 public class IndicatorController extends BaseController {
 public class IndicatorController extends BaseController {
 
 
-    private final IndicatorDashboardService indicatorDashboardService;
     private final SeIndicatorService seIndicatorService;
     private final SeIndicatorService seIndicatorService;
     private final SeAlarmService seAlarmService;
     private final SeAlarmService seAlarmService;
     private final SiStateEstimationClient siStateEstimationClient;
     private final SiStateEstimationClient siStateEstimationClient;
     private final SeSnapshotService seSnapshotService;
     private final SeSnapshotService seSnapshotService;
     private final SeCapacityDailyScheduler seCapacityDailyScheduler;
     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));
         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)
      * 供电能力(today/month/year)
      *
      *
@@ -195,17 +159,20 @@ public class IndicatorController extends BaseController {
     /**
     /**
      * 手动触发状估断面拉取
      * 手动触发状估断面拉取
      * <p>
      * <p>
-     * 按县域调 SI queryEquipByCounty 拉取最新状估数据,聚合后送入 processSnapshot 解析。
+     * 按县域调 SI queryEquipByCounty 拉取状估数据,聚合后送入 processSnapshot 解析。
      * 传 snapTime 时启用覆盖更新:先删该断面时刻存量明细再重新写入。
      * 传 snapTime 时启用覆盖更新:先删该断面时刻存量明细再重新写入。
+     * 传 pointTime 时按指定历史时刻拉取(SI 侧优先使用该时刻),不传则拉当前最新。
      *
      *
-     * @param params {snapTime(可选, yyyy-MM-dd HH:mm:ss),传值时覆盖更新}
+     * @param params {snapTime(可选, yyyy-MM-dd HH:mm:ss),传值时覆盖更新; pointTime(可选, yyyy-MM-dd HH:mm:ss),指定拉取时刻}
      */
      */
     @PostMapping("/snapshot/manual")
     @PostMapping("/snapshot/manual")
     public ApiResponse<String> manualSnapshot(@RequestBody Map<String, String> params) {
     public ApiResponse<String> manualSnapshot(@RequestBody Map<String, String> params) {
         String snapTimeStr = params.get("snapTime");
         String snapTimeStr = params.get("snapTime");
-        boolean overwrite = StringUtils.isNotBlank(snapTimeStr);
+        String pointTime = params.get("pointTime");
+        // 传 snapTime 或 pointTime 均走覆盖更新:手动补断面就是要重写该时刻存量
+        boolean overwrite = StringUtils.isNotBlank(snapTimeStr) || StringUtils.isNotBlank(pointTime);
 
 
-        log.info("手动状估拉取开始 overwrite={}", overwrite);
+        log.info("手动状估拉取开始 overwrite={}, pointTime={}", overwrite, pointTime);
         int successCount = 0;
         int successCount = 0;
         List<String> errors = new ArrayList<>();
         List<String> errors = new ArrayList<>();
 
 
@@ -215,6 +182,9 @@ public class IndicatorController extends BaseController {
             try {
             try {
                 MaintOrgRequest request = new MaintOrgRequest();
                 MaintOrgRequest request = new MaintOrgRequest();
                 request.setMaintOrg(maintOrg);
                 request.setMaintOrg(maintOrg);
+                if (StringUtils.isNotBlank(pointTime)) {
+                    request.setPointTime(pointTime);
+                }
 
 
                 ApiResponse<List<StateEstimation>> resp = siStateEstimationClient.queryEquipByCounty(request);
                 ApiResponse<List<StateEstimation>> resp = siStateEstimationClient.queryEquipByCounty(request);
                 if (resp == null || !resp.isSuccess() || resp.getData() == null || resp.getData().isEmpty()) {
                 if (resp == null || !resp.isSuccess() || resp.getData() == null || resp.getData().isEmpty()) {

+ 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;
     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 = "总事件次数")
     @ApiModelProperty(value = "总事件次数")
     private Integer totalEventCount;
     private Integer totalEventCount;
 
 
+    @ApiModelProperty(value = "线路损耗电量(kWh)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal lineLossKwh;
+
     // ============================================================
     // ============================================================
     // 今日馈线96点时序(仅馈线type=4+today时返回)
     // 今日馈线96点时序(仅馈线type=4+today时返回)
     // ============================================================
     // ============================================================

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

@@ -72,7 +72,7 @@ public class SeVoltageDashboardVO implements Serializable {
     @ApiModelProperty(value = "越下限用户事件次数")
     @ApiModelProperty(value = "越下限用户事件次数")
     private Integer overLowerConsumerTimes;
     private Integer overLowerConsumerTimes;
 
 
-    @ApiModelProperty(value = "IEC 用户电压指数(待实现)")
+    @ApiModelProperty(value = "IEC 用户电压合格率(%),如 98.93 表示合格率 98.93%")
     private BigDecimal iecIndex;
     private BigDecimal iecIndex;
 
 
     // ============================================================
     // ============================================================

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

@@ -13,9 +13,10 @@ import java.math.BigDecimal;
 /**
 /**
  * IEC 用户电压指数 VO
  * IEC 用户电压指数 VO
  * <p>
  * <p>
- * 指数 = 用户越限总时长(上+下) / (总用户数 × 周期天数 × 24h) × 100%
+ * 指数 = 用户电压合格率(%),即 100 − 越限率;
+ * 越限率 = 用户越限总时长(上+下) / (总用户数 × 周期天数 × 24h) × 100
  * 分子:中台 ads_mrt_voltage_over_limit_daily_result(历史)+ 事件表 fhzg_se_voltage_event(昨日/今日)组合
  * 分子:中台 ads_mrt_voltage_over_limit_daily_result(历史)+ 事件表 fhzg_se_voltage_event(昨日/今日)组合
- * 分母:fhzg_se_consumer_count_daily 按日累加的用户数 × 24(时户数)
+ * 分母:fhzg_se_consumer_count_daily 最近一天用户数 × 实际天数 × 24(时户数)
  *
  *
  * @author lsl
  * @author lsl
  * @since 2026-07-30
  * @since 2026-07-30
@@ -29,13 +30,13 @@ public class SeVoltageIndexVO implements Serializable {
 
 
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
-    @ApiModelProperty(value = "IEC用户电压指数(%)")
+    @ApiModelProperty(value = "IEC用户电压合格率(%),如 98.93 表示合格率 98.93%")
     private BigDecimal voltageIndex;
     private BigDecimal voltageIndex;
 
 
     @ApiModelProperty(value = "用户越限总时长(小时,分子)")
     @ApiModelProperty(value = "用户越限总时长(小时,分子)")
     private BigDecimal overLimitHours;
     private BigDecimal overLimitHours;
 
 
-    @ApiModelProperty(value = "用户总时户数(分母,用户数×24h)")
+    @ApiModelProperty(value = "用户总时户数(分母,用户数×天数×24h)")
     private BigDecimal consumerHours;
     private BigDecimal consumerHours;
 
 
     @ApiModelProperty(value = "平均用户数(调试用)")
     @ApiModelProperty(value = "平均用户数(调试用)")

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

@@ -108,7 +108,7 @@ public class StaticGridDashboardVO implements Serializable {
     // 三、电压质量
     // 三、电压质量
     // ============================================================
     // ============================================================
 
 
-    @ApiModelProperty(value = "IEC用户电压指数(预留,来源待定)")
+    @ApiModelProperty(value = "IEC用户电压合格率(%),与运行态同源,如 98.93 表示合格率 98.93%")
     private BigDecimal iecUserVoltageIndex;
     private BigDecimal iecUserVoltageIndex;
 
 
     @ApiModelProperty(value = "供电半径过长线路数")
     @ApiModelProperty(value = "供电半径过长线路数")

+ 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();
-    }
-}

+ 242 - 123
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.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 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.FhzgDwmVoltageOverLimitDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
 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.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
 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.FhzgDwmVoltageOverLimitDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
@@ -58,6 +62,8 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeCurrentEventMapper currentEventMapper;
     private final FhzgSeConsumerCountDailyMapper consumerCountDailyMapper;
     private final FhzgSeConsumerCountDailyMapper consumerCountDailyMapper;
     private final FhzgSeConsumerRelationMapper consumerRelationMapper;
     private final FhzgSeConsumerRelationMapper consumerRelationMapper;
+    private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
+    private final FhzgSeLineLossDailyMapper lineLossDailyMapper;
     private final FeederGroupApplication feederGroupApplication;
     private final FeederGroupApplication feederGroupApplication;
 
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
@@ -103,11 +109,21 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             return null;
             return null;
         }
         }
 
 
+        SeVoltageDashboardVO vo;
         if ("today".equalsIgnoreCase(timeScope)) {
         if ("today".equalsIgnoreCase(timeScope)) {
-            return queryVoltageToday(id, type);
+            vo = queryVoltageToday(id, type);
         } else {
         } 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) {
     private SeVoltageDashboardVO queryVoltageHistory(String id, Integer type, String timeScope) {
         LocalDate today = LocalDate.now();
         LocalDate today = LocalDate.now();
-        LocalDate endDate;
         LocalDate startDate;
         LocalDate startDate;
 
 
         if ("month".equalsIgnoreCase(timeScope)) {
         if ("month".equalsIgnoreCase(timeScope)) {
@@ -217,86 +232,95 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
 
 
         String dimLevel = resolveDimLevel(type);
         String dimLevel = resolveDimLevel(type);
 
 
-        // 查中台日汇总表最新日期,判断是否需要补昨日
+        // 中台覆盖到哪天(T-2,动态自适应);无数据时 midEnd < startDate → 中台贡献为 0
         String maxOverDate = queryDailyMaxOverDate(dimLevel, id, type);
         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<>();
             QueryWrapper<FhzgSeVoltageEvent> agg = new QueryWrapper<>();
             agg.select("alarm_level", "alarm_type", "COUNT(*) AS cnt")
             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)
                     .eq("status", 2)
                     .groupBy("alarm_level", "alarm_type");
                     .groupBy("alarm_level", "alarm_type");
             if (type == 2) agg.eq("county_id", id);
             if (type == 2) agg.eq("county_id", id);
@@ -416,73 +440,113 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
             vo.setHeavyLoadRate(BigDecimal.ZERO);
             vo.setHeavyLoadRate(BigDecimal.ZERO);
             vo.setOverloadRate(BigDecimal.ZERO);
             vo.setOverloadRate(BigDecimal.ZERO);
         }
         }
+
+        // 3. 线损电量:从明细表聚合(今日未刷日汇总,用明细实时算)
+        vo.setLineLossKwh(queryLineLossKwhFromDetail(id, type, startTime, endTime));
+
         return vo;
         return vo;
     }
     }
 
 
     /**
     /**
-     * 历史供电能力:负载率从日汇总表查,重过载次数从事件表统计
-     * month = 月初 ~ 昨天;year = 1.1 ~ 昨天
+     * 历史供电能力(分层口径):
+     * 历史部分(T+1)查日汇总表 [startDate, yesterday] —— 今天尚未入表;
+     * 今日部分实时补:负载率从断面明细聚合、重过载次数从事件表统计、线损从明细 SUM。
+     * month = 月初 ~ 今天;year = 1.1 ~ 今天
      */
      */
     private SeCapacityDashboardVO queryCapacityHistory(String id, Integer type, String timeScope) {
     private SeCapacityDashboardVO queryCapacityHistory(String id, Integer type, String timeScope) {
         LocalDate today = LocalDate.now();
         LocalDate today = LocalDate.now();
         LocalDate startDate;
         LocalDate startDate;
-        LocalDate endDate;
+        LocalDate endDate = today.minusDays(1);
 
 
         if ("month".equalsIgnoreCase(timeScope)) {
         if ("month".equalsIgnoreCase(timeScope)) {
             startDate = today.withDayOfMonth(1);
             startDate = today.withDayOfMonth(1);
         } else {
         } else {
             startDate = LocalDate.of(today.getYear(), 1, 1);
             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) {
             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) {
                 if (d.getAvgLoadRate() != null) {
-                    sumAvg = sumAvg.add(d.getAvgLoadRate());
+                    histSumAvg = histSumAvg.add(d.getAvgLoadRate());
+                    histDays++;
                 }
                 }
                 // 重过载事件数:日统计表存的就是当日事件表事件数,直接累加
                 // 重过载事件数:日统计表存的就是当日事件表事件数,直接累加
                 if (d.getHeavyCount() != null) sumHeavy += d.getHeavyCount();
                 if (d.getHeavyCount() != null) sumHeavy += d.getHeavyCount();
                 if (d.getOverloadCount() != null) sumOverload += d.getOverloadCount();
                 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;
         return vo;
@@ -597,7 +661,9 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     // ============================================================
     // ============================================================
 
 
     /**
     /**
-     * IEC 用户电压指数 = 用户越限总时长(上+下) / (总用户数 × 周期天数 × 24h) × 100%
+     * IEC 用户电压指数 = 用户电压合格率(%)
+     * <p>
+     * 计算:合格率 = 100 − 越限率;越限率 = 用户越限总时长(上+下) / (总用户数 × 周期天数 × 24h) × 100
      * <p>
      * <p>
      * 分子(组合):
      * 分子(组合):
      * 历史 = 中台 ads_mrt_voltage_over_limit_daily_result.total_over_limit_hours(SQL SUM);
      * 历史 = 中台 ads_mrt_voltage_over_limit_daily_result.total_over_limit_hours(SQL SUM);
@@ -606,6 +672,8 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
      * <p>
      * <p>
      * 分母 = 分层分级用户数 × 周期天数 × 24(时户数);
      * 分母 = 分层分级用户数 × 周期天数 × 24(时户数);
      * 用户数取最近一天日表值(用户数日变化可忽略,无需逐日累加/历史回填)。
      * 用户数取最近一天日表值(用户数日变化可忽略,无需逐日累加/历史回填)。
+     * <p>
+     * 返回值 voltageIndex 为合格率(0~100),如 98.93 表示电压合格率 98.93%。
      */
      */
     @Override
     @Override
     public SeVoltageIndexVO queryVoltageIndex(String id, Integer type, String timeScope) {
     public SeVoltageIndexVO queryVoltageIndex(String id, Integer type, String timeScope) {
@@ -652,11 +720,15 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
                 endDate = yesterday;
                 endDate = yesterday;
             }
             }
 
 
-            // 分子:中台区间 + 事件表补区间
+            // 分子:中台区间 + 事件表补区间(补差起点 = max(中台最后一天+1, 业务起点),跨月/跨年不越界)
             BigDecimal zhongtaiHours = sumZhongtaiOverLimitHours(id, type, startDate, endDate);
             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,
                     ? sumConsumerEventHours(id, type,
-                    endDate.plusDays(1).atStartOfDay(), today.plusDays(1).atStartOfDay())
+                    fillStart.atStartOfDay(), today.plusDays(1).atStartOfDay())
                     : BigDecimal.ZERO;
                     : BigDecimal.ZERO;
             overLimitHours = zhongtaiHours.add(eventHours);
             overLimitHours = zhongtaiHours.add(eventHours);
 
 
@@ -671,10 +743,13 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
 
 
         BigDecimal voltageIndex = null;
         BigDecimal voltageIndex = null;
         if (consumerHours.compareTo(BigDecimal.ZERO) > 0) {
         if (consumerHours.compareTo(BigDecimal.ZERO) > 0) {
-            voltageIndex = overLimitHours
+            // 越限率 = 用户越限总时长 / 总时户数 × 100
+            BigDecimal overRate = overLimitHours
                     .divide(consumerHours, 6, RoundingMode.HALF_UP)
                     .divide(consumerHours, 6, RoundingMode.HALF_UP)
                     .multiply(new BigDecimal("100"))
                     .multiply(new BigDecimal("100"))
                     .setScale(2, RoundingMode.HALF_UP);
                     .setScale(2, RoundingMode.HALF_UP);
+            // 合格率 = 100 − 越限率(展示口径与国网"居民客户端电压合格率"一致)
+            voltageIndex = new BigDecimal("100").subtract(overRate);
         }
         }
 
 
         return SeVoltageIndexVO.builder()
         return SeVoltageIndexVO.builder()
@@ -779,4 +854,48 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
         }
         }
         return 0;
         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;
+    }
 }
 }

+ 111 - 48
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.application.FeederGroupApplication;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
 import com.hdkj.lt.bf.common.SeMonitorThreshold;
 import com.hdkj.lt.bf.common.SeSnapshotConstants;
 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.DwdShbDsTransformerBase;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
 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.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
 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.mapper.DwdShbDsTransformerBaseMapper;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.lt.bf.service.SeSnapshotService;
 import com.hdkj.lt.core.bizms.modle.po.FeederTopo;
 import com.hdkj.lt.core.bizms.modle.po.FeederTopo;
@@ -56,6 +60,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final ApplicationEventPublisher eventPublisher;
     private final ApplicationEventPublisher eventPublisher;
     private final FeederTopoMapper feederTopoMapper;
     private final FeederTopoMapper feederTopoMapper;
     private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
     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");
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
 
@@ -69,7 +75,6 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional(rollbackFor = Exception.class)
     public void processSnapshot(String snapshotJson) {
     public void processSnapshot(String snapshotJson) {
         JSONObject root = JSON.parseObject(snapshotJson);
         JSONObject root = JSON.parseObject(snapshotJson);
         if (root == null) return;
         if (root == null) return;
@@ -81,23 +86,40 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         Map<String, String[]> feederMap = buildFeederMapping();
         Map<String, String[]> feederMap = buildFeederMapping();
         List<FhzgSeSnapshotDetail> batch = new ArrayList<>();
         List<FhzgSeSnapshotDetail> batch = new ArrayList<>();
 
 
-        // === 1. 馈线 ===
+        // === 0. 批量预加载状态机(避免逐设备 selectOne 循环导致连接持有过久 / abandon) ===
         JSONArray feeders = root.getJSONArray("periodFeederSeResult");
         JSONArray feeders = root.getJSONArray("periodFeederSeResult");
+        JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
+        Set<String> feederIds = new HashSet<>();
+        Set<String> allDeviceIds = new HashSet<>();
+        if (feeders != null) {
+            for (int i = 0; i < feeders.size(); i++) {
+                String id = feeders.getJSONObject(i).getString("psrId");
+                if (id != null) { feederIds.add(id); allDeviceIds.add(id); }
+            }
+        }
+        if (mvtrans != null) {
+            for (int i = 0; i < mvtrans.size(); i++) {
+                String id = mvtrans.getJSONObject(i).getString("psrId");
+                if (id != null) allDeviceIds.add(id);
+            }
+        }
+        Map<String, FhzgSeMonitorCurrent> currentStateMap = loadCurrentStateMap(feederIds);
+        Map<String, FhzgSeMonitorVoltage> voltageStateMap = loadVoltageStateMap(allDeviceIds);
+
+        // === 1. 馈线 ===
         if (feeders != null) {
         if (feeders != null) {
             for (int i = 0; i < feeders.size(); i++) {
             for (int i = 0; i < feeders.size(); i++) {
                 JSONObject f = feeders.getJSONObject(i);
                 JSONObject f = feeders.getJSONObject(i);
                 FhzgSeSnapshotDetail d = parseFeeder(f, snapTime, feederMap);
                 FhzgSeSnapshotDetail d = parseFeeder(f, snapTime, feederMap);
                 if (d != null) {
                 if (d != null) {
                     batch.add(d);
                     batch.add(d);
-                    processFeederCurrent(f, d, snapTime, feederMap);
-                    processFeederVoltage(f, d, snapTime, feederMap);
+                    processFeederCurrent(f, d, snapTime, feederMap, currentStateMap);
+                    processFeederVoltage(f, d, snapTime, feederMap, voltageStateMap);
                 }
                 }
             }
             }
         }
         }
 
 
         // === 2. 配变 ===
         // === 2. 配变 ===
-        JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
-        Map<String, String> transformerNameMap = new HashMap<>();
         if (mvtrans != null && !mvtrans.isEmpty()) {
         if (mvtrans != null && !mvtrans.isEmpty()) {
             // 先收集当前断面所有配变 ID,再一次性查名称(避免全表加载)
             // 先收集当前断面所有配变 ID,再一次性查名称(避免全表加载)
             Set<String> mvtransIds = new HashSet<>();
             Set<String> mvtransIds = new HashSet<>();
@@ -105,13 +127,13 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                 String id = mvtrans.getJSONObject(i).getString("psrId");
                 String id = mvtrans.getJSONObject(i).getString("psrId");
                 if (id != null) mvtransIds.add(id);
                 if (id != null) mvtransIds.add(id);
             }
             }
-            transformerNameMap = buildTransformerMapping(mvtransIds);
+            Map<String, String> transformerNameMap = buildTransformerMapping(mvtransIds);
             for (int i = 0; i < mvtrans.size(); i++) {
             for (int i = 0; i < mvtrans.size(); i++) {
                 JSONObject m = mvtrans.getJSONObject(i);
                 JSONObject m = mvtrans.getJSONObject(i);
                 FhzgSeSnapshotDetail d = parseMvtrans(m, snapTime, feederMap);
                 FhzgSeSnapshotDetail d = parseMvtrans(m, snapTime, feederMap);
                 if (d != null) {
                 if (d != null) {
                     batch.add(d);
                     batch.add(d);
-                    processMvtransMonitor(m, d, snapTime, feederMap, transformerNameMap);
+                    processMvtransMonitor(m, d, snapTime, feederMap, transformerNameMap, voltageStateMap);
                 }
                 }
             }
             }
         }
         }
@@ -171,7 +193,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
     private void processFeederCurrent(JSONObject f, FhzgSeSnapshotDetail detail,
-                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap,
+                                       Map<String, FhzgSeMonitorCurrent> currentStateMap) {
         if (detail.getCurrentStatus() == null) return;
         if (detail.getCurrentStatus() == null) return;
         String feederId = detail.getFeederId();
         String feederId = detail.getFeederId();
         String[] loc = feederMap.get(feederId);
         String[] loc = feederMap.get(feederId);
@@ -182,9 +205,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         int status = detail.getCurrentStatus();
         int status = detail.getCurrentStatus();
         boolean isOverLimit = status != 0;
         boolean isOverLimit = status != 0;
 
 
-        FhzgSeMonitorCurrent state = monitorCurrentMapper.selectOne(
-                new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
-                        .eq(FhzgSeMonitorCurrent::getFeederId, feederId).last("LIMIT 1"));
+        FhzgSeMonitorCurrent state = currentStateMap.get(feederId);
 
 
         if (state == null) {
         if (state == null) {
             state = FhzgSeMonitorCurrent.builder()
             state = FhzgSeMonitorCurrent.builder()
@@ -273,13 +294,14 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     }
     }
 
 
     private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
     private void processFeederVoltage(JSONObject f, FhzgSeSnapshotDetail detail,
-                                       LocalDateTime snapTime, Map<String, String[]> feederMap) {
+                                       LocalDateTime snapTime, Map<String, String[]> feederMap,
+                                       Map<String, FhzgSeMonitorVoltage> voltageStateMap) {
         if (detail.getVoltageStatus() == null) return;
         if (detail.getVoltageStatus() == null) return;
         String[] loc = feederMap.get(detail.getFeederId());
         String[] loc = feederMap.get(detail.getFeederId());
         String feederName = loc != null && loc.length > 2 ? loc[2] : null;
         String feederName = loc != null && loc.length > 2 ? loc[2] : null;
         checkVoltageMonitor(detail.getFeederId(), "feeder", feederName, detail.getVoltageRatio(), detail.getVoltageStatus(),
         checkVoltageMonitor(detail.getFeederId(), "feeder", feederName, detail.getVoltageRatio(), detail.getVoltageStatus(),
                 snapTime, detail.getFeederId(), feederName,
                 snapTime, detail.getFeederId(), feederName,
-                loc != null ? loc[0] : "", loc != null ? loc[1] : "", null, null);
+                loc != null ? loc[0] : "", loc != null ? loc[1] : "", null, null, voltageStateMap);
     }
     }
 
 
     // ============================================================
     // ============================================================
@@ -306,7 +328,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
 
 
     private void processMvtransMonitor(JSONObject m, FhzgSeSnapshotDetail detail,
     private void processMvtransMonitor(JSONObject m, FhzgSeSnapshotDetail detail,
                                         LocalDateTime snapTime, Map<String, String[]> feederMap,
                                         LocalDateTime snapTime, Map<String, String[]> feederMap,
-                                        Map<String, String> transformerNameMap) {
+                                        Map<String, String> transformerNameMap,
+                                        Map<String, FhzgSeMonitorVoltage> voltageStateMap) {
         if (detail.getVoltageStatus() == null) return;
         if (detail.getVoltageStatus() == null) return;
         String mvtransId = detail.getDeviceId();
         String mvtransId = detail.getDeviceId();
         String[] loc = feederMap.get(detail.getFeederId());
         String[] loc = feederMap.get(detail.getFeederId());
@@ -317,7 +340,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                 ? transformerNameMap.getOrDefault(mvtransId, mvtransId) : mvtransId;
                 ? transformerNameMap.getOrDefault(mvtransId, mvtransId) : mvtransId;
         checkVoltageMonitor(mvtransId, "mvtrans", transformerName, detail.getVoltageRatio(), status,
         checkVoltageMonitor(mvtransId, "mvtrans", transformerName, detail.getVoltageRatio(), status,
                 snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
                 snapTime, detail.getFeederId(), loc != null && loc.length > 2 ? loc[2] : null,
-                loc != null ? loc[0] : "", loc != null ? loc[1] : "", mvtransId, null);
+                loc != null ? loc[0] : "", loc != null ? loc[1] : "", mvtransId, null, voltageStateMap);
 
 
         if (status != 0) {
         if (status != 0) {
             eventPublisher.publishEvent(new MvtransOverVoltageEvent(
             eventPublisher.publishEvent(new MvtransOverVoltageEvent(
@@ -334,11 +357,9 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private void checkVoltageMonitor(String deviceId, String deviceType, String deviceName,
     private void checkVoltageMonitor(String deviceId, String deviceType, String deviceName,
                                       BigDecimal voltageRatio, int voltageStatus,
                                       BigDecimal voltageRatio, int voltageStatus,
                                       LocalDateTime snapTime, String feederId, String feederName,
                                       LocalDateTime snapTime, String feederId, String feederName,
-                                      String subsId, String countyId, String mvtransId, String consumerId) {
-        FhzgSeMonitorVoltage state = monitorVoltageMapper.selectOne(
-                new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
-                        .eq(FhzgSeMonitorVoltage::getDeviceId, deviceId)
-                        .eq(FhzgSeMonitorVoltage::getDeviceType, deviceType).last("LIMIT 1"));
+                                      String subsId, String countyId, String mvtransId, String consumerId,
+                                      Map<String, FhzgSeMonitorVoltage> voltageStateMap) {
+        FhzgSeMonitorVoltage state = voltageStateMap.get(deviceId + ":" + deviceType);
         boolean isOverLimit = voltageStatus != 0;
         boolean isOverLimit = voltageStatus != 0;
 
 
         if (state == null) {
         if (state == null) {
@@ -461,32 +482,41 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
      * <p>
      * <p>
      * line_loss_kW = 3000 × I_kA² × r_Ω_per_km × (L_m / 1000)
      * line_loss_kW = 3000 × I_kA² × r_Ω_per_km × (L_m / 1000)
      * 电阻率: dxd(导线段)=0.4132Ω/km, 0201(电缆段)=0.313Ω/km
      * 电阻率: 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
      * SE 返回的 seoIs 为安培,先 /1000 换算成 kA
      */
      */
     private void processSegmentsForLineLoss(JSONArray segments, LocalDateTime snapTime, Map<String, String[]> feederMap) {
     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 {
         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) {
         } catch (Exception e) {
-            log.warn("查询线路线损段长度失败", e);
+            log.warn("查询段长度失败", e);
         }
         }
 
 
         // 3. 逐段计算线损
         // 3. 逐段计算线损
@@ -502,8 +532,8 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
             BigDecimal current = firstValue(seg.getJSONArray("seoIs"));
             BigDecimal current = firstValue(seg.getJSONArray("seoIs"));
             if (current == null || current.compareTo(BigDecimal.ZERO) <= 0) continue;
             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;
             BigDecimal resistivity;
@@ -520,7 +550,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
             BigDecimal currentKa = current.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
             BigDecimal currentKa = current.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
 
 
             // 3000 × I_kA² × r × (L_m / 1000) = 3 × I_kA² × r × L_m
             // 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)
             BigDecimal loss = BigDecimal.valueOf(3000)
                     .multiply(currentKa.pow(2))
                     .multiply(currentKa.pow(2))
                     .multiply(resistivity)
                     .multiply(resistivity)
@@ -535,7 +565,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
                     .countyId(loc.length > 1 ? loc[1] : "")
                     .countyId(loc.length > 1 ? loc[1] : "")
                     .subsId(loc.length > 0 ? loc[0] : "")
                     .subsId(loc.length > 0 ? loc[0] : "")
                     .currentKa(currentKa)
                     .currentKa(currentKa)
-                    .sbLengthM(BigDecimal.valueOf(sbLength))
+                    .sbLengthM(sbLength)
                     .resistivity(resistivity)
                     .resistivity(resistivity)
                     .lineLossKw(loss)
                     .lineLossKw(loss)
                     .createTime(now)
                     .createTime(now)
@@ -573,6 +603,38 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     // 查询辅助
     // 查询辅助
     // ============================================================
     // ============================================================
 
 
+    /**
+     * 批量加载电流状态机(按馈线 ID 集合一次性查询,key=feederId)
+     * 避免 processFeederCurrent 逐设备 selectOne 造成海量 SQL 往返
+     */
+    private Map<String, FhzgSeMonitorCurrent> loadCurrentStateMap(Collection<String> feederIds) {
+        if (feederIds == null || feederIds.isEmpty()) return Collections.emptyMap();
+        List<FhzgSeMonitorCurrent> rows = monitorCurrentMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeMonitorCurrent>()
+                        .in(FhzgSeMonitorCurrent::getFeederId, feederIds));
+        Map<String, FhzgSeMonitorCurrent> map = new HashMap<>();
+        for (FhzgSeMonitorCurrent s : rows) {
+            map.put(s.getFeederId(), s);
+        }
+        return map;
+    }
+
+    /**
+     * 批量加载电压状态机(按设备 ID 集合一次性查询,key=deviceId:deviceType)
+     * 避免 checkVoltageMonitor 逐设备 selectOne 造成海量 SQL 往返
+     */
+    private Map<String, FhzgSeMonitorVoltage> loadVoltageStateMap(Collection<String> deviceIds) {
+        if (deviceIds == null || deviceIds.isEmpty()) return Collections.emptyMap();
+        List<FhzgSeMonitorVoltage> rows = monitorVoltageMapper.selectList(
+                new LambdaQueryWrapper<FhzgSeMonitorVoltage>()
+                        .in(FhzgSeMonitorVoltage::getDeviceId, deviceIds));
+        Map<String, FhzgSeMonitorVoltage> map = new HashMap<>();
+        for (FhzgSeMonitorVoltage s : rows) {
+            map.put(s.getDeviceId() + ":" + s.getDeviceType(), s);
+        }
+        return map;
+    }
+
     private BigDecimal[] queryVoltageRangeFromSnap(String feederId, LocalDateTime start, LocalDateTime end) {
     private BigDecimal[] queryVoltageRangeFromSnap(String feederId, LocalDateTime start, LocalDateTime end) {
         List<FhzgSeSnapshotDetail> list = snapshotDetailMapper.selectList(
         List<FhzgSeSnapshotDetail> list = snapshotDetailMapper.selectList(
                 new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
                 new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
@@ -739,9 +801,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         Map<String, String> map = new HashMap<>();
         Map<String, String> map = new HashMap<>();
         try {
         try {
             List<DwdShbDsTransformerBase> rows = transformerBaseMapper.selectList(
             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) {
             for (DwdShbDsTransformerBase row : rows) {
                 map.put(row.getPsrId(), row.getName());
                 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.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 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.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorMetric;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
 import com.hdkj.lt.bf.entity.staticgrid.StaticIndicatorResult;
 import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
 import com.hdkj.lt.bf.entity.staticgrid.StaticProblemDetail;
 import com.hdkj.lt.bf.entity.vo.StaticGridDashboardVO;
 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.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorMetricMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticIndicatorResultMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
 import com.hdkj.lt.bf.mapper.staticgrid.StaticProblemDetailMapper;
 import com.hdkj.lt.bf.service.SeIndicatorService;
 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.SjztPwwyyqxtPrmPdrOutageMvDsService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
 import com.hdkj.lt.bf.service.staticgrid.StaticGridDashboardService;
+import com.hdkj.lt.utils.cache.RedisRepository;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -57,6 +61,7 @@ public class StaticGridDashboardServiceImpl
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
     private final FhzgSeCapacityDailyMapper capacityDailyMapper;
     private final StaticProblemDetailMapper problemDetailMapper;
     private final StaticProblemDetailMapper problemDetailMapper;
     private final SeIndicatorService seIndicatorService;
     private final SeIndicatorService seIndicatorService;
+    private final RedisRepository redisRepository;
 
 
     // ============================================================
     // ============================================================
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
     // 指标聚合映射表:county 级 metric_code → feeder 级 metric_code
@@ -117,15 +122,30 @@ public class StaticGridDashboardServiceImpl
         }
         }
 
 
         YearMonth ym = YearMonth.now();
         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) {
         if (nodeType == 2) {
-            return queryByCounty(id, ym);
+            vo = queryByCounty(id, ym);
         } else if (nodeType == 3) {
         } else if (nodeType == 3) {
-            return queryBySubstation(id, ym);
+            vo = queryBySubstation(id, ym);
         } else if (nodeType == 4) {
         } 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 级指标
         // county 级指标
         List<StaticIndicatorMetric> countyMetrics = getMetrics(result.getId(), "county", null, null);
         List<StaticIndicatorMetric> countyMetrics = getMetrics(result.getId(), "county", null, null);
         Map<String, String> metricMap = metricsToMap(countyMetrics);
         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);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
         fillCapacityMetrics(vo, result.getCountyId(), null, null, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=2 区县维度)
         // IEC用户电压指数:与运行态本月同源同算法(type=2 区县维度)
@@ -178,10 +195,8 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
         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);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, substationId, null, ym);
         fillCapacityMetrics(vo, null, substationId, null, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=3 变电站维度)
         // IEC用户电压指数:与运行态本月同源同算法(type=3 变电站维度)
@@ -211,10 +226,8 @@ public class StaticGridDashboardServiceImpl
         Long resultId = feederMetrics.get(0).getResultId();
         Long resultId = feederMetrics.get(0).getResultId();
         StaticIndicatorResult result = getById(resultId);
         StaticIndicatorResult result = getById(resultId);
         Map<String, String> metricMap = aggregateFeederMetrics(feederMetrics);
         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);
         StaticGridDashboardVO vo = buildDashboardVO(result, metricMap, outage);
         fillCapacityMetrics(vo, null, null, feederId, ym);
         fillCapacityMetrics(vo, null, null, feederId, ym);
         // IEC用户电压指数:与运行态本月同源同算法(type=4 馈线维度)
         // 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
     // 聚合逻辑:feeder 级指标 → county 级格式指标 Map
     // ============================================================
     // ============================================================
@@ -543,7 +583,6 @@ public class StaticGridDashboardServiceImpl
         }
         }
 
 
         List<FhzgSeCapacityDaily> list = capacityDailyMapper.selectList(wrapper);
         List<FhzgSeCapacityDaily> list = capacityDailyMapper.selectList(wrapper);
-        if (list.isEmpty()) return;
 
 
         // 区域最大负载率
         // 区域最大负载率
         BigDecimal maxLoad = list.stream()
         BigDecimal maxLoad = list.stream()
@@ -551,7 +590,6 @@ public class StaticGridDashboardServiceImpl
                 .filter(Objects::nonNull)
                 .filter(Objects::nonNull)
                 .max(BigDecimal::compareTo)
                 .max(BigDecimal::compareTo)
                 .orElse(null);
                 .orElse(null);
-        vo.setRegionalMaxLoadRate(maxLoad);
 
 
         // 区域平均负载率:对每日 avg_load_rate 取均值
         // 区域平均负载率:对每日 avg_load_rate 取均值
         double avg = list.stream()
         double avg = list.stream()
@@ -560,9 +598,8 @@ public class StaticGridDashboardServiceImpl
                 .mapToDouble(BigDecimal::doubleValue)
                 .mapToDouble(BigDecimal::doubleValue)
                 .average()
                 .average()
                 .orElse(-1);
                 .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()
         long sumHeavy = list.stream()
@@ -573,6 +610,33 @@ public class StaticGridDashboardServiceImpl
                 .filter(d -> d.getOverloadCount() != null)
                 .filter(d -> d.getOverloadCount() != null)
                 .mapToLong(FhzgSeCapacityDaily::getOverloadCount)
                 .mapToLong(FhzgSeCapacityDaily::getOverloadCount)
                 .sum();
                 .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;
         long totalEvents = sumHeavy + sumOverload;
         if (totalEvents > 0) {
         if (totalEvents > 0) {
             vo.setLineOverloadCount((int) totalEvents);
             vo.setLineOverloadCount((int) totalEvents);

+ 7 - 3
services/load-transfer-si/src/main/java/com/hdkj/lt/si/service/estimation/impl/StateEstimationServiceImpl.java

@@ -180,7 +180,7 @@ public class StateEstimationServiceImpl implements StateEstimationService {
         //存放失败的线路id
         //存放失败的线路id
         List<String> failFeederIds = Lists.newArrayList();
         List<String> failFeederIds = Lists.newArrayList();
         // 并行调用 (有界线程池), 每批带重试, 任一批最终失败则整体失败
         // 并行调用 (有界线程池), 每批带重试, 任一批最终失败则整体失败
-        StateEstimationRequest req = buildStateEstimationReq();
+        StateEstimationRequest req = buildStateEstimationReq(request.getPointTime());
         for (List<String> batchFeederIds : batches) {
         for (List<String> batchFeederIds : batches) {
             List<CompletableFuture<StateEstimation>> futures = batchFeederIds.stream()
             List<CompletableFuture<StateEstimation>> futures = batchFeederIds.stream()
                     .map(id -> CompletableFuture.supplyAsync(() -> callWithRetry(id, req), executor))
                     .map(id -> CompletableFuture.supplyAsync(() -> callWithRetry(id, req), executor))
@@ -225,14 +225,18 @@ public class StateEstimationServiceImpl implements StateEstimationService {
         return mergedList;
         return mergedList;
     }
     }
 
 
-    private StateEstimationRequest buildStateEstimationReq(){
-        //当前时间前一个小时
+    private StateEstimationRequest buildStateEstimationReq(String pointTime){
+        // 当前时间前一个小时
         ZoneId zone = ZoneId.of(ZoneId.systemDefault().getId());
         ZoneId zone = ZoneId.of(ZoneId.systemDefault().getId());
         LocalDateTime now = LocalDateTime.now(zone);
         LocalDateTime now = LocalDateTime.now(zone);
         int slot = (now.getMinute() / 15) * 15;
         int slot = (now.getMinute() / 15) * 15;
         LocalDateTime floorNow = now.withMinute(slot).withSecond(0).withNano(0);
         LocalDateTime floorNow = now.withMinute(slot).withSecond(0).withNano(0);
         LocalDateTime point = floorNow.minusMinutes(periodMinutes);
         LocalDateTime point = floorNow.minusMinutes(periodMinutes);
         String s = point.format(POINT_FMT);
         String s = point.format(POINT_FMT);
+        // 指定补断面时刻时优先使用传入时间(手动补历史断面)
+        if (pointTime != null && !pointTime.isEmpty()) {
+            s = pointTime;
+        }
         StateEstimationRequest req = new StateEstimationRequest();
         StateEstimationRequest req = new StateEstimationRequest();
         req.setContainerCode(CONTAINER_CODE);
         req.setContainerCode(CONTAINER_CODE);
         req.setStartTime(s);
         req.setStartTime(s);