Pārlūkot izejas kodu

监测曲线复用基线方法,调整补贴完成率与偏差率

Co-authored-by: Cursor <cursoragent@cursor.com>
fuyuchuan 6 dienas atpakaļ
vecāks
revīzija
ea6bac9788

+ 4 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java

@@ -56,8 +56,10 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:两条基线(5日/3日)+ 当日实测;时间窗为响应前2小时 + 响应时长 + 响应后1小时
-     * <p>baselineDays 传 3 或 5 时优先展示对应基线到 baselineKw;为空则同时返回两条。</p>
+     * 折线图:基线负荷 + 实际响应容量;时间窗为响应前2小时 + 响应时长 + 响应后1小时
+     * <p>基线负荷复用 getSiteBaseline,实际响应容量复用 getSiteDeclaredCapacity。</p>
+     * <p>id 为响应记录/事件主键;siteIds 为站点范围,二者不是同一资源。</p>
+     * <p>baselineDays 传 3 或 5 时按对应周期计算基线负荷;为空则按 5 日基线。</p>
      */
     @GetMapping("/records/{id}/curve")
     public ApiResult<DrMonitorCurveVO> curve(@PathVariable("id") Long id,

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java

@@ -87,11 +87,11 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("price_adjustment_coefficient")
     private BigDecimal priceAdjustmentCoefficient;
 
-    /** 完成率(百分比)= 平台实际响应量 / 申报出清量 × 100 */
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     @TableField("completion_rate")
     private BigDecimal completionRate;
 
-    /** 偏差率(百分比)= (电网实际响应量 − 平台实际响应量) / 申报出清量 × 100 */
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     @TableField("deviation_rate")
     private BigDecimal deviationRate;
 

+ 4 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java

@@ -29,9 +29,11 @@ public interface VppDrMonitorService {
                                               List<Long> siteIds, Map<String, Object> params);
 
     /**
-     * 折线图:基线/实际负荷按可调峰资源点加权平均;申报/实际响应按资源点平均
+     * 折线图:基线负荷复用 getSiteBaseline,实际响应容量复用 getSiteDeclaredCapacity,再按可调峰资源点平均
      *
-     * @param baselineDays 基线参考天数,仅支持 3 或 5;为空时同时返回 3/5 日两条基线
+     * @param eventId      响应记录/事件主键
+     * @param siteIds      站点范围(与 eventId 互补,不是同一资源)
+     * @param baselineDays 基线参考天数,仅支持 3 或 5;为空时按 5 日基线
      */
     DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds, Integer baselineDays);
 }

+ 50 - 56
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -22,6 +22,7 @@ import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import com.usky.vpp.service.vo.SiteBaselineVO;
+import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
 import org.springframework.beans.BeanUtils;
@@ -257,62 +258,50 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
 
         List<VppDrInvitation> invitations = listInvitations(
                 Collections.singleton(event.getId()), peakSiteIds, tenantId);
-        BigDecimal declaredSum = BigDecimal.ZERO;
-        BigDecimal actualSum = BigDecimal.ZERO;
+        Map<Long, BigDecimal> invitationActualBySite = new HashMap<>();
         for (VppDrInvitation invitation : invitations) {
-            declaredSum = declaredSum.add(nz(invitation.getDeclaredCapacityKw()));
-            actualSum = actualSum.add(nz(invitation.getActualResponseCapacityKw()));
+            if (invitation.getSiteId() == null) {
+                continue;
+            }
+            invitationActualBySite.merge(invitation.getSiteId(),
+                    nz(invitation.getActualResponseCapacityKw()), BigDecimal::add);
         }
-        BigDecimal declaredAvg = avg(declaredSum, resourceCount);
-        BigDecimal actualAvg = avg(actualSum, resourceCount);
 
         LocalDateTime windowStart = event.getStartTime().minusHours(2);
         LocalDateTime windowEnd = (event.getEndTime() != null ? event.getEndTime() : event.getStartTime()).plusHours(1);
         String responseStartTime = event.getStartTime().format(DATE_TIME_FMT);
+        String eventEndTime = (event.getEndTime() != null ? event.getEndTime() : event.getStartTime()).format(DATE_TIME_FMT);
         String actualEndTime = windowEnd.format(DATE_TIME_FMT);
+        int requiredDays = (baselineDays != null && baselineDays == 3) ? 3 : 5;
 
-        Map<String, BigDecimal> baseline5Weighted = new LinkedHashMap<>();
-        Map<String, BigDecimal> baseline3Weighted = new LinkedHashMap<>();
-        Map<String, BigDecimal> loadWeighted = new LinkedHashMap<>();
+        BigDecimal actualSum = BigDecimal.ZERO;
+        for (Long siteId : peakSiteIds) {
+            actualSum = actualSum.add(resolveSiteActualResponseKw(
+                    siteId, responseStartTime, eventEndTime, invitationActualBySite.get(siteId)));
+        }
+        BigDecimal actualAvg = avg(actualSum, resourceCount);
+
+        Map<String, BigDecimal> baselineWeighted = new LinkedHashMap<>();
         Set<String> dataSources = new LinkedHashSet<>();
         List<Long> usedSiteIds = new ArrayList<>();
 
         for (Map.Entry<Long, Long> entry : siteResourceCount.entrySet()) {
             Long siteId = entry.getKey();
             BigDecimal weight = BigDecimal.valueOf(entry.getValue());
-            SiteBaselineVO baseline5 = baselineService.getSiteBaseline(siteId, responseStartTime, 5, actualEndTime);
-            SiteBaselineVO baseline3 = baselineService.getSiteBaseline(siteId, responseStartTime, 3, actualEndTime);
-            if ((baseline5 == null || CollectionUtils.isEmpty(baseline5.getPoints()))
-                    && (baseline3 == null || CollectionUtils.isEmpty(baseline3.getPoints()))) {
+            SiteBaselineVO baseline = baselineService.getSiteBaseline(siteId, responseStartTime, requiredDays, actualEndTime);
+            if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
                 continue;
             }
             usedSiteIds.add(siteId);
-            if (baseline5 != null && StringUtils.hasText(baseline5.getDataSource())) {
-                dataSources.add(baseline5.getDataSource());
+            if (StringUtils.hasText(baseline.getDataSource())) {
+                dataSources.add(baseline.getDataSource());
             }
-            if (baseline5 != null && !CollectionUtils.isEmpty(baseline5.getPoints())) {
-                for (BaselinePointVO point : baseline5.getPoints()) {
-                    if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
-                        continue;
-                    }
-                    baseline5Weighted.merge(point.getTime(),
-                            nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
-                    loadWeighted.merge(point.getTime(),
-                            nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
-                }
-            }
-            if (baseline3 != null && !CollectionUtils.isEmpty(baseline3.getPoints())) {
-                for (BaselinePointVO point : baseline3.getPoints()) {
-                    if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
-                        continue;
-                    }
-                    baseline3Weighted.merge(point.getTime(),
-                            nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
-                    if (!loadWeighted.containsKey(point.getTime())) {
-                        loadWeighted.merge(point.getTime(),
-                                nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
-                    }
+            for (BaselinePointVO point : baseline.getPoints()) {
+                if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
+                    continue;
                 }
+                baselineWeighted.merge(point.getTime(),
+                        nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
             }
         }
 
@@ -322,7 +311,6 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         curve.setEventId(event.getEventId());
         curve.setResponseDate(event.getStartTime().toLocalDate().format(DATE_FMT));
         curve.setSiteIds(usedSiteIds);
-        curve.setDeclaredCapacityKw(declaredAvg);
         curve.setActualResponseCapacityKw(actualAvg);
         if (dataSources.isEmpty()) {
             curve.setDataSource("mock");
@@ -332,35 +320,41 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
             curve.setDataSource("mixed");
         }
 
-        Set<String> timeSet = new LinkedHashSet<>();
-        timeSet.addAll(baseline5Weighted.keySet());
-        timeSet.addAll(baseline3Weighted.keySet());
-        timeSet.addAll(loadWeighted.keySet());
-        List<String> times = new ArrayList<>(timeSet);
+        List<String> times = new ArrayList<>(baselineWeighted.keySet());
         times.sort(Comparator.naturalOrder());
         for (String time : times) {
             DrMonitorCurveVO.Point point = new DrMonitorCurveVO.Point();
             point.setTime(time);
-            BigDecimal b5 = baseline5Weighted.getOrDefault(time, BigDecimal.ZERO)
-                    .divide(divisor, SCALE, RoundingMode.HALF_UP);
-            BigDecimal b3 = baseline3Weighted.getOrDefault(time, BigDecimal.ZERO)
-                    .divide(divisor, SCALE, RoundingMode.HALF_UP);
-            point.setBaseline5DayKw(b5);
-            point.setBaseline3DayKw(b3);
-            if (baselineDays != null && baselineDays == 3) {
-                point.setBaselineKw(b3);
-            } else {
-                point.setBaselineKw(b5);
-            }
-            point.setLoadKw(loadWeighted.getOrDefault(time, BigDecimal.ZERO)
+            point.setBaselineKw(baselineWeighted.getOrDefault(time, BigDecimal.ZERO)
                     .divide(divisor, SCALE, RoundingMode.HALF_UP));
-            point.setDeclaredCapacityKw(declaredAvg);
             point.setActualResponseCapacityKw(actualAvg);
             curve.getPoints().add(point);
         }
         return curve;
     }
 
+    /**
+     * 实际响应容量复用申报容量链路 {@link VppBaselineService#getSiteDeclaredCapacity},
+     * 取预估响应容量;无数据时回退邀约实际响应容量。
+     */
+    private BigDecimal resolveSiteActualResponseKw(Long siteId, String responseStartTime,
+                                                   String responseEndTime, BigDecimal fallbackKw) {
+        if (siteId == null || !StringUtils.hasText(responseStartTime) || !StringUtils.hasText(responseEndTime)) {
+            return nz(fallbackKw);
+        }
+        try {
+            SiteDeclaredCapacityVO capacity = baselineService.getSiteDeclaredCapacity(
+                    siteId, responseStartTime, responseEndTime);
+            if (capacity != null && capacity.getEstimatedResponseCapacityKw() != null
+                    && capacity.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
+                return capacity.getEstimatedResponseCapacityKw();
+            }
+        } catch (Exception ignored) {
+            // 回退邀约实际响应容量,保持曲线可返回
+        }
+        return nz(fallbackKw);
+    }
+
     private boolean inCurveWindow(String timeKey, LocalDateTime windowStart, LocalDateTime windowEnd) {
         try {
             LocalTime t = LocalTime.parse(timeKey.length() == 5 ? timeKey : timeKey.substring(0, 5));

+ 16 - 19
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -133,6 +133,9 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
                 ? invitation.getDeclaredCapacityKw()
                 : invitation.getActualClearedCapacityKw();
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
+                ? invitation.getActualClearedCapacityKw()
+                : declaredKw;
         BigDecimal platformKw = resolvePlatformActualResponseKw(
                 invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
                 invitation.getEstimatedResponseCapacityKw());
@@ -141,7 +144,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         entity.setActualResponsePlatformKw(platformKw);
         entity.setDeclaredClearedKw(declaredKw);
         entity.setEstimatedCapacityKw(platformKw);
-        entity.setClearedCapacityKw(declaredKw);
+        entity.setClearedCapacityKw(actualClearedKw);
         entity.setTenantId(event.getTenantId() != null ? event.getTenantId() : invitation.getTenantId());
 
         recalculate(entity, loadActivePriceAdjustments(
@@ -319,12 +322,15 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
                 ? invitation.getDeclaredCapacityKw()
                 : invitation.getActualClearedCapacityKw();
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
+                ? invitation.getActualClearedCapacityKw()
+                : declaredKw;
 
         p.setActualResponsePlatformKw(platformKw);
         p.setActualResponseGridKw(gridKw);
         p.setDeclaredClearedKw(declaredKw);
         p.setEstimatedCapacityKw(platformKw);
-        p.setClearedCapacityKw(declaredKw);
+        p.setClearedCapacityKw(actualClearedKw);
         p.setTenantId(event.getTenantId());
         recalculate(p, adjustments);
         return p;
@@ -508,8 +514,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     /**
      * 自动计算:响应时长、偏差率、完成率、补贴金额
      * <ul>
-     *   <li>完成率(%)= 平台实际响应量 / 申报出清量 × 100(≥0,正常 0~100,超发可 &gt;100)</li>
-     *   <li>偏差率(%)= (电网实际响应量 − 平台实际响应量) / 申报出清量 × 100(可正可负)</li>
+     *   <li>完成率(%)= 实际响应容量(电网) / 实际出清 × 100(实际出清为 null 或 ≤0 时不计算)</li>
+     *   <li>偏差率(%)= (1 − 完成率) × 100</li>
      *   <li>价格调整系数:按完成率比例匹配响应量偏差价格调整配置</li>
      *   <li>补贴金额 = 电网实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
      * </ul>
@@ -523,16 +529,12 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         }
         entity.setResponseDurationMin(durationMin);
 
-        BigDecimal platformKw = entity.getActualResponsePlatformKw() != null
-                ? entity.getActualResponsePlatformKw()
-                : entity.getEstimatedCapacityKw();
         BigDecimal gridKw = entity.getActualResponseGridKw();
-        BigDecimal declaredKw = entity.getDeclaredClearedKw() != null
-                ? entity.getDeclaredClearedKw()
-                : entity.getClearedCapacityKw();
+        BigDecimal clearedKw = entity.getClearedCapacityKw() != null
+                ? entity.getClearedCapacityKw()
+                : entity.getDeclaredClearedKw();
 
-        if (declaredKw == null || declaredKw.compareTo(BigDecimal.ZERO) <= 0
-                || platformKw == null || gridKw == null) {
+        if (clearedKw == null || clearedKw.compareTo(BigDecimal.ZERO) <= 0 || gridKw == null) {
             entity.setDeviationRate(null);
             entity.setCompletionRate(null);
             entity.setPriceAdjustmentCoefficient(null);
@@ -540,13 +542,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
             return;
         }
 
-        // 完成率用平台量;价格档按完成率比例匹配
-        BigDecimal completionRatio = platformKw.divide(declaredKw, 6, RoundingMode.HALF_UP);
-        if (completionRatio.compareTo(BigDecimal.ZERO) < 0) {
-            completionRatio = BigDecimal.ZERO;
-        }
-        BigDecimal deviationRatio = gridKw.subtract(platformKw)
-                .divide(declaredKw, 6, RoundingMode.HALF_UP);
+        BigDecimal completionRatio = gridKw.divide(clearedKw, 6, RoundingMode.HALF_UP);
+        BigDecimal deviationRatio = BigDecimal.ONE.subtract(completionRatio);
         BigDecimal priceAdjustmentCoefficient = resolvePriceAdjustmentCoefficient(completionRatio, adjustments);
 
         entity.setCompletionRate(completionRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));

+ 2 - 12
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorCurveVO.java

@@ -7,7 +7,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * 响应监测 - 折线图(基线/实际负荷 + 申报/实际响应容量)
+ * 响应监测 - 折线图(基线负荷 + 实际响应容量)
  */
 @Data
 public class DrMonitorCurveVO {
@@ -18,8 +18,6 @@ public class DrMonitorCurveVO {
     /** tsdb / mock / mixed */
     private String dataSource;
     private List<Long> siteIds = new ArrayList<>();
-    /** 申报容量合计 kW(邀约) */
-    private BigDecimal declaredCapacityKw = BigDecimal.ZERO;
     /** 实际响应容量合计 kW(邀约) */
     private BigDecimal actualResponseCapacityKw = BigDecimal.ZERO;
     private List<Point> points = new ArrayList<>();
@@ -28,16 +26,8 @@ public class DrMonitorCurveVO {
     public static class Point {
         /** 时刻 HH:mm */
         private String time;
-        /** 基线负荷(兼容旧字段,默认取 5 日或指定周期) */
+        /** 基线负荷 kW */
         private BigDecimal baselineKw;
-        /** 5 日基线 kW */
-        private BigDecimal baseline5DayKw;
-        /** 3 日基线 kW */
-        private BigDecimal baseline3DayKw;
-        /** 实际负荷 kW */
-        private BigDecimal loadKw;
-        /** 申报容量 kW */
-        private BigDecimal declaredCapacityKw;
         /** 实际响应容量 kW */
         private BigDecimal actualResponseCapacityKw;
     }

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionRequest.java

@@ -56,10 +56,10 @@ public class DrSubsidyPredictionRequest {
     /** 响应时长(分钟,试算结果) */
     private Integer responseDurationMin;
 
-    /** 偏差率(百分比)= (电网−平台)/申报出清 ×100 */
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     private BigDecimal deviationRate;
 
-    /** 完成率(百分比)= 平台实际响应量/申报出清 ×100 */
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     private BigDecimal completionRate;
 
     /** 价格调整系数(试算结果) */

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java

@@ -61,10 +61,10 @@ public class DrSubsidyPredictionVO {
     /** 按完成率匹配得到的价格调整系数。 */
     private BigDecimal priceAdjustmentCoefficient;
 
-    /** 完成率(百分比)= 平台实际响应量 / 申报出清量 × 100(≥0,正常 0~100,超发可 &gt;100) */
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     private BigDecimal completionRate;
 
-    /** 偏差率(百分比)= (电网实际响应量 − 平台实际响应量) / 申报出清量 × 100(可正可负) */
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     private BigDecimal deviationRate;
 
     /** 预估补贴金额(元) */