Explorar o código

Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master

hanzhengyi hai 1 semana
pai
achega
b265b21bee
Modificáronse 31 ficheiros con 778 adicións e 458 borrados
  1. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java
  2. 13 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventType.java
  3. 15 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  4. 17 9
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java
  5. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  6. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  7. 3 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  8. 14 8
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java
  9. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java
  10. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java
  11. 187 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSubsidyPredictionCalculator.java
  12. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java
  13. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java
  14. 6 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java
  15. 49 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  16. 255 289
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java
  17. 35 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  18. 30 99
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  19. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java
  20. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteCompletionRateTaskServiceImpl.java
  21. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java
  22. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventStatusRequest.java
  23. 3 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  24. 15 8
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java
  25. 35 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSiteResourceVO.java
  26. 4 6
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSummaryVO.java
  27. 5 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java
  28. 27 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SubsidyPredictionCalcResult.java
  29. 4 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java
  30. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnPayloadHelper.java
  31. 2 2
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

+ 18 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java

@@ -0,0 +1,18 @@
+package com.usky.vpp.constant;
+
+/**
+ * 需求响应事件 / 邀约响应状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消
+ * <p>vpp_dr_event.event_status 与 vpp_dr_invitation.response_status 共用同一套取值。</p>
+ */
+public final class VppDrEventStatus {
+
+    public static final int PENDING = 0;
+    public static final int DECLARED = 1;
+    public static final int DECLARE_COMPLETED = 2;
+    public static final int EXECUTING = 3;
+    public static final int ENDED = 4;
+    public static final int CANCELLED = 5;
+
+    private VppDrEventStatus() {
+    }
+}

+ 13 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventType.java

@@ -0,0 +1,13 @@
+package com.usky.vpp.constant;
+
+/**
+ * 需求响应事件类型:1削峰 2填谷
+ */
+public final class VppDrEventType {
+
+    public static final int PEAK = 1;
+    public static final int VALLEY = 2;
+
+    private VppDrEventType() {
+    }
+}

+ 15 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java

@@ -12,6 +12,7 @@ import com.usky.vpp.service.VppResponseDeviationPriceService;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrEventStatusRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
 import com.usky.vpp.service.vo.DrInvitationReplyRequest;
@@ -53,6 +54,17 @@ public class DrController {
         return ApiResult.success(vppDrService.pageEvent(params));
     }
 
+    /**
+     * 修改事件状态(独立接口,不修改事件其它字段)。
+     * <p>校验租户后,仅允许从 0待参与 改为 1已申报。</p>
+     * <p>事件状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消</p>
+     */
+    @PutMapping(value = "/event/status")
+    public ApiResult<Void> updateEventStatus(@RequestBody DrEventStatusRequest body) {
+        vppDrService.updateEventStatus(body);
+        return ApiResult.success();
+    }
+
     @GetMapping(value = "/event/{id}")
     public ApiResult<DrEventDetailVO> getEvent(@PathVariable("id") Long id) {
         return ApiResult.success(vppDrService.getEvent(id));
@@ -147,6 +159,9 @@ public class DrController {
         return ApiResult.success();
     }
 
+    /**
+     * 邀约响应状态:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消(与事件状态一致)
+     */
     @GetMapping(value = "/invitation")
     public ApiResult<CommonPage<DrInvitationVO>> pageInvitation(@RequestParam(required = false) Map<String, Object> params) {
         return ApiResult.success(vppDrInvitationService.pageInvitation(params));

+ 17 - 9
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java

@@ -5,6 +5,7 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppDrMonitorService;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
+import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -28,7 +29,7 @@ public class DrMonitorController {
 
     /**
      * 顶部 KPI:响应次数 / 响应容量(kW) / 历史达标率
-     * <p>必传站点;先查站点下 is_support_peak=1 资源点,再按资源点平均。</p>
+     * <p>时间区间内事件开始、结束均落在区间且状态为执行中/已结束,再关联参与且执行中/已结束的邀约。</p>
      */
     @GetMapping("/summary")
     public ApiResult<DrMonitorSummaryVO> summary(@RequestParam("startDate") String startDate,
@@ -38,8 +39,8 @@ public class DrMonitorController {
     }
 
     /**
-     * 响应记录分页(字段对齐 vpp_dr_event
-     * <p>必传站点;仅统计可调峰资源点参与过的事件。</p>
+     * 响应记录分页(口径与 summary 一致,按邀约展开
+     * <p>在事件字段基础上补充客户、站点;预估响应容量/实际出清取自邀约,并计算补贴金额预测。</p>
      */
     @GetMapping("/records")
     public ApiResult<CommonPage<DrMonitorRecordVO>> records(@RequestParam("startDate") String startDate,
@@ -50,13 +51,20 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:基线负荷 + 实际响应容量;查询范围为当天
-     * <p>基线负荷复用 getSiteBaseline,实际响应容量复用 getSiteDeclaredCapacity。</p>
-     * <p>baselineDays 传 3 或 5 时按对应周期计算基线负荷;为空则按 5 日基线。</p>
+     * 折线图:基线负荷全天曲线 + 实际响应容量
+     * <p>id 为邀约表主键;按邀约站点与响应时间调用 getSiteBaseline。</p>
      */
     @GetMapping("/records/curve")
-    public ApiResult<DrMonitorCurveVO> curve(@RequestParam("siteId") Long siteId,
-                                             @RequestParam(value = "baselineDays", required = false) Integer baselineDays) {
-        return ApiResult.success(drMonitorService.getCurve(siteId, baselineDays));
+    public ApiResult<DrMonitorCurveVO> curve(@RequestParam("id") Long id) {
+        return ApiResult.success(drMonitorService.getCurve(id));
+    }
+
+    /**
+     * 按响应事件查询邀约站点及其可调峰资源点
+     * <p>id 为 vpp_dr_event 主键;先查事件下全部邀约站点(去重),再查各站点 is_support_peak=1 的资源点,按站点分组返回。</p>
+     */
+    @GetMapping("/site-resources")
+    public ApiResult<List<DrMonitorSiteResourceVO>> siteResources(@RequestParam("id") Long id) {
+        return ApiResult.success(drMonitorService.listEventSitePeakResources(id));
     }
 }

+ 1 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java

@@ -60,6 +60,7 @@ public class VppDrEvent implements Serializable {
     /** 下浮系数(必填) */
     @TableField("floating_coefficient")
     private BigDecimal floatingCoefficient;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     @TableField("event_status")
     private Integer eventStatus;
     @TableField("raw_payload")

+ 1 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java

@@ -68,6 +68,7 @@ public class VppDrInvitation implements Serializable {
     private Integer isWinningBid;
     @TableField("reply_status")
     private Integer replyStatus;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     @TableField("response_status")
     private Integer responseStatus;
     @TableField("sms_notify_status")

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

@@ -83,11 +83,11 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("share_ratio")
     private BigDecimal shareRatio;
 
-    /** 按完成率(比例)命中响应量偏差价格调整表得到的价格调整系数。 */
+    /** 按响应量系数(比例)命中响应量偏差价格调整表得到的价格调整系数。 */
     @TableField("price_adjustment_coefficient")
     private BigDecimal priceAdjustmentCoefficient;
 
-    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
+    /** 完成率(百分比)= 实际出清 / 预估响应容量 × 100 */
     @TableField("completion_rate")
     private BigDecimal completionRate;
 
@@ -95,6 +95,7 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("deviation_rate")
     private BigDecimal deviationRate;
 
+    /** 预估补贴金额(元)= 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数 */
     @TableField("estimated_subsidy_amount")
     private BigDecimal estimatedSubsidyAmount;
 

+ 14 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java

@@ -3,6 +3,7 @@ package com.usky.vpp.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
+import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 
 import java.util.List;
@@ -14,26 +15,31 @@ import java.util.Map;
 public interface VppDrMonitorService {
 
     /**
-     * 顶部 KPI
+     * 顶部 KPI:按时间区间内执行中/已结束事件关联邀约统计
      *
      * @param startDate 开始日期 yyyy-MM-dd
      * @param endDate   结束日期 yyyy-MM-dd
-     * @param siteIds   站点 ID(必填,可多个);先查站点下 is_support_peak=1 资源点再平均
+     * @param siteIds   站点 ID(必填,可多个)
      */
     DrMonitorSummaryVO getSummary(String startDate, String endDate, List<Long> siteIds);
 
     /**
-     * 响应记录分页(按事件表字段;仅可调峰资源点参与过的事件
+     * 响应记录分页(查询口径与 summary 一致,按邀约展开
      */
     CommonPage<DrMonitorRecordVO> pageRecords(String startDate, String endDate,
                                               List<Long> siteIds, Map<String, Object> params);
 
     /**
-     * 折线图:以当天为查询范围,基线负荷复用 getSiteBaseline,实际响应容量复用
-     * getSiteDeclaredCapacity,再按可调峰资源点平均
+     * 折线图:按邀约主键取站点与响应时间,复用 getSiteBaseline 全天曲线,并带上实际响应容量
      *
-     * @param siteId       站点 ID(必填)
-     * @param baselineDays 基线参考天数,仅支持 3 或 5;为空时按 5 日基线
+     * @param invitationId 邀约表主键 id
      */
-    DrMonitorCurveVO getCurve(Long siteId, Integer baselineDays);
+    DrMonitorCurveVO getCurve(Long invitationId);
+
+    /**
+     * 按响应事件查询邀约站点及其可调峰资源点,以站点分组返回。
+     *
+     * @param eventId 响应事件主键 id(vpp_dr_event.id)
+     */
+    List<DrMonitorSiteResourceVO> listEventSitePeakResources(Long eventId);
 }

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java

@@ -7,6 +7,7 @@ import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrEventStatusRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
@@ -27,6 +28,11 @@ public interface VppDrService {
 
     void updateEvent(Long id, DrEventRequest request);
 
+    /**
+     * 修改事件状态(独立接口,不走编辑事件)。校验租户后仅允许 0待参与 → 1已申报。
+     */
+    void updateEventStatus(DrEventStatusRequest request);
+
     void deleteEvent(Long id);
 
     Object assessCapability(Long eventId);

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java

@@ -19,7 +19,7 @@ public interface VppDrSubsidyPredictionService {
 
     /**
      * 试算补贴预测(不落库)。
-     * <p>入参为邀约编号 + 电网实际响应容量;时长/平台响应量/申报出清量/补贴标准等从邀约与事件查询后计算。</p>
+     * <p>入参为邀约编号 + 电网实际响应容量;补贴金额按统一计算器口径计算。</p>
      */
     DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request);
 

+ 187 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSubsidyPredictionCalculator.java

@@ -0,0 +1,187 @@
+package com.usky.vpp.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.constant.VppDrEventType;
+import com.usky.vpp.domain.VppBiddingConfig;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
+import com.usky.vpp.mapper.VppBiddingConfigMapper;
+import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
+import com.usky.vpp.service.vo.SubsidyPredictionCalcResult;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppResponseCoefficientHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 补贴金额预测统一计算器。
+ * <p>完成率/响应量系数 = 实际出清 / 预估响应容量(与偏差配置同一量纲,如 [0.6,0.8)),
+ * 命中区间后取价格调整系数。补贴金额 = 预估响应容量 × 响应时长(h)
+ * × 下浮系数对应竞价价格(削峰=最高价,填谷=最低价)× 价格调整系数。</p>
+ */
+@Component
+public class VppSubsidyPredictionCalculator {
+
+    private static final int SCALE = 4;
+    private static final int RATIO_SCALE = 6;
+    private static final BigDecimal MINUTES_PER_HOUR = BigDecimal.valueOf(60);
+    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+    @Autowired
+    private VppResponseDeviationPriceMapper priceAdjustmentMapper;
+    @Autowired
+    private VppBiddingConfigMapper biddingConfigMapper;
+
+    public List<VppResponseDeviationPrice> loadAdjustments(Integer tenantId) {
+        return priceAdjustmentMapper.selectList(new LambdaQueryWrapper<VppResponseDeviationPrice>()
+                .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppResponseDeviationPrice::getTenantId, tenantId)
+                .orderByAsc(VppResponseDeviationPrice::getId));
+    }
+
+    public List<VppBiddingConfig> loadBiddingConfigs(Integer tenantId) {
+        return biddingConfigMapper.selectList(new LambdaQueryWrapper<VppBiddingConfig>()
+                .eq(VppBiddingConfig::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppBiddingConfig::getTenantId, tenantId));
+    }
+
+    public SubsidyPredictionCalcResult calculate(VppDrInvitation invitation, VppDrEvent event) {
+        Integer tenantId = resolveTenantId(invitation, event);
+        return calculate(invitation, event, loadAdjustments(tenantId), loadBiddingConfigs(tenantId));
+    }
+
+    /**
+     * 按邀约 + 事件计算补贴预测。边界情况(预估为空/≤0、出清为空、区间未命中、竞价未唯一匹配等)金额记 0,不抛业务异常。
+     */
+    public SubsidyPredictionCalcResult calculate(VppDrInvitation invitation, VppDrEvent event,
+                                                 List<VppResponseDeviationPrice> adjustments,
+                                                 List<VppBiddingConfig> biddingConfigs) {
+        SubsidyPredictionCalcResult result = new SubsidyPredictionCalcResult();
+        LocalDateTime start = event == null ? null : event.getStartTime();
+        LocalDateTime end = event == null ? null : event.getEndTime();
+        int durationMin = resolveDurationMin(start, end);
+        result.setResponseDurationMin(durationMin);
+
+        BigDecimal estimatedKw = invitation == null ? null : invitation.getEstimatedResponseCapacityKw();
+        BigDecimal clearedKw = invitation == null ? null : invitation.getActualClearedCapacityKw();
+        // 预估为空或 ≤0 时无法计算完成率(分母),金额记 0
+        if (estimatedKw == null || estimatedKw.compareTo(BigDecimal.ZERO) <= 0) {
+            return result;
+        }
+        if (clearedKw == null) {
+            return result;
+        }
+
+        BigDecimal estimated = estimatedKw;
+        // 响应量系数/完成率口径:实际出清 ÷ 预估响应容量(如 60/100 = 0.6)
+        BigDecimal coefficient = clearedKw.divide(estimated, RATIO_SCALE, RoundingMode.HALF_UP);
+        result.setResponseCoefficient(coefficient);
+        result.setCompletionRate(coefficient.multiply(HUNDRED).setScale(SCALE, RoundingMode.HALF_UP));
+        result.setDeviationRate(BigDecimal.ONE.subtract(coefficient)
+                .multiply(HUNDRED).setScale(SCALE, RoundingMode.HALF_UP));
+
+        BigDecimal priceAdjustment = matchPriceAdjustment(coefficient, adjustments);
+        result.setPriceAdjustmentCoefficient(priceAdjustment);
+        if (priceAdjustment == null) {
+            return result;
+        }
+        if (priceAdjustment.compareTo(BigDecimal.ZERO) == 0) {
+            result.setEstimatedSubsidyAmount(BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP));
+            return result;
+        }
+
+        BigDecimal subsidyPrice = resolveSubsidyPrice(event, invitation, biddingConfigs);
+        result.setSubsidyPrice(subsidyPrice);
+        if (subsidyPrice == null || durationMin <= 0) {
+            return result;
+        }
+
+        BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, RATIO_SCALE, RoundingMode.HALF_UP);
+        // 金额 = 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数(不得用完成率替代价格调整系数)
+        result.setEstimatedSubsidyAmount(estimated
+                .multiply(hours)
+                .multiply(subsidyPrice)
+                .multiply(priceAdjustment)
+                .setScale(SCALE, RoundingMode.HALF_UP));
+        return result;
+    }
+
+    private static int resolveDurationMin(LocalDateTime start, LocalDateTime end) {
+        if (start == null || end == null || !end.isAfter(start)) {
+            return 0;
+        }
+        return (int) Duration.between(start, end).toMinutes();
+    }
+
+    private static BigDecimal matchPriceAdjustment(BigDecimal coefficient,
+                                                   List<VppResponseDeviationPrice> adjustments) {
+        if (coefficient == null || adjustments == null) {
+            return null;
+        }
+        for (VppResponseDeviationPrice adjustment : adjustments) {
+            if (VppResponseCoefficientHelper.matches(
+                    adjustment.getCoeffLower(),
+                    adjustment.getCoeffUpper(),
+                    adjustment.getLowerInclusive(),
+                    adjustment.getUpperInclusive(),
+                    coefficient)) {
+                return adjustment.getPriceAdjustmentCoefficient();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 用事件下浮系数匹配唯一竞价配置,再按削峰/填谷取最高价/最低价作为补贴标准。
+     */
+    private static BigDecimal resolveSubsidyPrice(VppDrEvent event, VppDrInvitation invitation,
+                                                  List<VppBiddingConfig> biddingConfigs) {
+        if (event == null || event.getFloatingCoefficient() == null || biddingConfigs == null) {
+            return null;
+        }
+        VppBiddingConfig matched = null;
+        for (VppBiddingConfig config : biddingConfigs) {
+            if (config.getTypicalCoefficient() == null) {
+                continue;
+            }
+            if (config.getTypicalCoefficient().compareTo(event.getFloatingCoefficient()) != 0) {
+                continue;
+            }
+            if (matched != null) {
+                return null;
+            }
+            matched = config;
+        }
+        if (matched == null) {
+            return null;
+        }
+        Integer eventType = event.getEventType();
+        if (eventType == null && invitation != null) {
+            eventType = invitation.getTransactionType();
+        }
+        if (eventType == null) {
+            return null;
+        }
+        if (eventType == VppDrEventType.PEAK) {
+            return matched.getMaxPrice();
+        }
+        if (eventType == VppDrEventType.VALLEY) {
+            return matched.getMinPrice();
+        }
+        return null;
+    }
+
+    private static Integer resolveTenantId(VppDrInvitation invitation, VppDrEvent event) {
+        if (event != null && event.getTenantId() != null) {
+            return event.getTenantId();
+        }
+        return invitation == null ? null : invitation.getTenantId();
+    }
+}

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java

@@ -2,6 +2,7 @@ package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.domain.VppDevice;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppResourcePoint;
@@ -42,7 +43,7 @@ import java.util.stream.Collectors;
 @Service
 public class VppBaselineServiceImpl implements VppBaselineService {
 
-    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
     private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
     @Autowired

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java

@@ -2,6 +2,7 @@ package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.constant.VppSocialBenefitConstants;
 import com.usky.vpp.domain.DmpDeviceStatus;
 import com.usky.vpp.domain.VppCustomer;
@@ -61,7 +62,7 @@ import java.util.stream.Collectors;
 @Service
 public class VppDashboardServiceImpl implements VppDashboardService {
 
-    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
     private static final int EVENT_TYPE_PEAK = 1;
     private static final int EVENT_TYPE_VALLEY = 2;
     private static final int REPLY_ACCEPT = 1;

+ 6 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java

@@ -3,6 +3,7 @@ package com.usky.vpp.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.enums.VppUnEventPhase;
 import com.usky.vpp.mapper.VppDrEventMapper;
@@ -29,11 +30,11 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
 
     private static final Logger log = LoggerFactory.getLogger(VppDrEventIngestServiceImpl.class);
 
-    private static final int EVENT_STATUS_PENDING = 0;
-    private static final int EVENT_STATUS_DECLARED = 1;
-    private static final int EVENT_STATUS_EXECUTING = 2;
-    private static final int EVENT_STATUS_ENDED = 3;
-    private static final int EVENT_STATUS_CANCELLED = 4;
+    private static final int EVENT_STATUS_PENDING = VppDrEventStatus.PENDING;
+    private static final int EVENT_STATUS_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
+    private static final int EVENT_STATUS_CANCELLED = VppDrEventStatus.CANCELLED;
 
     @Autowired
     private VppDrEventMapper eventMapper;

+ 49 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.vpp.config.VppSmsProperties;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.domain.VppDrEvent;
@@ -56,9 +57,10 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     private static final int REPLY_REJECT = 2;
     private static final int REPLY_TIMEOUT = 3;
 
-    private static final int RESPONSE_PENDING = 0;
-    private static final int RESPONSE_DECLARED = 1;
-    private static final int RESPONSE_CANCELLED = 4;
+    /** 与事件状态一致:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
+    private static final int RESPONSE_PENDING = VppDrEventStatus.PENDING;
+    private static final int RESPONSE_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int RESPONSE_CANCELLED = VppDrEventStatus.CANCELLED;
 
     private static final int SMS_NOT_SENT = 0;
     private static final int SMS_SENT = 1;
@@ -208,7 +210,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
             throw new BusinessException("已参与的邀约不可删除");
         }
         if (invitation.getResponseStatus() != RESPONSE_PENDING) {
-            throw new BusinessException("仅待响应状态的邀约可删除");
+            throw new BusinessException("仅待参与状态的邀约可删除");
         }
         if (invitation.getParticipationId() != null) {
             throw new BusinessException("邀约已关联参与记录,不可删除");
@@ -311,6 +313,19 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (params.get("drEventId") != null) {
             wrapper.eq(VppDrInvitation::getDrEventId, Long.parseLong(params.get("drEventId").toString()));
         }
+        if (params.get("eventId") != null && StringUtils.hasText(params.get("eventId").toString())) {
+            List<Long> eventPks = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
+                            .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                            .like(VppDrEvent::getEventId, params.get("eventId").toString().trim()))
+                    .stream()
+                    .map(VppDrEvent::getId)
+                    .collect(Collectors.toList());
+            if (eventPks.isEmpty()) {
+                wrapper.eq(VppDrInvitation::getDrEventId, -1L);
+            } else {
+                wrapper.in(VppDrInvitation::getDrEventId, eventPks);
+            }
+        }
         if (params.get("responseType") != null) {
             wrapper.eq(VppDrInvitation::getResponseType, Integer.parseInt(params.get("responseType").toString()));
         }
@@ -489,13 +504,27 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (CollectionUtils.isEmpty(invitations)) {
             return Collections.emptyList();
         }
-        Set<Long> customerIds = invitations.stream().map(VppDrInvitation::getCustomerId).collect(Collectors.toSet());
-
-        Map<Long, VppCustomer> customerMap = customerMapper.selectBatchIds(customerIds).stream()
+        Set<Long> customerIds = invitations.stream()
+                .map(VppDrInvitation::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, VppCustomer> customerMap = customerIds.isEmpty()
+                ? Collections.emptyMap()
+                : customerMapper.selectBatchIds(customerIds).stream()
                 .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
 
+        Set<Long> eventPks = invitations.stream()
+                .map(VppDrInvitation::getDrEventId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, VppDrEvent> eventMap = eventPks.isEmpty()
+                ? Collections.emptyMap()
+                : eventMapper.selectBatchIds(eventPks).stream()
+                .filter(e -> !VppAuditHelper.isDeleted(e.getDeleteFlag()))
+                .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
+
         return invitations.stream()
-                .map(inv -> toVo(inv, customerMap.get(inv.getCustomerId()), null))
+                .map(inv -> toVo(inv, customerMap.get(inv.getCustomerId()), null, eventMap.get(inv.getDrEventId())))
                 .collect(Collectors.toList());
     }
 
@@ -504,10 +533,17 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         VppCustomerContact contact = invitation.getSmsContactId() != null
                 ? contactMapper.selectById(invitation.getSmsContactId())
                 : null;
-        return toVo(invitation, customer, contact);
+        VppDrEvent event = invitation.getDrEventId() != null
+                ? eventMapper.selectById(invitation.getDrEventId())
+                : null;
+        if (event != null && VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+            event = null;
+        }
+        return toVo(invitation, customer, contact, event);
     }
 
-    private DrInvitationVO toVo(VppDrInvitation invitation, VppCustomer customer, VppCustomerContact contact) {
+    private DrInvitationVO toVo(VppDrInvitation invitation, VppCustomer customer, VppCustomerContact contact,
+                                VppDrEvent event) {
         DrInvitationVO vo = new DrInvitationVO();
         BeanUtils.copyProperties(invitation, vo);
         if (customer != null) {
@@ -517,6 +553,9 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
             vo.setSmsContactName(contact.getContactName());
             vo.setSmsContactPhone(contact.getContactPhone());
         }
+        if (event != null) {
+            vo.setEventId(event.getEventId());
+        }
         return vo;
     }
 

+ 255 - 289
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -5,26 +5,32 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppDrEventStatus;
+import com.usky.vpp.domain.VppBiddingConfig;
+import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
-import com.usky.vpp.domain.VppDrParticipation;
-import com.usky.vpp.domain.VppDrSubsidyPrediction;
 import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrInvitationMapper;
-import com.usky.vpp.mapper.VppDrParticipationMapper;
-import com.usky.vpp.mapper.VppDrSubsidyPredictionMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppBaselineService;
 import com.usky.vpp.service.VppDrMonitorService;
+import com.usky.vpp.service.VppSubsidyPredictionCalculator;
 import com.usky.vpp.service.vo.BaselinePointVO;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
+import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
 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.service.vo.SubsidyPredictionCalcResult;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppResourceTypeHelper;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -41,8 +47,7 @@ import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -51,15 +56,15 @@ import java.util.stream.Collectors;
 
 /**
  * 需求响应监测(响应记录)
- * <p>统一口径:站点 → is_support_peak=1 资源点 → 按资源点平均。</p>
+ * <p>统一口径:时间区间内执行中/已结束事件 → 参与且执行中/已结束的未删除邀约。</p>
  */
 @Service
 public class VppDrMonitorServiceImpl implements VppDrMonitorService {
 
-    private static final int EVENT_STATUS_EXECUTING = 2;
-    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
+    private static final int REPLY_ACCEPT = 1;
     private static final int SUPPORT_PEAK = 1;
-    private static final int PARTICIPATE_ACCEPT = 1;
     private static final int SCALE = 4;
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -71,146 +76,77 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     @Autowired
     private VppDrInvitationMapper invitationMapper;
     @Autowired
-    private VppDrParticipationMapper participationMapper;
+    private VppCustomerMapper customerMapper;
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
     private VppBaselineService baselineService;
     @Autowired
-    private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
+    private VppSubsidyPredictionCalculator subsidyPredictionCalculator;
 
     @Override
     public DrMonitorSummaryVO getSummary(String startDate, String endDate, List<Long> siteIds) {
-        Integer tenantId = SecurityUtils.getTenantId();
-        LocalDateTime rangeStart = parseStart(startDate);
-        LocalDateTime rangeEnd = parseEnd(endDate);
         requireSiteIds(siteIds);
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppDrInvitation> invitations = listMonitorInvitations(
+                parseStart(startDate), parseEnd(endDate), siteIds, tenantId);
 
-        List<VppResourcePoint> peakResources = listPeakResources(siteIds, tenantId);
         DrMonitorSummaryVO vo = new DrMonitorSummaryVO();
-        vo.setPeakResourceCount((long) peakResources.size());
-        if (peakResources.isEmpty()) {
+        if (invitations.isEmpty()) {
             return vo;
         }
-
-        Set<Long> resourceIds = peakResources.stream().map(VppResourcePoint::getId).collect(Collectors.toSet());
-        Map<Long, Long> resourceSiteMap = peakResources.stream()
-                .collect(Collectors.toMap(VppResourcePoint::getId, VppResourcePoint::getSiteId, (a, b) -> a));
-
-        List<VppDrParticipation> participations = listParticipations(resourceIds, tenantId);
-        Map<Long, VppDrEvent> eventMap = loadEventMap(participations, tenantId);
-        List<VppDrEvent> rangedEvents = filterEventsByTime(eventMap.values(), rangeStart, rangeEnd);
-
-        Set<Long> countEventIds = rangedEvents.stream()
-                .filter(e -> e.getEventStatus() != null
-                        && (e.getEventStatus() == EVENT_STATUS_EXECUTING || e.getEventStatus() == EVENT_STATUS_ENDED))
-                .map(VppDrEvent::getId)
-                .collect(Collectors.toSet());
-        Set<Long> endedEventIds = rangedEvents.stream()
-                .filter(e -> Objects.equals(e.getEventStatus(), EVENT_STATUS_ENDED))
-                .map(VppDrEvent::getId)
-                .collect(Collectors.toSet());
-
-        // 每个资源点:参与的执行中/已结束事件数
-        Map<Long, Set<Long>> resourceCountEvents = new HashMap<>();
-        // 每个资源点:参与的已结束事件
-        Map<Long, Set<Long>> resourceEndedEvents = new HashMap<>();
-        for (VppDrParticipation p : participations) {
-            if (p.getResourceId() == null || p.getEventId() == null) {
-                continue;
-            }
-            if (countEventIds.contains(p.getEventId())) {
-                resourceCountEvents.computeIfAbsent(p.getResourceId(), k -> new HashSet<>()).add(p.getEventId());
-            }
-            if (endedEventIds.contains(p.getEventId())) {
-                resourceEndedEvents.computeIfAbsent(p.getResourceId(), k -> new HashSet<>()).add(p.getEventId());
-            }
-        }
-
-        Long countSum = 0L;
-        for (VppResourcePoint resource : peakResources) {
-            countSum += resourceCountEvents.getOrDefault(resource.getId(), Collections.emptySet()).size();
-        }
-        vo.setResponseCount(countSum);
-
-        // 邀约按站点:容量与达标
-        Set<Long> peakSiteIds = peakResources.stream().map(VppResourcePoint::getSiteId).collect(Collectors.toSet());
-        List<VppDrInvitation> endedInvitations = listInvitations(endedEventIds, peakSiteIds, tenantId);
-        Map<Long, List<VppDrInvitation>> invitationsBySite = endedInvitations.stream()
-                .filter(i -> i.getSiteId() != null)
-                .collect(Collectors.groupingBy(VppDrInvitation::getSiteId));
-
-        BigDecimal capacitySum = BigDecimal.ZERO;
-        BigDecimal rateSum = BigDecimal.ZERO;
-        int rateResourceCount = 0;
-        for (VppResourcePoint resource : peakResources) {
-            Long siteId = resourceSiteMap.get(resource.getId());
-            Set<Long> endedForResource = resourceEndedEvents.getOrDefault(resource.getId(), Collections.emptySet());
-            List<VppDrInvitation> siteInvitations = invitationsBySite.getOrDefault(siteId, Collections.emptyList())
-                    .stream()
-                    .filter(i -> endedForResource.contains(i.getDrEventId()))
-                    .collect(Collectors.toList());
-
-            BigDecimal resourceCapacity = BigDecimal.ZERO;
-            long qualified = 0;
-            for (VppDrInvitation invitation : siteInvitations) {
-                resourceCapacity = resourceCapacity.add(nz(invitation.getActualResponseCapacityKw()));
-                if (isCompletionQualified(invitation.getResponseCompletionRate())) {
-                    qualified++;
-                }
-            }
-            capacitySum = capacitySum.add(resourceCapacity);
-            if (!siteInvitations.isEmpty()) {
-                rateSum = rateSum.add(BigDecimal.valueOf(qualified)
-                        .divide(BigDecimal.valueOf(siteInvitations.size()), 8, RoundingMode.HALF_UP));
-                rateResourceCount++;
+        int total = invitations.size();
+        BigDecimal capacity = BigDecimal.ZERO;
+        long qualified = 0L;
+        for (VppDrInvitation invitation : invitations) {
+            capacity = capacity.add(nz(invitation.getActualResponseCapacityKw()));
+            if (isCompletionQualified(invitation.getResponseCompletionRate())) {
+                qualified++;
             }
         }
-        vo.setResponseCapacityKw(avg(capacitySum, peakResources.size()));
-        if (rateResourceCount > 0) {
-            vo.setHistoricalQualifiedRate(avg(rateSum, rateResourceCount));
-        }
+        vo.setResponseCount((long) total);
+        vo.setResponseCapacityKw(capacity.setScale(SCALE, RoundingMode.HALF_UP));
+        vo.setHistoricalQualifiedRate(BigDecimal.valueOf(qualified)
+                .divide(BigDecimal.valueOf(total), SCALE, RoundingMode.HALF_UP));
         return vo;
     }
 
     @Override
     public CommonPage<DrMonitorRecordVO> pageRecords(String startDate, String endDate,
                                                      List<Long> siteIds, Map<String, Object> params) {
-        Integer tenantId = SecurityUtils.getTenantId();
-        LocalDateTime rangeStart = parseStart(startDate);
-        LocalDateTime rangeEnd = parseEnd(endDate);
         requireSiteIds(siteIds);
-
-        List<VppResourcePoint> peakResources = listPeakResources(siteIds, tenantId);
-        if (peakResources.isEmpty()) {
-            Page<?> page = VppPageHelper.of(params);
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppDrInvitation> invitations = listMonitorInvitations(
+                parseStart(startDate), parseEnd(endDate), siteIds, tenantId);
+        Page<?> page = VppPageHelper.of(params);
+        if (invitations.isEmpty()) {
             return new CommonPage<>(Collections.emptyList(), 0L, page.getCurrent(), page.getSize());
         }
 
-        Set<Long> resourceIds = peakResources.stream().map(VppResourcePoint::getId).collect(Collectors.toSet());
-        Set<Long> peakSiteIds = peakResources.stream().map(VppResourcePoint::getSiteId).collect(Collectors.toSet());
-        List<VppDrParticipation> participations = listParticipations(resourceIds, tenantId);
-        Map<Long, VppDrEvent> eventMap = loadEventMap(participations, tenantId);
-
-        List<VppDrEvent> events = filterEventsByTime(eventMap.values(), rangeStart, rangeEnd).stream()
-                .filter(e -> e.getEventStatus() != null
-                        && (e.getEventStatus() == EVENT_STATUS_EXECUTING || e.getEventStatus() == EVENT_STATUS_ENDED))
-                .sorted(Comparator.comparing(VppDrEvent::getStartTime, Comparator.nullsLast(Comparator.reverseOrder())))
-                .collect(Collectors.toList());
-
-        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
-        List<VppDrInvitation> invitations = listInvitations(eventIds, peakSiteIds, tenantId);
-        Map<Long, List<VppDrInvitation>> byEvent = invitations.stream()
-                .filter(i -> i.getDrEventId() != null)
-                .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));
-        Map<Long, BigDecimal> subsidyByEvent = loadSubsidyAmountByEvent(eventIds, peakSiteIds, tenantId);
-
-        List<DrMonitorRecordVO> records = events.stream()
-                .map(e -> toRecordVo(e, byEvent.getOrDefault(e.getId(), Collections.emptyList()),
-                        subsidyByEvent.get(e.getId())))
+        Set<Long> eventIds = invitations.stream()
+                .map(VppDrInvitation::getDrEventId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, VppDrEvent> eventMap = loadEventMapByIds(eventIds, tenantId);
+        Map<Long, VppCustomer> customerMap = loadCustomerMap(invitations);
+        Map<Long, VppSite> siteMap = loadSiteMap(invitations, tenantId);
+        List<VppResponseDeviationPrice> adjustments =
+                subsidyPredictionCalculator.loadAdjustments(tenantId);
+        List<VppBiddingConfig> biddingConfigs =
+                subsidyPredictionCalculator.loadBiddingConfigs(tenantId);
+
+        List<DrMonitorRecordVO> records = invitations.stream()
+                .sorted(Comparator.comparing(
+                        i -> eventMap.get(i.getDrEventId()) == null
+                                ? null : eventMap.get(i.getDrEventId()).getStartTime(),
+                        Comparator.nullsLast(Comparator.reverseOrder())))
+                .map(inv -> toRecordVo(inv, eventMap.get(inv.getDrEventId()),
+                        customerMap.get(inv.getCustomerId()), siteMap.get(inv.getSiteId()),
+                        adjustments, biddingConfigs))
                 .collect(Collectors.toList());
 
-        Page<?> page = VppPageHelper.of(params);
         long current = page.getCurrent();
         long size = page.getSize();
         int from = (int) Math.min((current - 1) * size, records.size());
@@ -222,119 +158,139 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     }
 
     @Override
-    public DrMonitorCurveVO getCurve(Long siteId, Integer baselineDays) {
-        if (siteId == null) {
-            throw new BusinessException("站点ID不能为空");
-        }
-        if (baselineDays != null && baselineDays != 3 && baselineDays != 5) {
-            throw new BusinessException("基线负荷仅支持3天或5天维度查询!");
+    public DrMonitorCurveVO getCurve(Long invitationId) {
+        if (invitationId == null) {
+            throw new BusinessException("邀约ID不能为空");
         }
         Integer tenantId = SecurityUtils.getTenantId();
-        List<VppResourcePoint> peakResources = listPeakResources(siteId, tenantId);
-        if (peakResources.isEmpty()) {
-            throw new BusinessException("所选站点下无可参与调峰的资源点!");
+        VppDrInvitation invitation = invitationMapper.selectById(invitationId);
+        if (invitation == null || VppAuditHelper.isDeleted(invitation.getDeleteFlag())) {
+            throw new BusinessException("邀约记录不存在");
+        }
+        if (tenantId != null && invitation.getTenantId() != null && !tenantId.equals(invitation.getTenantId())) {
+            throw new BusinessException("无权操作其他租户的邀约");
+        }
+        if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
+            throw new BusinessException("邀约未关联站点");
         }
-        int resourceCount = peakResources.size();
-
-        LocalDate today = LocalDate.now();
-        LocalDateTime windowStart = today.atStartOfDay();
-        LocalDateTime windowEnd = today.atTime(LocalTime.MAX);
-        String responseStartTime = windowStart.format(DATE_TIME_FMT);
-        String responseEndTime = windowEnd.format(DATE_TIME_FMT);
-        String actualEndTime = windowEnd.format(DATE_TIME_FMT);
-        int requiredDays = (baselineDays != null && baselineDays == 3) ? 3 : 5;
-
-        BigDecimal actualAvg = avg(resolveSiteActualResponseKw(
-                siteId, responseStartTime, responseEndTime, BigDecimal.ZERO), resourceCount);
 
-        SiteBaselineVO baseline = baselineService.getSiteBaseline(
-                siteId, responseStartTime, requiredDays, actualEndTime);
+        VppDrEvent event = invitation.getDrEventId() != null
+                ? eventMapper.selectById(invitation.getDrEventId()) : null;
+        if (event != null && VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+            event = null;
+        }
+        String responseStartTime = resolveInvitationResponseStartTime(invitation, event);
+        SiteBaselineVO baseline = baselineService.getSiteBaseline(invitation.getSiteId(), responseStartTime);
+        BigDecimal actualKw = nz(invitation.getActualResponseCapacityKw()).setScale(SCALE, RoundingMode.HALF_UP);
 
         DrMonitorCurveVO curve = new DrMonitorCurveVO();
-        curve.setResponseDate(today.format(DATE_FMT));
-        curve.setSiteId(siteId);
-        curve.setActualResponseCapacityKw(actualAvg);
+        curve.setId(invitation.getId());
+        curve.setSiteId(invitation.getSiteId());
+        curve.setActualResponseCapacityKw(actualKw);
+        if (invitation.getExecuteStartDate() != null) {
+            curve.setResponseDate(invitation.getExecuteStartDate().format(DATE_FMT));
+        } else if (event != null && event.getStartTime() != null) {
+            curve.setResponseDate(event.getStartTime().toLocalDate().format(DATE_FMT));
+        }
+        if (event != null) {
+            curve.setEventId(event.getEventId());
+        }
         if (baseline != null && StringUtils.hasText(baseline.getDataSource())) {
             curve.setDataSource(baseline.getDataSource());
         }
-        // if (dataSources.isEmpty()) {
-        //     curve.setDataSource("mock");
-        // }
-
         if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
             return curve;
         }
         for (BaselinePointVO point : baseline.getPoints()) {
-            if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
+            if (!StringUtils.hasText(point.getTime())) {
                 continue;
             }
             DrMonitorCurveVO.Point curvePoint = new DrMonitorCurveVO.Point();
             curvePoint.setTime(point.getTime());
             curvePoint.setBaselineKw(nz(point.getPredictedBaselineKw()).setScale(SCALE, RoundingMode.HALF_UP));
-            curvePoint.setActualResponseCapacityKw(actualAvg);
+            curvePoint.setActualResponseCapacityKw(actualKw);
             curve.getPoints().add(curvePoint);
         }
         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);
+    @Override
+    public List<DrMonitorSiteResourceVO> listEventSitePeakResources(Long eventId) {
+        if (eventId == null) {
+            throw new BusinessException("事件ID不能为空");
         }
-        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) {
-            // 回退邀约实际响应容量,保持曲线可返回
+        Integer tenantId = SecurityUtils.getTenantId();
+        VppDrEvent event = eventMapper.selectById(eventId);
+        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+            throw new BusinessException("需求响应事件不存在");
         }
-        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));
-            LocalDate date = windowStart.toLocalDate();
-            LocalDateTime point = LocalDateTime.of(date, t);
-            // 跨日窗口兜底:若点早于 windowStart 且 windowEnd 跨日,放到次日
-            if (point.isBefore(windowStart) && windowEnd.toLocalDate().isAfter(date)) {
-                point = point.plusDays(1);
-            }
-            return !point.isBefore(windowStart) && !point.isAfter(windowEnd);
-        } catch (Exception ex) {
-            return true;
+        if (tenantId != null && event.getTenantId() != null && !tenantId.equals(event.getTenantId())) {
+            throw new BusinessException("无权操作其他租户的事件");
         }
-    }
 
-    private Map<Long, BigDecimal> loadSubsidyAmountByEvent(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
-        if (CollectionUtils.isEmpty(eventIds)) {
-            return Collections.emptyMap();
-        }
-        LambdaQueryWrapper<VppDrSubsidyPrediction> wrapper = new LambdaQueryWrapper<>();
-        wrapper.in(VppDrSubsidyPrediction::getDrEventId, eventIds)
-                .eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId);
-        if (!CollectionUtils.isEmpty(siteIds)) {
-            wrapper.in(VppDrSubsidyPrediction::getSiteId, siteIds);
+        List<VppDrInvitation> invitations = invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDrEventId, eventId)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
+        Set<Long> siteIds = invitations.stream()
+                .map(VppDrInvitation::getSiteId)
+                .filter(id -> id != null && id > 0)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        if (siteIds.isEmpty()) {
+            return Collections.emptyList();
         }
-        List<VppDrSubsidyPrediction> list = subsidyPredictionMapper.selectList(wrapper);
-        Map<Long, BigDecimal> result = new HashMap<>();
-        for (VppDrSubsidyPrediction item : list) {
-            if (item.getDrEventId() == null || item.getEstimatedSubsidyAmount() == null) {
+
+        Map<Long, VppSite> siteMap = siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                        .in(VppSite::getId, siteIds)
+                        .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppSite::getTenantId, tenantId))
+                .stream()
+                .collect(Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));
+
+        List<VppResourcePoint> peakResources = listPeakResources(new ArrayList<>(siteIds), tenantId);
+        Map<Long, List<VppResourcePoint>> resourcesBySite = peakResources.stream()
+                .filter(r -> r.getSiteId() != null)
+                .collect(Collectors.groupingBy(VppResourcePoint::getSiteId));
+
+        List<DrMonitorSiteResourceVO> result = new ArrayList<>();
+        for (Long siteId : siteIds) {
+            VppSite site = siteMap.get(siteId);
+            if (site == null) {
                 continue;
             }
-            result.merge(item.getDrEventId(), item.getEstimatedSubsidyAmount(), BigDecimal::add);
+            DrMonitorSiteResourceVO vo = new DrMonitorSiteResourceVO();
+            vo.setSiteId(site.getId());
+            vo.setSiteCode(site.getSiteCode());
+            vo.setSiteName(site.getSiteName());
+            List<VppResourcePoint> siteResources = resourcesBySite.getOrDefault(siteId, Collections.emptyList())
+                    .stream()
+                    .sorted(Comparator.comparing(VppResourcePoint::getId, Comparator.nullsLast(Long::compareTo)))
+                    .collect(Collectors.toList());
+            for (VppResourcePoint resource : siteResources) {
+                vo.getResources().add(toResourceItem(resource));
+            }
+            result.add(vo);
         }
         return result;
     }
 
+    private static DrMonitorSiteResourceVO.ResourceItem toResourceItem(VppResourcePoint resource) {
+        DrMonitorSiteResourceVO.ResourceItem item = new DrMonitorSiteResourceVO.ResourceItem();
+        item.setResourceId(resource.getId());
+        item.setResourceCode(resource.getResourceCode());
+        item.setResourceName(resource.getResourceName());
+        item.setResourceType(resource.getResourceType());
+        item.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(resource.getResourceType()));
+        item.setCapacityKw(resource.getCapacityKw());
+        item.setIsControl(resource.getIsControl());
+        item.setIsSupportPeak(resource.getIsSupportPeak());
+        item.setMaxUpKw(resource.getMaxUpKw());
+        item.setMinDownKw(resource.getMinDownKw());
+        item.setIsSupportFm(resource.getIsSupportFm());
+        item.setRemark(resource.getRemark());
+        return item;
+    }
+
     // ---------- loaders ----------
 
     private void requireSiteIds(List<Long> siteIds) {
@@ -343,40 +299,34 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         }
     }
 
-    private List<VppResourcePoint> listPeakResources(Long siteId, Integer tenantId) {
-        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
-                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .eq(VppResourcePoint::getIsSupportPeak, SUPPORT_PEAK)
-                .eq(VppResourcePoint::getSiteId, siteId)
-                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
-    }
-
-    private List<VppResourcePoint> listPeakResources(List<Long> siteIds, Integer tenantId) {
-        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
-                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .eq(VppResourcePoint::getIsSupportPeak, SUPPORT_PEAK)
-                .in(VppResourcePoint::getSiteId, siteIds)
-                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId)
-                .isNotNull(VppResourcePoint::getSiteId));
-    }
-
-    private List<VppDrParticipation> listParticipations(Set<Long> resourceIds, Integer tenantId) {
-        if (CollectionUtils.isEmpty(resourceIds)) {
+    /**
+     * 时间区间内事件开始、结束均落在区间,且状态为执行中/已结束;再关联参与且执行中/已结束的未删除邀约。
+     */
+    private List<VppDrInvitation> listMonitorInvitations(LocalDateTime rangeStart, LocalDateTime rangeEnd,
+                                                         List<Long> siteIds, Integer tenantId) {
+        List<VppDrEvent> events = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
+                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .in(VppDrEvent::getEventStatus, EVENT_STATUS_EXECUTING, EVENT_STATUS_ENDED)
+                .ge(VppDrEvent::getStartTime, rangeStart)
+                .le(VppDrEvent::getEndTime, rangeEnd)
+                .isNotNull(VppDrEvent::getStartTime)
+                .isNotNull(VppDrEvent::getEndTime)
+                .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
+        if (events.isEmpty()) {
             return Collections.emptyList();
         }
-        return participationMapper.selectList(new LambdaQueryWrapper<VppDrParticipation>()
-                .in(VppDrParticipation::getResourceId, resourceIds)
-                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_ACCEPT)
-                .eq(tenantId != null, VppDrParticipation::getTenantId, tenantId));
+        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .in(VppDrInvitation::getDrEventId, eventIds)
+                .in(VppDrInvitation::getSiteId, siteIds)
+                .eq(VppDrInvitation::getReplyStatus, REPLY_ACCEPT)
+                .in(VppDrInvitation::getResponseStatus, EVENT_STATUS_EXECUTING, EVENT_STATUS_ENDED)
+                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
     }
 
-    private Map<Long, VppDrEvent> loadEventMap(List<VppDrParticipation> participations, Integer tenantId) {
-        Set<Long> eventIds = participations.stream()
-                .map(VppDrParticipation::getEventId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-        if (eventIds.isEmpty()) {
+    private Map<Long, VppDrEvent> loadEventMapByIds(Set<Long> eventIds, Integer tenantId) {
+        if (CollectionUtils.isEmpty(eventIds)) {
             return Collections.emptyMap();
         }
         return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
@@ -387,63 +337,84 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
                 .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
     }
 
-    private List<VppDrEvent> filterEventsByTime(Iterable<VppDrEvent> events,
-                                                LocalDateTime rangeStart, LocalDateTime rangeEnd) {
-        List<VppDrEvent> result = new ArrayList<>();
-        for (VppDrEvent event : events) {
-            if (event.getStartTime() == null) {
-                continue;
-            }
-            if (rangeStart != null && event.getStartTime().isBefore(rangeStart)) {
-                continue;
-            }
-            if (rangeEnd != null && event.getStartTime().isAfter(rangeEnd)) {
-                continue;
-            }
-            result.add(event);
+    private Map<Long, VppCustomer> loadCustomerMap(List<VppDrInvitation> invitations) {
+        Set<Long> customerIds = invitations.stream()
+                .map(VppDrInvitation::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (customerIds.isEmpty()) {
+            return Collections.emptyMap();
         }
-        return result;
+        return customerMapper.selectList(new LambdaQueryWrapper<VppCustomer>()
+                        .in(VppCustomer::getId, customerIds)
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED))
+                .stream()
+                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
     }
 
-    private List<VppDrInvitation> listInvitations(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
-        if (CollectionUtils.isEmpty(eventIds) || CollectionUtils.isEmpty(siteIds)) {
-            return Collections.emptyList();
+    private Map<Long, VppSite> loadSiteMap(List<VppDrInvitation> invitations, Integer tenantId) {
+        Set<Long> siteIds = invitations.stream()
+                .map(VppDrInvitation::getSiteId)
+                .filter(id -> id != null && id > 0)
+                .collect(Collectors.toSet());
+        if (siteIds.isEmpty()) {
+            return Collections.emptyMap();
         }
-        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
-                .in(VppDrInvitation::getDrEventId, eventIds)
-                .in(VppDrInvitation::getSiteId, siteIds)
-                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
+        return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                        .in(VppSite::getId, siteIds)
+                        .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppSite::getTenantId, tenantId))
+                .stream()
+                .collect(Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));
     }
 
-    private DrMonitorRecordVO toRecordVo(VppDrEvent event, List<VppDrInvitation> invitations,
-                                         BigDecimal estimatedSubsidyAmount) {
+    private DrMonitorRecordVO toRecordVo(VppDrInvitation invitation, VppDrEvent event,
+                                         VppCustomer customer, VppSite site,
+                                         List<VppResponseDeviationPrice> adjustments,
+                                         List<VppBiddingConfig> biddingConfigs) {
         DrMonitorRecordVO vo = new DrMonitorRecordVO();
-        BeanUtils.copyProperties(event, vo);
-        vo.setEstimatedSubsidyAmount(estimatedSubsidyAmount);
-        if (CollectionUtils.isEmpty(invitations)) {
-            vo.setResponseCompletionRate(null);
-            vo.setQualified(false);
-            return vo;
-        }
-        BigDecimal sum = BigDecimal.ZERO;
-        int rateCount = 0;
-        boolean allQualified = true;
-        for (VppDrInvitation invitation : invitations) {
-            BigDecimal rate = invitation.getResponseCompletionRate();
-            if (rate != null) {
-                sum = sum.add(rate);
-                rateCount++;
-            }
-            if (!isCompletionQualified(rate)) {
-                allQualified = false;
-            }
+        if (event != null) {
+            BeanUtils.copyProperties(event, vo);
+        }
+        vo.setInvitationId(invitation.getId());
+        vo.setCustomerId(invitation.getCustomerId());
+        if (customer != null) {
+            vo.setCustomerName(customer.getCustomerName());
+        }
+        vo.setSiteId(invitation.getSiteId());
+        if (site != null) {
+            vo.setSiteName(site.getSiteName());
+        }
+        vo.setEstimatedResponseCapacityKw(invitation.getEstimatedResponseCapacityKw());
+        vo.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
+        vo.setResponseCompletionRate(invitation.getResponseCompletionRate());
+        vo.setQualified(isCompletionQualified(invitation.getResponseCompletionRate()));
+        SubsidyPredictionCalcResult subsidy = subsidyPredictionCalculator.calculate(
+                invitation, event, adjustments, biddingConfigs);
+        vo.setEstimatedSubsidyAmount(subsidy.getEstimatedSubsidyAmount());
+        vo.setSubsidyPrice(subsidy.getSubsidyPrice());
+        return vo;
+    }
+
+    private String resolveInvitationResponseStartTime(VppDrInvitation invitation, VppDrEvent event) {
+        if (invitation.getExecuteStartDate() != null) {
+            LocalTime timeOfDay = (event != null && event.getStartTime() != null)
+                    ? event.getStartTime().toLocalTime() : LocalTime.MIN;
+            return LocalDateTime.of(invitation.getExecuteStartDate(), timeOfDay).format(DATE_TIME_FMT);
         }
-        if (rateCount > 0) {
-            vo.setResponseCompletionRate(sum.divide(BigDecimal.valueOf(rateCount), SCALE, RoundingMode.HALF_UP));
+        if (event != null && event.getStartTime() != null) {
+            return event.getStartTime().format(DATE_TIME_FMT);
         }
-        vo.setQualified(allQualified && rateCount == invitations.size());
-        return vo;
+        throw new BusinessException("邀约缺少响应时间");
+    }
+
+    private List<VppResourcePoint> listPeakResources(List<Long> siteIds, Integer tenantId) {
+        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppResourcePoint::getIsSupportPeak, SUPPORT_PEAK)
+                .in(VppResourcePoint::getSiteId, siteIds)
+                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId)
+                .isNotNull(VppResourcePoint::getSiteId));
     }
 
     static boolean isCompletionQualified(BigDecimal rate) {
@@ -451,15 +422,10 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
             return false;
         }
         BigDecimal scaled = rate.setScale(2, RoundingMode.HALF_UP);
-        return scaled.compareTo(ONE.setScale(2, RoundingMode.HALF_UP)) == 0
-                || scaled.compareTo(HUNDRED.setScale(2, RoundingMode.HALF_UP)) == 0;
-    }
-
-    private static BigDecimal avg(BigDecimal sum, int divisor) {
-        if (divisor <= 0) {
-            return BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+        if (scaled.compareTo(ONE) > 0) {
+            return scaled.compareTo(HUNDRED) >= 0;
         }
-        return nz(sum).divide(BigDecimal.valueOf(divisor), SCALE, RoundingMode.HALF_UP);
+        return scaled.compareTo(ONE) >= 0;
     }
 
     private static LocalDateTime parseStart(String date) {

+ 35 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvaluation;
 import com.usky.vpp.domain.VppDrEvent;
@@ -26,6 +28,7 @@ import com.usky.vpp.service.VppUnIntegrationService;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrEventStatusRequest;
 import com.usky.vpp.service.vo.DrParticipationVO;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
@@ -53,11 +56,11 @@ public class VppDrServiceImpl implements VppDrService {
 
     private static final Logger log = LoggerFactory.getLogger(VppDrServiceImpl.class);
 
-    private static final int EVENT_STATUS_PENDING = 0;
-    private static final int EVENT_STATUS_DECLARED = 1;
-    private static final int EVENT_STATUS_EXECUTING = 2;
-    private static final int EVENT_STATUS_ENDED = 3;
-    private static final int EVENT_STATUS_CANCELLED = 4;
+    private static final int EVENT_STATUS_PENDING = VppDrEventStatus.PENDING;
+    private static final int EVENT_STATUS_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
+    private static final int EVENT_STATUS_CANCELLED = VppDrEventStatus.CANCELLED;
 
     private static final int PARTICIPATE_STATUS_ACCEPT = 1;
 
@@ -194,6 +197,33 @@ public class VppDrServiceImpl implements VppDrService {
         eventMapper.updateById(event);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateEventStatus(DrEventStatusRequest request) {
+        if (request == null || request.getId() == null) {
+            throw new BusinessException("事件ID不能为空");
+        }
+        if (request.getEventStatus() == null) {
+            throw new BusinessException("事件状态不能为空");
+        }
+        if (request.getEventStatus() != EVENT_STATUS_DECLARED) {
+            throw new BusinessException("仅允许将事件从待参与改为已申报");
+        }
+
+        VppDrEvent event = requireEvent(request.getId());
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null && event.getTenantId() != null && !tenantId.equals(event.getTenantId())) {
+            throw new BusinessException("无权操作其他租户的事件");
+        }
+        if (event.getEventStatus() == null || event.getEventStatus() != EVENT_STATUS_PENDING) {
+            throw new BusinessException("仅待参与状态的事件可改为已申报");
+        }
+
+        event.setEventStatus(EVENT_STATUS_DECLARED);
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void deleteEvent(Long id) {

+ 30 - 99
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -5,23 +5,27 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppBiddingConfig;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrSubsidyPrediction;
 import com.usky.vpp.domain.VppResponseDeviationPrice;
 import com.usky.vpp.domain.VppSite;
-import com.usky.vpp.mapper.*;
 import com.usky.vpp.mapper.VppCustomerMapper;
-import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrSubsidyPredictionMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppBaselineService;
 import com.usky.vpp.service.VppDrSubsidyPredictionService;
+import com.usky.vpp.service.VppSubsidyPredictionCalculator;
 import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
+import com.usky.vpp.service.vo.SubsidyPredictionCalcResult;
 import com.usky.vpp.util.VppAuditHelper;
-import com.usky.vpp.util.VppResponseCoefficientHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -32,7 +36,6 @@ import org.springframework.util.StringUtils;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Collections;
@@ -49,7 +52,6 @@ import java.util.stream.Collectors;
 public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPredictionService {
 
     private static final Logger log = LoggerFactory.getLogger(VppDrSubsidyPredictionServiceImpl.class);
-    private static final BigDecimal MINUTES_PER_HOUR = BigDecimal.valueOf(60);
     private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
     @Autowired
@@ -63,9 +65,9 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     @Autowired
     private VppCustomerMapper customerMapper;
     @Autowired
-    private VppResponseDeviationPriceMapper priceAdjustmentMapper;
-    @Autowired
     private VppBaselineService vppBaselineService;
+    @Autowired
+    private VppSubsidyPredictionCalculator subsidyPredictionCalculator;
 
     @Override
     public CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
@@ -128,14 +130,11 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         fillCustomerName(entity);
         entity.setEventStartTime(event.getStartTime());
         entity.setEventEndTime(event.getEndTime());
-        entity.setSubsidyPrice(event.getSubsidyPrice());
 
         BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
                 ? invitation.getDeclaredCapacityKw()
                 : invitation.getActualClearedCapacityKw();
-        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
-                ? invitation.getActualClearedCapacityKw()
-                : declaredKw;
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw();
         BigDecimal platformKw = resolvePlatformActualResponseKw(
                 invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
                 invitation.getEstimatedResponseCapacityKw());
@@ -143,12 +142,11 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         entity.setActualResponseGridKw(request.getActualResponseGridKw());
         entity.setActualResponsePlatformKw(platformKw);
         entity.setDeclaredClearedKw(declaredKw);
-        entity.setEstimatedCapacityKw(platformKw);
+        entity.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
         entity.setClearedCapacityKw(actualClearedKw);
         entity.setTenantId(event.getTenantId() != null ? event.getTenantId() : invitation.getTenantId());
 
-        recalculate(entity, loadActivePriceAdjustments(
-                entity.getTenantId() != null ? entity.getTenantId() : SecurityUtils.getTenantId()));
+        applyCalcResult(entity, subsidyPredictionCalculator.calculate(invitation, event));
 
         Map<Long, String> invitationNoMap = Collections.singletonMap(invitation.getId(), invitation.getInvitationNo());
         return toVo(entity, invitationNoMap);
@@ -246,7 +244,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         }
 
         VppDrEvent event = eventMapper.selectById(drEventId);
-        if (event == null) {
+        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
             log.warn("补贴预测生成跳过:事件不存在 drEventId={}", drEventId);
             return;
         }
@@ -275,11 +273,13 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 .collect(Collectors.toSet());
         Map<Long, String> customerNameMap = loadCustomerNameMap(customerIds);
 
-        List<VppResponseDeviationPrice> adjustments = loadActivePriceAdjustments(event.getTenantId());
+        List<VppResponseDeviationPrice> adjustments = subsidyPredictionCalculator.loadAdjustments(event.getTenantId());
+        List<VppBiddingConfig> biddingConfigs =
+                subsidyPredictionCalculator.loadBiddingConfigs(event.getTenantId());
 
         for (VppDrInvitation invitation : invitations) {
             VppDrSubsidyPrediction prediction = buildPrediction(
-                    event, invitation, siteNameMap, customerNameMap, adjustments);
+                    event, invitation, siteNameMap, customerNameMap, adjustments, biddingConfigs);
 
             VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
                     new LambdaQueryWrapper<VppDrSubsidyPrediction>()
@@ -303,7 +303,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
                                                     Map<Long, String> siteNameMap,
                                                     Map<Long, String> customerNameMap,
-                                                    List<VppResponseDeviationPrice> adjustments) {
+                                                    List<VppResponseDeviationPrice> adjustments,
+                                                    List<VppBiddingConfig> biddingConfigs) {
         VppDrSubsidyPrediction p = new VppDrSubsidyPrediction();
         p.setInvitationId(invitation.getId());
         p.setDrEventId(event.getId());
@@ -313,7 +314,6 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         p.setCustomerName(invitation.getCustomerId() == null ? null : customerNameMap.get(invitation.getCustomerId()));
         p.setEventStartTime(event.getStartTime());
         p.setEventEndTime(event.getEndTime());
-        p.setSubsidyPrice(event.getSubsidyPrice());
 
         BigDecimal platformKw = resolvePlatformActualResponseKw(
                 invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
@@ -322,17 +322,16 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
                 ? invitation.getDeclaredCapacityKw()
                 : invitation.getActualClearedCapacityKw();
-        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
-                ? invitation.getActualClearedCapacityKw()
-                : declaredKw;
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw();
 
         p.setActualResponsePlatformKw(platformKw);
         p.setActualResponseGridKw(gridKw);
         p.setDeclaredClearedKw(declaredKw);
-        p.setEstimatedCapacityKw(platformKw);
+        p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
         p.setClearedCapacityKw(actualClearedKw);
         p.setTenantId(event.getTenantId());
-        recalculate(p, adjustments);
+        applyCalcResult(p, subsidyPredictionCalculator.calculate(
+                invitation, event, adjustments, biddingConfigs));
         return p;
     }
 
@@ -473,29 +472,13 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
     }
 
-    private List<VppResponseDeviationPrice> loadActivePriceAdjustments(Integer tenantId) {
-        return priceAdjustmentMapper.selectList(
-                new LambdaQueryWrapper<VppResponseDeviationPrice>()
-                        .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                        .eq(tenantId != null, VppResponseDeviationPrice::getTenantId, tenantId)
-                        .orderByAsc(VppResponseDeviationPrice::getId));
-    }
-
-    private BigDecimal resolvePriceAdjustmentCoefficient(BigDecimal completionRate,
-                                                         List<VppResponseDeviationPrice> adjustments) {
-        if (adjustments != null) {
-            for (VppResponseDeviationPrice adjustment : adjustments) {
-                if (VppResponseCoefficientHelper.matches(
-                        adjustment.getCoeffLower(),
-                        adjustment.getCoeffUpper(),
-                        adjustment.getLowerInclusive(),
-                        adjustment.getUpperInclusive(),
-                        completionRate)) {
-                    return adjustment.getPriceAdjustmentCoefficient();
-                }
-            }
-        }
-        throw new BusinessException("未匹配到响应量偏差价格调整配置!");
+    private void applyCalcResult(VppDrSubsidyPrediction entity, SubsidyPredictionCalcResult calc) {
+        entity.setResponseDurationMin(calc.getResponseDurationMin());
+        entity.setPriceAdjustmentCoefficient(calc.getPriceAdjustmentCoefficient());
+        entity.setCompletionRate(calc.getCompletionRate());
+        entity.setDeviationRate(calc.getDeviationRate());
+        entity.setSubsidyPrice(calc.getSubsidyPrice());
+        entity.setEstimatedSubsidyAmount(calc.getEstimatedSubsidyAmount());
     }
 
     private Map<Long, String> loadInvitationNoMap(List<VppDrSubsidyPrediction> predictions) {
@@ -511,58 +494,6 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                         (left, right) -> left));
     }
 
-    /**
-     * 自动计算:响应时长、偏差率、完成率、补贴金额
-     * <ul>
-     *   <li>完成率(%)= 实际响应容量(电网) / 实际出清 × 100(实际出清为 null 或 ≤0 时不计算)</li>
-     *   <li>偏差率(%)= (1 − 完成率) × 100</li>
-     *   <li>价格调整系数:按完成率比例匹配响应量偏差价格调整配置</li>
-     *   <li>补贴金额 = 电网实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
-     * </ul>
-     */
-    private void recalculate(VppDrSubsidyPrediction entity,
-                             List<VppResponseDeviationPrice> adjustments) {
-        int durationMin = 0;
-        if (entity.getEventStartTime() != null && entity.getEventEndTime() != null
-                && entity.getEventEndTime().isAfter(entity.getEventStartTime())) {
-            durationMin = (int) Duration.between(entity.getEventStartTime(), entity.getEventEndTime()).toMinutes();
-        }
-        entity.setResponseDurationMin(durationMin);
-
-        BigDecimal gridKw = entity.getActualResponseGridKw();
-        BigDecimal clearedKw = entity.getClearedCapacityKw() != null
-                ? entity.getClearedCapacityKw()
-                : entity.getDeclaredClearedKw();
-
-        if (clearedKw == null || clearedKw.compareTo(BigDecimal.ZERO) <= 0 || gridKw == null) {
-            entity.setDeviationRate(null);
-            entity.setCompletionRate(null);
-            entity.setPriceAdjustmentCoefficient(null);
-            entity.setEstimatedSubsidyAmount(null);
-            return;
-        }
-
-        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));
-        entity.setDeviationRate(deviationRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));
-        entity.setPriceAdjustmentCoefficient(priceAdjustmentCoefficient);
-
-        // 补贴按电网核定实际响应量结算
-        BigDecimal subsidyAmount = null;
-        if (entity.getSubsidyPrice() != null && durationMin > 0) {
-            BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
-            subsidyAmount = gridKw
-                    .multiply(hours)
-                    .multiply(priceAdjustmentCoefficient)
-                    .multiply(entity.getSubsidyPrice())
-                    .setScale(4, RoundingMode.HALF_UP);
-        }
-        entity.setEstimatedSubsidyAmount(subsidyAmount);
-    }
-
     /** 分成比例统一为 0~1;若传入值 &gt; 1 则按百分数换算 */
     private BigDecimal normalizeShareRatio(BigDecimal raw) {
         if (raw == null) {

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java

@@ -2,6 +2,7 @@ package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.constant.VppTsdbConstants;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDevice;
@@ -57,7 +58,7 @@ import java.util.stream.Collectors;
 public class VppResponseMonitorServiceImpl implements VppResponseMonitorService {
 
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
 
     @Autowired
     private VppSiteMapper siteMapper;

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteCompletionRateTaskServiceImpl.java

@@ -3,6 +3,7 @@ package com.usky.vpp.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.domain.VppDevice;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrParticipation;
@@ -40,7 +41,7 @@ public class VppSiteCompletionRateTaskServiceImpl implements VppSiteCompletionRa
 
     private static final Logger log = LoggerFactory.getLogger(VppSiteCompletionRateTaskServiceImpl.class);
 
-    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
     private static final int SITE_STATUS_ONLINE = 0;
     private static final int SITE_STATUS_OFFLINE = 1;
     private static final int SITE_STATUS_FAULT = 2;

+ 1 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java

@@ -36,6 +36,7 @@ public class DrEventDetailVO {
     private BigDecimal subsidyPrice;
     /** 下浮系数 */
     private BigDecimal floatingCoefficient;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     private Integer eventStatus;
     private List<DrParticipationVO> participations;
 }

+ 19 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventStatusRequest.java

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 修改需求响应事件状态
+ */
+@Data
+public class DrEventStatusRequest {
+
+    /** 事件表主键 id */
+    private Long id;
+
+    /**
+     * 目标状态。当前仅允许 1(已申报),且原状态必须为 0(待参与)。
+     * 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消
+     */
+    private Integer eventStatus;
+}

+ 3 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java

@@ -15,6 +15,8 @@ public class DrInvitationVO {
     private Long id;
     private String invitationNo;
     private Long drEventId;
+    /** 事件业务编号(vpp_dr_event.event_id) */
+    private String eventId;
     private Long customerId;
     private String customerName;
     private LocalDate executeStartDate;
@@ -37,6 +39,7 @@ public class DrInvitationVO {
     /** 是否中标 0否 1是 */
     private Integer isWinningBid;
     private Integer replyStatus;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     private Integer responseStatus;
     private Integer smsNotifyStatus;
     private LocalDateTime smsNotifyAt;

+ 15 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java

@@ -7,16 +7,22 @@ import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
- * 响应监测 - 响应记录(按 vpp_dr_event 实际字段 + 完成率
+ * 响应监测 - 响应记录(事件字段 + 邀约维度补充
  */
 @Data
 public class DrMonitorRecordVO {
 
+    /** 事件主键 */
     private Long id;
+    /** 邀约主键(曲线查询入参) */
+    private Long invitationId;
     private String eventId;
     private String eventName;
+    private Long customerId;
+    private String customerName;
+    private Long siteId;
+    private String siteName;
     private Integer responseType;
-    private Integer eventType;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime startTime;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -28,16 +34,17 @@ public class DrMonitorRecordVO {
     /** 邀约范围 */
     private String inviteScope;
 
-    /** 邀约容量 */
-    private BigDecimal targetCapacityKw;
+    /** 预估响应容量:邀约 estimated_response_capacity_kw */
+    private BigDecimal estimatedResponseCapacityKw;
 
-    /** 出清容量 */
+    /** 实际出清:邀约 actual_cleared_capacity_kw */
     private BigDecimal clearedCapacityKw;
     private String remark;
     private String attachmentUrl;
     private BigDecimal subsidyPrice;
     /** 下浮系数 */
     private BigDecimal floatingCoefficient;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     private Integer eventStatus;
     private String rawPayload;
     private Integer tenantId;
@@ -48,10 +55,10 @@ public class DrMonitorRecordVO {
     private String createdBy;
     private String updatedBy;
 
-    /** 关联邀约完成率均值(0~1) */
+    /** 邀约响应完成率 */
     private BigDecimal responseCompletionRate;
-    /** 是否达标:关联邀约 response_completion_rate = 100% */
+    /** 是否达标:完成率 >= 100% */
     private Boolean qualified;
-    /** 补贴金额预测) */
+    /** 补贴金额预测(元= 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数 */
     private BigDecimal estimatedSubsidyAmount;
 }

+ 35 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSiteResourceVO.java

@@ -0,0 +1,35 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 响应监测 - 事件下邀约站点及其可调峰资源点(站点 → 资源点)
+ */
+@Data
+public class DrMonitorSiteResourceVO {
+
+    private Long siteId;
+    private String siteCode;
+    private String siteName;
+    private List<ResourceItem> resources = new ArrayList<>();
+
+    @Data
+    public static class ResourceItem {
+        private Long resourceId;
+        private String resourceCode;
+        private String resourceName;
+        private String resourceType;
+        private String resourceTypeLabel;
+        private BigDecimal capacityKw;
+        private Integer isControl;
+        private Integer isSupportPeak;
+        private BigDecimal maxUpKw;
+        private BigDecimal minDownKw;
+        private Integer isSupportFm;
+        private String remark;
+    }
+}

+ 4 - 6
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSummaryVO.java

@@ -5,17 +5,15 @@ import lombok.Data;
 import java.math.BigDecimal;
 
 /**
- * 响应监测 - 顶部 KPI(按可调峰资源点平均
+ * 响应监测 - 顶部 KPI(按符合条件的邀约条数统计
  */
 @Data
 public class DrMonitorSummaryVO {
 
-    /** 可调峰资源点数(分母) */
-    private Long peakResourceCount = 0L;
-    /** 响应次数(执行中+已结束,按资源点平均) */
+    /** 响应次数(符合条件的邀约总条数) */
     private Long responseCount = 0L;
-    /** 响应容量 kW(仅已结束,按资源点平均) */
+    /** 响应容量 kW(actual_response_capacity_kw 累加) */
     private BigDecimal responseCapacityKw = BigDecimal.ZERO;
-    /** 历史达标率 0~1(已结束,按资源点平均) */
+    /** 历史达标率 0~1(完成率 >= 100% 的条数 / 总条数) */
     private BigDecimal historicalQualifiedRate = BigDecimal.ZERO;
 }

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

@@ -40,10 +40,10 @@ public class DrSubsidyPredictionVO {
     /** 补贴标准(元/kWh) */
     private BigDecimal subsidyPrice;
 
-    /** 预估响应容量(kW,兼容) */
+    /** 预估响应容量(kW,取自邀约) */
     private BigDecimal estimatedCapacityKw;
 
-    /** 实际出清(kW,兼容) */
+    /** 实际出清(kW,取自邀约) */
     private BigDecimal clearedCapacityKw;
 
     /** 实际响应量-电网(kW) */
@@ -58,16 +58,16 @@ public class DrSubsidyPredictionVO {
     /** 分成比例(0~1) */
     private BigDecimal shareRatio;
 
-    /** 按完成率匹配得到的价格调整系数。 */
+    /** 用完成率对应的响应量系数命中偏差配置后的价格调整系数。 */
     private BigDecimal priceAdjustmentCoefficient;
 
-    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
+    /** 完成率(百分比)= 实际出清 / 预估响应容量 × 100 */
     private BigDecimal completionRate;
 
     /** 偏差率(百分比)= (1 − 完成率) × 100 */
     private BigDecimal deviationRate;
 
-    /** 预估补贴金额(元) */
+    /** 预估补贴金额(元)= 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数 */
     private BigDecimal estimatedSubsidyAmount;
 
     private LocalDateTime createTime;

+ 27 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SubsidyPredictionCalcResult.java

@@ -0,0 +1,27 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 补贴金额预测统一计算结果。
+ */
+@Data
+public class SubsidyPredictionCalcResult {
+
+    /** 响应时长(分钟) */
+    private Integer responseDurationMin = 0;
+    /** 响应量系数(实际出清 / 预估响应容量,比例) */
+    private BigDecimal responseCoefficient;
+    /** 完成率(百分比)= 响应量系数 × 100 */
+    private BigDecimal completionRate;
+    /** 偏差率(百分比)= (1 − 响应量系数) × 100 */
+    private BigDecimal deviationRate;
+    /** 价格调整系数;未命中区间时为空 */
+    private BigDecimal priceAdjustmentCoefficient;
+    /** 竞价价格(元/kWh):下浮系数命中唯一配置后,削峰取最高价、填谷取最低价 */
+    private BigDecimal subsidyPrice;
+    /** 补贴金额预测(元)= 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数;边界情况为 0 */
+    private BigDecimal estimatedSubsidyAmount = BigDecimal.ZERO;
+}

+ 4 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java

@@ -1,5 +1,6 @@
 package com.usky.vpp.util;
 
+import com.usky.vpp.constant.VppDrEventStatus;
 import com.usky.vpp.enums.VppUnEventPhase;
 import org.springframework.util.StringUtils;
 
@@ -130,11 +131,11 @@ public final class VppUnEventParser {
         }
         switch (status.trim().toLowerCase()) {
             case "cancelled":
-                return 4;
+                return VppDrEventStatus.CANCELLED;
             case "completed":
-                return 3;
+                return VppDrEventStatus.ENDED;
             case "active":
-                return 2;
+                return VppDrEventStatus.EXECUTING;
             case "far":
             case "near":
             case "none":

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

@@ -1,5 +1,6 @@
 package com.usky.vpp.util;
 
+import com.usky.vpp.constant.VppDrEventStatus;
 import org.springframework.util.StringUtils;
 
 import java.math.BigDecimal;
@@ -205,7 +206,7 @@ public final class VppUnPayloadHelper {
 
     public static boolean isCancelled(Map<String, Object> payload) {
         Integer status = getInteger(payload, "eventStatus", "event_status", "status");
-        if (status != null && status == 4) {
+        if (status != null && (status == 4 || status == VppDrEventStatus.CANCELLED)) {
             return true;
         }
         String text = getString(payload, "eventStatus", "event_status", "status", "eventState");
@@ -213,7 +214,8 @@ public final class VppUnPayloadHelper {
             return false;
         }
         text = text.trim().toUpperCase();
-        return text.contains("取消") || "CANCELLED".equals(text) || "CANCELED".equals(text) || "4".equals(text);
+        return text.contains("取消") || "CANCELLED".equals(text) || "CANCELED".equals(text)
+                || "4".equals(text) || "5".equals(text);
     }
 
     private static Object getValue(Map<String, Object> map, String... keys) {

+ 2 - 2
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -453,7 +453,7 @@ CREATE TABLE `vpp_dr_event` (
     `attachment_name` VARCHAR(200) NULL COMMENT '附件名称',
     `subsidy_price` DECIMAL(10,4) NULL COMMENT '补贴标准 元/kWh',
     `floating_coefficient` DECIMAL(10,4) NULL COMMENT '下浮系数',
-    `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2执行中 3已结束 4已取消',
+    `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消',
     `raw_payload` JSON NULL COMMENT '原始邀约报文',
     `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
@@ -509,7 +509,7 @@ CREATE TABLE `vpp_dr_invitation` (
     `response_completion_rate` DECIMAL(12,4) NULL COMMENT '响应完成率=实际响应容量/邀约规模',
     `is_winning_bid` TINYINT NULL COMMENT '是否中标 0否 1是',
     `reply_status` TINYINT NOT NULL COMMENT '0待回复 1参与 2拒绝 3超时',
-    `response_status` TINYINT NOT NULL COMMENT '0待响应 1已申报 2执行中 3已结束 4已取消',
+    `response_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消',
     `sms_notify_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未发送 1已发送 2发送失败',
     `sms_notify_at` DATETIME(3) NULL COMMENT '短信通知时间',
     `sms_contact_id` BIGINT NULL COMMENT '通知联系人ID',