Переглянути джерело

按邀约口径调整响应监测 summary、records 与曲线接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
fuyuchuan 3 днів тому
батько
коміт
f546d31949

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

@@ -29,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,
@@ -39,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,
@@ -51,14 +51,12 @@ 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));
     }
 
     /**

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

@@ -15,28 +15,26 @@ 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);
 
     /**
      * 按响应事件查询邀约站点及其可调峰资源点,以站点分组返回。

+ 167 - 270
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -6,15 +6,15 @@ 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.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.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;
@@ -26,7 +26,6 @@ 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.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
 import com.usky.vpp.util.VppResourceTypeHelper;
@@ -47,7 +46,6 @@ 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;
@@ -57,15 +55,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 = 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");
@@ -77,7 +75,7 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     @Autowired
     private VppDrInvitationMapper invitationMapper;
     @Autowired
-    private VppDrParticipationMapper participationMapper;
+    private VppCustomerMapper customerMapper;
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
@@ -89,136 +87,63 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
 
     @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);
+        Map<Long, BigDecimal> subsidyByEvent = loadSubsidyAmountByEvent(
+                eventIds, new LinkedHashSet<>(siteIds), 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()),
+                        subsidyByEvent.get(inv.getDrEventId())))
                 .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());
@@ -230,56 +155,57 @@ 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;
@@ -357,46 +283,11 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         item.setIsSupportPeak(resource.getIsSupportPeak());
         item.setMaxUpKw(resource.getMaxUpKw());
         item.setMinDownKw(resource.getMinDownKw());
+        item.setIsSupportFm(resource.getIsSupportFm());
+        item.setRemark(resource.getRemark());
         return item;
     }
 
-    /**
-     * 实际响应容量复用申报容量链路 {@link VppBaselineService#getSiteDeclaredCapacity},
-     * 取预估响应容量;无数据时回退邀约实际响应容量。
-     */
-    private BigDecimal resolveSiteActualResponseKw(Long siteId, String responseStartTime,
-                                                   String responseEndTime, BigDecimal fallbackKw) {
-        if (siteId == null || !StringUtils.hasText(responseStartTime) || !StringUtils.hasText(responseEndTime)) {
-            return nz(fallbackKw);
-        }
-        try {
-            SiteDeclaredCapacityVO capacity = baselineService.getSiteDeclaredCapacity(
-                    siteId, responseStartTime, responseEndTime);
-            if (capacity != null && capacity.getEstimatedResponseCapacityKw() != null
-                    && capacity.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
-                return capacity.getEstimatedResponseCapacityKw();
-            }
-        } catch (Exception ignored) {
-            // 回退邀约实际响应容量,保持曲线可返回
-        }
-        return nz(fallbackKw);
-    }
-
-    private boolean inCurveWindow(String timeKey, LocalDateTime windowStart, LocalDateTime windowEnd) {
-        try {
-            LocalTime t = LocalTime.parse(timeKey.length() == 5 ? timeKey : timeKey.substring(0, 5));
-            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;
-        }
-    }
-
     private Map<Long, BigDecimal> loadSubsidyAmountByEvent(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
         if (CollectionUtils.isEmpty(eventIds)) {
             return Collections.emptyMap();
@@ -426,40 +317,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>()
@@ -470,79 +355,91 @@ 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,
+    private DrMonitorRecordVO toRecordVo(VppDrInvitation invitation, VppDrEvent event,
+                                         VppCustomer customer, VppSite site,
                                          BigDecimal estimatedSubsidyAmount) {
         DrMonitorRecordVO vo = new DrMonitorRecordVO();
-        BeanUtils.copyProperties(event, vo);
-        vo.setEstimatedSubsidyAmount(estimatedSubsidyAmount);
-        if (CollectionUtils.isEmpty(invitations)) {
-            vo.setResponseCompletionRate(null);
-            vo.setQualified(false);
-            return vo;
+        if (event != null) {
+            BeanUtils.copyProperties(event, 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;
-            }
+        vo.setInvitationId(invitation.getId());
+        vo.setCustomerId(invitation.getCustomerId());
+        if (customer != null) {
+            vo.setCustomerName(customer.getCustomerName());
         }
-        if (rateCount > 0) {
-            vo.setResponseCompletionRate(sum.divide(BigDecimal.valueOf(rateCount), SCALE, RoundingMode.HALF_UP));
+        vo.setSiteId(invitation.getSiteId());
+        if (site != null) {
+            vo.setSiteName(site.getSiteName());
         }
-        vo.setQualified(allQualified && rateCount == invitations.size());
+        vo.setTargetCapacityKw(invitation.getDeclaredCapacityKw());
+        vo.setClearedCapacityKw(invitation.getActualResponseCapacityKw());
+        vo.setResponseCompletionRate(invitation.getResponseCompletionRate());
+        vo.setQualified(isCompletionQualified(invitation.getResponseCompletionRate()));
+        vo.setEstimatedSubsidyAmount(estimatedSubsidyAmount);
         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 (event != null && event.getStartTime() != null) {
+            return event.getStartTime().format(DATE_TIME_FMT);
+        }
+        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) {
         if (rate == null) {
             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) {

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

@@ -7,14 +7,21 @@ 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")
@@ -28,10 +35,10 @@ public class DrMonitorRecordVO {
     /** 邀约范围 */
     private String inviteScope;
 
-    /** 邀约容量 */
+    /** 目标容量:邀约申报出清容量 declared_capacity_kw */
     private BigDecimal targetCapacityKw;
 
-    /** 出清容量 */
+    /** 出清容量:邀约实际响应容量 actual_response_capacity_kw */
     private BigDecimal clearedCapacityKw;
     private String remark;
     private String attachmentUrl;
@@ -49,9 +56,9 @@ public class DrMonitorRecordVO {
     private String createdBy;
     private String updatedBy;
 
-    /** 关联邀约完成率均值(0~1) */
+    /** 邀约响应完成率 */
     private BigDecimal responseCompletionRate;
-    /** 是否达标:关联邀约 response_completion_rate = 100% */
+    /** 是否达标:完成率 >= 100% */
     private Boolean qualified;
     /** 补贴金额(预测) */
     private BigDecimal estimatedSubsidyAmount;

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