Prechádzať zdrojové kódy

线缆段状估存储

lisonglin 6 hodín pred
rodič
commit
0d8205e0b3

+ 65 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeSegmentPqui.java

@@ -0,0 +1,65 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 段状估 PQUI 明细(每断面每段一行)
+ * <p>
+ * 数据来源:状估 JSON periodSegmentSeResult(0201 电缆段 + dxd 导线段)
+ * 取数:P=seoPfs(首端有功)、Q=seoQfs(首端无功)、U=headUs(首端电压)、I=seoIs(电流)
+ * 归属:段数据只有 feederId,subs/county 由馈线映射带出。
+ * <p>
+ * 用途:30 天平均 PQUI 的明细层(日统计表从本表聚合)。
+ *
+ * @author lsl
+ * @since 2026-08
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("fhzg_se_segment_pqui")
+public class FhzgSeSegmentPqui implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 断面时间 */
+    private LocalDateTime snapTime;
+
+    /** 段ID(psrId) */
+    private String segmentId;
+
+    /** 段类型: 0201=电缆段 dxd=导线段 */
+    private String psrType;
+
+    private String feederId;
+    private String subsId;
+    private String countyId;
+
+    /** P 有功(首端 seoPfs, MW) */
+    private BigDecimal pValue;
+
+    /** Q 无功(首端 seoQfs, MVar) */
+    private BigDecimal qValue;
+
+    /** U 电压(首端 headUs, kV) */
+    private BigDecimal uValue;
+
+    /** I 电流(seoIs, A) */
+    private BigDecimal iValue;
+
+    private LocalDateTime createTime;
+}

+ 68 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/entity/FhzgSeSegmentPquiDaily.java

@@ -0,0 +1,68 @@
+package com.hdkj.lt.bf.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 段状估 PQUI 日统计(每段每天一行)
+ * <p>
+ * 存当日 sum + count(不存 avg),查询 30 天平均时断面数加权:
+ * avgX = SUM(sum_x) / SUM(snap_count),零精度损失。
+ * <p>
+ * 先删后插 + @Transactional,唯一键 uk_date_segment 防重跑翻倍。
+ *
+ * @author lsl
+ * @since 2026-08
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("fhzg_se_segment_pqui_daily")
+public class FhzgSeSegmentPquiDaily implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 统计日期 */
+    private LocalDate statDate;
+
+    /** 段ID(psrId) */
+    private String segmentId;
+
+    /** 段类型: 0201=电缆段 dxd=导线段 */
+    private String psrType;
+
+    private String feederId;
+    private String subsId;
+    private String countyId;
+
+    /** 当日 P 求和 */
+    private BigDecimal sumP;
+
+    /** 当日 Q 求和 */
+    private BigDecimal sumQ;
+
+    /** 当日 U 求和 */
+    private BigDecimal sumU;
+
+    /** 当日 I 求和 */
+    private BigDecimal sumI;
+
+    /** 当日断面数 */
+    private Integer snapCount;
+
+    private LocalDateTime createTime;
+}

+ 9 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeSegmentPquiDailyMapper.java

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

+ 9 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/mapper/FhzgSeSegmentPquiMapper.java

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

+ 115 - 0
common/common-se/src/main/java/com/hdkj/lt/bf/scheduler/SeSegmentPquiDailyScheduler.java

@@ -0,0 +1,115 @@
+package com.hdkj.lt.bf.scheduler;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.hdkj.lt.bf.entity.FhzgSeSegmentPqui;
+import com.hdkj.lt.bf.entity.FhzgSeSegmentPquiDaily;
+import com.hdkj.lt.bf.mapper.FhzgSeSegmentPquiMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSegmentPquiDailyMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 段 PQUI 日汇总逻辑(公共模块)
+ * <p>
+ * 定时调度由 pwfhzg-job 通过 sys_job 配置触发(凌晨1点聚合昨天),
+ * 也可由 bf 手动端点触发。
+ * <p>
+ * 从明细表 fhzg_se_segment_pqui 按段聚合当日 sum + count 写 fhzg_se_segment_pqui_daily。
+ * 先删后插 + @Transactional,唯一键 uk_date_segment 防重跑翻倍。
+ *
+ * @author lsl
+ * @since 2026-08
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SeSegmentPquiDailyScheduler {
+
+    private final FhzgSeSegmentPquiMapper segmentPquiMapper;
+    private final FhzgSeSegmentPquiDailyMapper segmentPquiDailyMapper;
+
+    /**
+     * 聚合指定日期的段 PQUI 明细到日统计表
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public String aggregateForDate(LocalDate date) {
+        LocalDateTime startTime = date.atStartOfDay();
+        LocalDateTime endTime = date.atTime(23, 59, 59);
+
+        // 先删当日旧数据(防重跑叠加)
+        segmentPquiDailyMapper.delete(new LambdaQueryWrapper<FhzgSeSegmentPquiDaily>()
+                .eq(FhzgSeSegmentPquiDaily::getStatDate, date));
+
+        // 明细按段 SQL 聚合:SUM + COUNT(snap_count 为断面数)
+        QueryWrapper<FhzgSeSegmentPqui> wrapper = new QueryWrapper<FhzgSeSegmentPqui>()
+                .select("segment_id",
+                        "MAX(psr_type) AS psr_type",
+                        "MAX(feeder_id) AS feeder_id",
+                        "MAX(subs_id) AS subs_id",
+                        "MAX(county_id) AS county_id",
+                        "COALESCE(SUM(p_value), 0) AS sum_p",
+                        "COALESCE(SUM(q_value), 0) AS sum_q",
+                        "COALESCE(SUM(u_value), 0) AS sum_u",
+                        "COALESCE(SUM(i_value), 0) AS sum_i",
+                        "COUNT(*) AS snap_count")
+                .ge("snap_time", startTime)
+                .le("snap_time", endTime)
+                .groupBy("segment_id");
+
+        List<Map<String, Object>> rows = segmentPquiMapper.selectMaps(wrapper);
+        if (rows == null || rows.isEmpty()) {
+            return "无明细数据";
+        }
+
+        List<FhzgSeSegmentPquiDaily> batch = new ArrayList<>();
+        LocalDateTime now = LocalDateTime.now();
+        for (Map<String, Object> row : rows) {
+            batch.add(FhzgSeSegmentPquiDaily.builder()
+                    .statDate(date)
+                    .segmentId(str(row.get("segment_id")))
+                    .psrType(str(row.get("psr_type")))
+                    .feederId(str(row.get("feeder_id")))
+                    .subsId(str(row.get("subs_id")))
+                    .countyId(str(row.get("county_id")))
+                    .sumP(dec(row.get("sum_p")))
+                    .sumQ(dec(row.get("sum_q")))
+                    .sumU(dec(row.get("sum_u")))
+                    .sumI(dec(row.get("sum_i")))
+                    .snapCount(intOf(row.get("snap_count")))
+                    .createTime(now)
+                    .build());
+        }
+
+        int inserted = 0;
+        for (FhzgSeSegmentPquiDaily d : batch) {
+            segmentPquiDailyMapper.insert(d);
+            inserted++;
+        }
+        log.info("[段PQUI日汇总] {} 聚合完成, 写入{}条", date, inserted);
+        return "写入 " + inserted + " 条";
+    }
+
+    private String str(Object o) {
+        return o == null ? null : o.toString();
+    }
+
+    private BigDecimal dec(Object o) {
+        return o == null ? null : new BigDecimal(o.toString());
+    }
+
+    private Integer intOf(Object o) {
+        if (o == null) return null;
+        if (o instanceof Number) return ((Number) o).intValue();
+        return Integer.parseInt(o.toString());
+    }
+}

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

@@ -10,6 +10,7 @@ import com.hdkj.lt.bf.entity.dto.SeVoltageAlarmListReq;
 import com.hdkj.lt.bf.entity.vo.PageResult;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
+import com.hdkj.lt.bf.entity.vo.SeSegmentPquiVO;
 import com.hdkj.lt.bf.entity.vo.FeederSelectVO;
 import com.hdkj.lt.bf.entity.vo.SeAlarmDetailVO;
 import com.hdkj.lt.bf.entity.vo.SeAlarmListVO;
@@ -123,6 +124,20 @@ public class IndicatorController extends BaseController {
         return ApiResponse.success(seIndicatorService.queryLineLoss(id, Integer.parseInt(type), timeScope));
     }
 
+    /**
+     * 段 PQUI 30 天平均(按馈线查该线路下所有段)
+     *
+     * @param params {feederId}
+     */
+    @PostMapping("/segment-pqui")
+    public ApiResponse<List<SeSegmentPquiVO>> segmentPqui(@RequestBody Map<String, String> params) {
+        String feederId = params.get("feederId");
+        if (feederId == null) {
+            return ApiResponse.fail("参数缺失");
+        }
+        return ApiResponse.success(seIndicatorService.querySegmentPqui(feederId));
+    }
+
     /**
      * 异常告警-线路重过载列表
      */

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

@@ -0,0 +1,64 @@
+package com.hdkj.lt.bf.entity.vo;
+
+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;
+
+/**
+ * 段 PQUI 30 天平均 VO(贴状估接口段结构:feederId + psrId + psrType + p/q/u/i)
+ * <p>
+ * 口径:P=seoPfs、Q=seoQfs、U=headUs、I=seoIs(首端取值);
+ * 30 天平均 = 断面数加权(SUM(sum_x)/SUM(snap_count))。
+ *
+ * @author lsl
+ * @since 2026-08
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel(description = "段PQUI 30天平均")
+public class SeSegmentPquiVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 设备所属线路 */
+    @ApiModelProperty(value = "设备所属线路")
+    private String feederId;
+
+    /** 设备ID(段 psrId) */
+    @ApiModelProperty(value = "设备ID(段psrId)")
+    private String psrId;
+
+    /** 段类型: 0201=电缆段 dxd=导线段 */
+    @ApiModelProperty(value = "段类型: 0201=电缆段 dxd=导线段")
+    private String psrType;
+
+    /** 30天平均有功(MW) */
+    @ApiModelProperty(value = "30天平均有功(MW)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal p;
+
+    /** 30天平均无功(MVar) */
+    @ApiModelProperty(value = "30天平均无功(MVar)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal q;
+
+    /** 30天平均电压(kV) */
+    @ApiModelProperty(value = "30天平均电压(kV)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal u;
+
+    /** 30天平均电流(A) */
+    @ApiModelProperty(value = "30天平均电流(A)")
+    @JsonSerialize(using = BigDecimalSerializer.StripTrailingZerosSerializer.class)
+    private BigDecimal i;
+}

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

@@ -3,9 +3,12 @@ package com.hdkj.lt.bf.service;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
+import com.hdkj.lt.bf.entity.vo.SeSegmentPquiVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 
+import java.util.List;
+
 /**
  * 状估监测体系 三个查询接口 Service
  *
@@ -57,4 +60,12 @@ public interface SeIndicatorService {
      * @param timeScope today/month/year
      */
     SeLineLossRateVO queryLineLoss(String id, Integer type, String timeScope);
+
+    /**
+     * 段 PQUI 30 天平均(按馈线查该线路下所有段)
+     *
+     * @param feederId 馈线ID
+     * @return 段列表(feederId/psrId/psrType/p/q/u/i),30 天断面数加权平均
+     */
+    List<SeSegmentPquiVO> querySegmentPqui(String feederId);
 }

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

@@ -6,6 +6,7 @@ import com.hdkj.lt.bf.application.FeederGroupApplication;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
 import com.hdkj.lt.bf.entity.FhzgSeLineLossDaily;
+import com.hdkj.lt.bf.entity.FhzgSeSegmentPquiDaily;
 import com.hdkj.lt.bf.entity.FhzgDwmVoltageOverLimitDaily;
 import com.hdkj.lt.bf.entity.FhzgSeCapacityDaily;
 import com.hdkj.lt.bf.entity.FhzgSeVoltageEvent;
@@ -15,12 +16,14 @@ import com.hdkj.lt.bf.entity.FhzgSeConsumerRelation;
 import com.hdkj.lt.bf.entity.dto.TodayOutageData;
 import com.hdkj.lt.bf.entity.vo.SeCapacityDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeLineLossRateVO;
+import com.hdkj.lt.bf.entity.vo.SeSegmentPquiVO;
 import com.hdkj.lt.bf.entity.vo.SeOutageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageDashboardVO;
 import com.hdkj.lt.bf.entity.vo.SeVoltageIndexVO;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeLineLossRateMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeLineLossDailyMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSegmentPquiDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgDwmVoltageOverLimitDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCapacityDailyMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
@@ -65,6 +68,7 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
     private final FhzgSeConsumerRelationMapper consumerRelationMapper;
     private final FhzgSeLineLossRateMapper lineLossRateMapper;
     private final FhzgSeLineLossDailyMapper lineLossDailyMapper;
+    private final FhzgSeSegmentPquiDailyMapper segmentPquiDailyMapper;
     private final FeederGroupApplication feederGroupApplication;
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
@@ -647,6 +651,67 @@ public class SeIndicatorServiceImpl implements SeIndicatorService {
         return SeLineLossRateVO.builder().lineLossRate(rate).build();
     }
 
+    /**
+     * 段 PQUI 30 天平均:查日统计表 fhzg_se_segment_pqui_daily 近 30 天,按段断面数加权
+     * <p>
+     * avgX = SUM(sum_x) / SUM(snap_count)。返回该馈线下所有段(贴状估接口结构)。
+     */
+    @Override
+    public List<SeSegmentPquiVO> querySegmentPqui(String feederId) {
+        if (StringUtils.isBlank(feederId)) {
+            return Collections.emptyList();
+        }
+
+        LocalDate startDate = LocalDate.now().minusDays(30);
+        QueryWrapper<FhzgSeSegmentPquiDaily> qw = new QueryWrapper<>();
+        qw.select("segment_id",
+                "MAX(psr_type) AS psr_type",
+                "MAX(feeder_id) AS feeder_id",
+                "COALESCE(SUM(sum_p), 0) AS total_p",
+                "COALESCE(SUM(sum_q), 0) AS total_q",
+                "COALESCE(SUM(sum_u), 0) AS total_u",
+                "COALESCE(SUM(sum_i), 0) AS total_i",
+                "COALESCE(SUM(snap_count), 0) AS total_count")
+                .eq("feeder_id", feederId)
+                .ge("stat_date", startDate)
+                .groupBy("segment_id");
+
+        List<Map<String, Object>> rows = segmentPquiDailyMapper.selectMaps(qw);
+        if (rows == null || rows.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<SeSegmentPquiVO> result = new ArrayList<>();
+        for (Map<String, Object> row : rows) {
+            int count = ((Number) row.getOrDefault("total_count", 0)).intValue();
+            if (count <= 0) continue;
+            result.add(SeSegmentPquiVO.builder()
+                    .feederId(str(row.get("feeder_id")))
+                    .psrId(str(row.get("segment_id")))
+                    .psrType(str(row.get("psr_type")))
+                    .p(weightedAvg(row.get("total_p"), count))
+                    .q(weightedAvg(row.get("total_q"), count))
+                    .u(weightedAvg(row.get("total_u"), count))
+                    .i(weightedAvg(row.get("total_i"), count))
+                    .build());
+        }
+        return result;
+    }
+
+    private String str(Object o) {
+        return o == null ? null : o.toString();
+    }
+
+    private BigDecimal weightedAvg(Object sum, int count) {
+        if (sum == null) return null;
+        try {
+            return new BigDecimal(sum.toString())
+                    .divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
     /**
      * 构建馈线今日96点时序(负载率+电流+电压+有功+无功)
      */

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

@@ -11,6 +11,7 @@ import com.hdkj.lt.bf.common.SeSnapshotConstants;
 import com.hdkj.lt.bf.entity.DwdShbDsTransformerBase;
 import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
 import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
+import com.hdkj.lt.bf.entity.FhzgSeSegmentPqui;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
 import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
 import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
@@ -20,6 +21,7 @@ import com.hdkj.lt.bf.event.ReconTriggerEvent;
 import com.hdkj.lt.bf.mapper.FeederTopoMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeLineLossRateMapper;
+import com.hdkj.lt.bf.mapper.FhzgSeSegmentPquiMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
 import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
@@ -56,6 +58,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
     private final ApplicationEventPublisher eventPublisher;
     private final FeederTopoMapper feederTopoMapper;
     private final FhzgSeLineLossRateMapper lineLossRateMapper;
+    private final FhzgSeSegmentPquiMapper segmentPquiMapper;
 
     private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
@@ -169,6 +172,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         // 总功率=馈线块 seoPs(≈出线开关功率);配变块=0302+0110;中压用户接入点块=370000
         processLineLossRate(root, snapTime, feederMap, advanceState);
 
+        // === 4. 段 PQUI 明细(0201 电缆段 + dxd 导线段,首端取值)===
+        // P=seoPfs Q=seoQfs U=headUs I=seoIs;每断面每段一行 → fhzg_se_segment_pqui
+        processSegmentPqui(root, snapTime, feederMap);
+
         if (!batch.isEmpty()) {
             // 防重:查该断面时刻已有的所有 device_id(精确到秒,无需再 IN 过滤)
             Set<String> existingIds = new HashSet<>();
@@ -637,8 +644,84 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
         }
     }
 
+    /**
+     * 段 PQUI 明细入库:periodSegmentSeResult(0201 电缆段 + dxd 导线段)
+     * <p>
+     * 取数:P=seoPfs(首端有功)、Q=seoQfs(首端无功)、U=headUs(首端电压)、I=seoIs(电流)。
+     * 每断面每段一行写 fhzg_se_segment_pqui;段数据只有 feederId,subs/county 由 feederMap 带出。
+     * 防重:查该断面时刻已有的 segment_id(避免重复跑同一断面时叠加)。
+     */
+    private void processSegmentPqui(JSONObject root, LocalDateTime snapTime,
+                                    Map<String, String[]> feederMap) {
+        try {
+            JSONArray segments = root.getJSONArray("periodSegmentSeResult");
+            if (segments == null || segments.isEmpty()) {
+                return;
+            }
+            List<FhzgSeSegmentPqui> batch = new ArrayList<>();
+            LocalDateTime now = LocalDateTime.now();
+            for (int i = 0; i < segments.size(); i++) {
+                JSONObject s = segments.getJSONObject(i);
+                String segmentId = s.getString("psrId");
+                if (segmentId == null || segmentId.isEmpty()) continue;
+                String feederId = s.getString("feederId");
+                if (feederId == null || feederId.isEmpty()) continue;
+
+                BigDecimal p = firstValue(s.getJSONArray("seoPfs"));
+                BigDecimal q = firstValue(s.getJSONArray("seoQfs"));
+                BigDecimal u = firstValue(s.getJSONArray("headUs"));
+                BigDecimal cur = firstValue(s.getJSONArray("seoIs"));
+                if (p == null && q == null && u == null && cur == null) continue;
+
+                String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
+                batch.add(FhzgSeSegmentPqui.builder()
+                        .snapTime(snapTime)
+                        .segmentId(segmentId)
+                        .psrType(s.getString("psrType"))
+                        .feederId(feederId)
+                        .subsId(loc.length > 0 ? loc[0] : "")
+                        .countyId(loc.length > 1 ? loc[1] : "")
+                        .pValue(p)
+                        .qValue(q)
+                        .uValue(u)
+                        .iValue(cur)
+                        .createTime(now)
+                        .build());
+            }
+
+            if (!batch.isEmpty()) {
+                // 防重:查该断面时刻已有的段(避免重复跑同一断面时叠加)
+                Set<String> existingSegmentIds = new HashSet<>();
+                segmentPquiMapper.selectList(
+                        new LambdaQueryWrapper<FhzgSeSegmentPqui>()
+                                .select(FhzgSeSegmentPqui::getSegmentId)
+                                .eq(FhzgSeSegmentPqui::getSnapTime, snapTime)
+                ).forEach(e -> existingSegmentIds.add(e.getSegmentId()));
+
+                List<FhzgSeSegmentPqui> toInsert = new ArrayList<>();
+                for (FhzgSeSegmentPqui d : batch) {
+                    if (existingSegmentIds.contains(d.getSegmentId())) continue;
+                    toInsert.add(d);
+                }
+
+                int inserted = 0;
+                if (!toInsert.isEmpty()) {
+                    inserted = insertBatchSharded(rows -> {
+                        toInsert.forEach(segmentPquiMapper::insert);
+                        return toInsert.size();
+                    }, toInsert);
+                }
+                log.info("段PQUI明细入库完成 snapTime={}, 写入{}条, 跳过{}条(无数据{}条/重复{}条)",
+                        snapTime, inserted, batch.size() - inserted,
+                        batch.size() - toInsert.size(), existingSegmentIds.size());
+            }
+        } catch (Exception e) {
+            log.warn("段PQUI明细入库失败 snapTime={}", snapTime, e);
+        }
+    }
+
     // ============================================================
-    // 查询辅助
+
     // ============================================================
 
     /**

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

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