瀏覽代碼

响应监测曲线改为前2小时至结束后1小时,并补充实际负荷。

Co-authored-by: Cursor <cursoragent@cursor.com>
fuyuchuan 5 天之前
父節點
當前提交
7ffb2d5572

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

@@ -51,8 +51,8 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:基线负荷全天曲线 + 实际响应容量
-     * <p>id 为邀约表主键;按邀约站点与响应时间调用 getSiteBaseline。</p>
+     * 折线图:基线负荷 + 实际负荷 + 实际响应容量
+     * <p>id 为邀约表主键;时间范围为响应开始前 2 小时至响应结束后 1 小时。</p>
      */
     @GetMapping("/records/curve")
     public ApiResult<DrMonitorCurveVO> curve(@RequestParam("id") Long id) {

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

@@ -30,7 +30,8 @@ public interface VppDrMonitorService {
                                               List<Long> siteIds, Map<String, Object> params);
 
     /**
-     * 折线图:按邀约主键取站点与响应时间,复用 getSiteBaseline 全天曲线,并带上实际响应容量
+     * 折线图:按邀约主键取站点与响应时段,查询范围为开始前 2 小时至结束后 1 小时,
+     * 返回基线负荷、实际负荷与实际响应容量。
      *
      * @param invitationId 邀约表主键 id
      */

+ 115 - 14
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -8,12 +8,14 @@ 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.VppDevice;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 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.VppDeviceMapper;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrInvitationMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
@@ -21,6 +23,7 @@ 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.VppTsdbQueryService;
 import com.usky.vpp.service.vo.BaselinePointVO;
 import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
@@ -29,6 +32,7 @@ import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import com.usky.vpp.service.vo.SiteBaselineVO;
 import com.usky.vpp.service.vo.SubsidyPredictionCalcResult;
 import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppBaselineHelper;
 import com.usky.vpp.util.VppDrParticipationHelper;
 import com.usky.vpp.util.VppPageHelper;
 import com.usky.vpp.util.VppResourceTypeHelper;
@@ -48,6 +52,7 @@ import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -67,8 +72,11 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     private static final int REPLY_ACCEPT = 1;
     private static final int SUPPORT_PEAK = 1;
     private static final int SCALE = 4;
+    private static final int CURVE_BEFORE_HOURS = 2;
+    private static final int CURVE_AFTER_HOURS = 1;
     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");
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
     private static final BigDecimal ONE = BigDecimal.ONE;
     private static final BigDecimal HUNDRED = new BigDecimal("100");
 
@@ -81,10 +89,14 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
     private VppSiteMapper siteMapper;
     @Autowired
     private VppBaselineService baselineService;
     @Autowired
+    private VppTsdbQueryService tsdbQueryService;
+    @Autowired
     private VppSubsidyPredictionCalculator subsidyPredictionCalculator;
 
     @Override
@@ -180,9 +192,19 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         if (event != null && VppAuditHelper.isDeleted(event.getDeleteFlag())) {
             event = null;
         }
-        String responseStartTime = resolveInvitationResponseStartTime(invitation, event);
-        SiteBaselineVO baseline = baselineService.getSiteBaseline(invitation.getSiteId(), responseStartTime);
+        LocalDateTime responseStart = resolveInvitationResponseStart(invitation, event);
+        LocalDateTime responseEnd = resolveInvitationResponseEnd(invitation, event);
+        LocalDateTime queryStart = responseStart.minusHours(CURVE_BEFORE_HOURS);
+        LocalDateTime queryEnd = responseEnd.plusHours(CURVE_AFTER_HOURS);
+        if (queryEnd.isBefore(queryStart)) {
+            throw new BusinessException("响应时段无效");
+        }
+
+        SiteBaselineVO baseline = baselineService.getSiteBaseline(
+                invitation.getSiteId(), responseStart.format(DATE_TIME_FMT));
         BigDecimal actualKw = nz(invitation.getActualResponseCapacityKw()).setScale(SCALE, RoundingMode.HALF_UP);
+        Map<String, BigDecimal> actualLoadByTime = queryActualLoadByTime(
+                invitation.getSiteId(), tenantId, queryStart, queryEnd);
 
         DrMonitorCurveVO curve = new DrMonitorCurveVO();
         curve.setId(invitation.getId());
@@ -198,19 +220,22 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         }
         if (baseline != null && StringUtils.hasText(baseline.getDataSource())) {
             curve.setDataSource(baseline.getDataSource());
+        } else if (!actualLoadByTime.isEmpty()) {
+            curve.setDataSource("tsdb");
         }
-        if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
-            return curve;
-        }
-        for (BaselinePointVO point : baseline.getPoints()) {
-            if (!StringUtils.hasText(point.getTime())) {
-                continue;
-            }
+
+        Map<String, BigDecimal> baselineByTime = toBaselineByTime(baseline);
+        int interval = VppBaselineHelper.INTERVAL_MINUTES;
+        LocalDateTime cursor = alignToInterval(queryStart, interval);
+        while (!cursor.isAfter(queryEnd)) {
+            String timeKey = cursor.toLocalTime().format(TIME_FMT);
             DrMonitorCurveVO.Point curvePoint = new DrMonitorCurveVO.Point();
-            curvePoint.setTime(point.getTime());
-            curvePoint.setBaselineKw(nz(point.getPredictedBaselineKw()).setScale(SCALE, RoundingMode.HALF_UP));
+            curvePoint.setTime(timeKey);
+            curvePoint.setBaselineKw(scaleKw(baselineByTime.get(timeKey)));
+            curvePoint.setActualLoadKw(scaleKw(actualLoadByTime.get(timeKey)));
             curvePoint.setActualResponseCapacityKw(actualKw);
             curve.getPoints().add(curvePoint);
+            cursor = cursor.plusMinutes(interval);
         }
         return curve;
     }
@@ -399,18 +424,94 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         return vo;
     }
 
-    private String resolveInvitationResponseStartTime(VppDrInvitation invitation, VppDrEvent event) {
+    private LocalDateTime resolveInvitationResponseStart(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);
+            return LocalDateTime.of(invitation.getExecuteStartDate(), timeOfDay);
         }
         if (event != null && event.getStartTime() != null) {
-            return event.getStartTime().format(DATE_TIME_FMT);
+            return event.getStartTime();
         }
         throw new BusinessException("邀约缺少响应时间");
     }
 
+    private LocalDateTime resolveInvitationResponseEnd(VppDrInvitation invitation, VppDrEvent event) {
+        if (invitation.getExecuteEndDate() != null) {
+            LocalTime timeOfDay = (event != null && event.getEndTime() != null)
+                    ? event.getEndTime().toLocalTime() : LocalTime.of(23, 59, 59);
+            return LocalDateTime.of(invitation.getExecuteEndDate(), timeOfDay);
+        }
+        if (event != null && event.getEndTime() != null) {
+            return event.getEndTime();
+        }
+        throw new BusinessException("邀约缺少响应结束时间");
+    }
+
+    /**
+     * 按站点可控设备查询实际负荷,时间范围为响应开始前 2 小时至结束后 1 小时。
+     * <p>调用方式对齐 {@link VppTsdbQueryService#queryBaselineAvgByTimePoint}:
+     * 设备 UUID + 时段 + {@link VppBaselineHelper#POWER_METRICS},再按 interval 聚合成 HH:mm。</p>
+     */
+    private Map<String, BigDecimal> queryActualLoadByTime(Long siteId, Integer tenantId,
+                                                          LocalDateTime queryStart, LocalDateTime queryEnd) {
+        List<String> deviceUuids = listSiteDeviceUuids(siteId, tenantId);
+        if (deviceUuids.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return VppBaselineHelper.buildActualLoadByTimePoint(
+                tsdbQueryService.queryDeviceMetricHistory(
+                        deviceUuids, queryStart, queryEnd, VppBaselineHelper.POWER_METRICS),
+                VppBaselineHelper.INTERVAL_MINUTES);
+    }
+
+    private List<String> listSiteDeviceUuids(Long siteId, Integer tenantId) {
+        List<VppResourcePoint> resources = listPeakResources(Collections.singletonList(siteId), tenantId);
+        if (CollectionUtils.isEmpty(resources)) {
+            return Collections.emptyList();
+        }
+        Set<Long> deviceIds = resources.stream()
+                .map(VppResourcePoint::getDeviceId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (deviceIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Set<String> uuids = new LinkedHashSet<>();
+        for (VppDevice device : deviceMapper.selectBatchIds(deviceIds)) {
+            if (device == null || VppAuditHelper.isDeleted(device.getDeleteFlag())) {
+                continue;
+            }
+            if (StringUtils.hasText(device.getDeviceUuid())) {
+                uuids.add(device.getDeviceUuid().trim());
+            }
+        }
+        return new ArrayList<>(uuids);
+    }
+
+    private static Map<String, BigDecimal> toBaselineByTime(SiteBaselineVO baseline) {
+        if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
+            return Collections.emptyMap();
+        }
+        Map<String, BigDecimal> result = new LinkedHashMap<>();
+        for (BaselinePointVO point : baseline.getPoints()) {
+            if (!StringUtils.hasText(point.getTime())) {
+                continue;
+            }
+            result.put(point.getTime(), point.getPredictedBaselineKw());
+        }
+        return result;
+    }
+
+    private static LocalDateTime alignToInterval(LocalDateTime time, int intervalMinutes) {
+        int minute = time.getMinute() / intervalMinutes * intervalMinutes;
+        return time.withMinute(minute).withSecond(0).withNano(0);
+    }
+
+    private static BigDecimal scaleKw(BigDecimal value) {
+        return value == null ? null : value.setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
     private List<VppResourcePoint> listPeakResources(List<Long> siteIds, Integer tenantId) {
         return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
                 .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)

+ 3 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorCurveVO.java

@@ -7,7 +7,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * 响应监测 - 折线图(基线负荷 + 实际响应容量)
+ * 响应监测 - 折线图(基线负荷 + 实际负荷 + 实际响应容量)
  */
 @Data
 public class DrMonitorCurveVO {
@@ -28,6 +28,8 @@ public class DrMonitorCurveVO {
         private String time;
         /** 基线负荷 kW */
         private BigDecimal baselineKw;
+        /** 实际负荷 kW */
+        private BigDecimal actualLoadKw;
         /** 实际响应容量 kW */
         private BigDecimal actualResponseCapacityKw;
     }