|
|
@@ -0,0 +1,463 @@
|
|
|
+package com.usky.vpp.service.impl;
|
|
|
+
|
|
|
+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.domain.VppDrEvent;
|
|
|
+import com.usky.vpp.domain.VppDrInvitation;
|
|
|
+import com.usky.vpp.domain.VppDrParticipation;
|
|
|
+import com.usky.vpp.domain.VppResourcePoint;
|
|
|
+import com.usky.vpp.mapper.VppDrEventMapper;
|
|
|
+import com.usky.vpp.mapper.VppDrInvitationMapper;
|
|
|
+import com.usky.vpp.mapper.VppDrParticipationMapper;
|
|
|
+import com.usky.vpp.mapper.VppResourcePointMapper;
|
|
|
+import com.usky.vpp.service.VppBaselineService;
|
|
|
+import com.usky.vpp.service.VppDrMonitorService;
|
|
|
+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.DrMonitorSummaryVO;
|
|
|
+import com.usky.vpp.service.vo.SiteBaselineVO;
|
|
|
+import com.usky.vpp.util.VppAuditHelper;
|
|
|
+import com.usky.vpp.util.VppPageHelper;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.LocalTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 需求响应监测(响应记录)
|
|
|
+ * <p>统一口径:站点 → is_support_peak=1 资源点 → 按资源点平均。</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 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");
|
|
|
+ private static final BigDecimal ONE = BigDecimal.ONE;
|
|
|
+ private static final BigDecimal HUNDRED = new BigDecimal("100");
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VppDrEventMapper eventMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrInvitationMapper invitationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrParticipationMapper participationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppResourcePointMapper resourcePointMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppBaselineService baselineService;
|
|
|
+
|
|
|
+ @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);
|
|
|
+
|
|
|
+ List<VppResourcePoint> peakResources = listPeakResources(siteIds, tenantId);
|
|
|
+ DrMonitorSummaryVO vo = new DrMonitorSummaryVO();
|
|
|
+ vo.setPeakResourceCount((long) peakResources.size());
|
|
|
+ if (peakResources.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++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ vo.setResponseCapacityKw(avg(capacitySum, peakResources.size()));
|
|
|
+ if (rateResourceCount > 0) {
|
|
|
+ vo.setHistoricalQualifiedRate(avg(rateSum, rateResourceCount));
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ 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));
|
|
|
+
|
|
|
+ List<DrMonitorRecordVO> records = events.stream()
|
|
|
+ .map(e -> toRecordVo(e, byEvent.getOrDefault(e.getId(), Collections.emptyList())))
|
|
|
+ .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());
|
|
|
+ int to = (int) Math.min(from + size, records.size());
|
|
|
+ List<DrMonitorRecordVO> pageRecords = from >= records.size()
|
|
|
+ ? Collections.emptyList()
|
|
|
+ : records.subList(from, to);
|
|
|
+ return new CommonPage<>(pageRecords, (long) records.size(), current, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds) {
|
|
|
+ if (eventId == null) {
|
|
|
+ throw new BusinessException("事件ID不能为空");
|
|
|
+ }
|
|
|
+ requireSiteIds(siteIds);
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ VppDrEvent event = eventMapper.selectById(eventId);
|
|
|
+ if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
|
|
|
+ throw new BusinessException("事件不存在");
|
|
|
+ }
|
|
|
+ if (tenantId != null && event.getTenantId() != null && !tenantId.equals(event.getTenantId())) {
|
|
|
+ throw new BusinessException("事件不存在");
|
|
|
+ }
|
|
|
+ if (event.getStartTime() == null) {
|
|
|
+ throw new BusinessException("事件缺少开始时间");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VppResourcePoint> peakResources = listPeakResources(siteIds, tenantId);
|
|
|
+ if (peakResources.isEmpty()) {
|
|
|
+ throw new BusinessException("所选站点下无可参与调峰的资源点");
|
|
|
+ }
|
|
|
+ int resourceCount = peakResources.size();
|
|
|
+ Map<Long, Long> siteResourceCount = new HashMap<>();
|
|
|
+ for (VppResourcePoint resource : peakResources) {
|
|
|
+ siteResourceCount.merge(resource.getSiteId(), 1L, Long::sum);
|
|
|
+ }
|
|
|
+ Set<Long> peakSiteIds = siteResourceCount.keySet();
|
|
|
+
|
|
|
+ List<VppDrInvitation> invitations = listInvitations(
|
|
|
+ Collections.singleton(event.getId()), peakSiteIds, tenantId);
|
|
|
+ BigDecimal declaredSum = BigDecimal.ZERO;
|
|
|
+ BigDecimal actualSum = BigDecimal.ZERO;
|
|
|
+ for (VppDrInvitation invitation : invitations) {
|
|
|
+ declaredSum = declaredSum.add(nz(invitation.getDeclaredCapacityKw()));
|
|
|
+ actualSum = actualSum.add(nz(invitation.getActualResponseCapacityKw()));
|
|
|
+ }
|
|
|
+ BigDecimal declaredAvg = avg(declaredSum, resourceCount);
|
|
|
+ BigDecimal actualAvg = avg(actualSum, resourceCount);
|
|
|
+
|
|
|
+ String responseStartTime = event.getStartTime().format(DATE_TIME_FMT);
|
|
|
+ // 按资源点加权:站点曲线 × 该站可调峰资源数,再 / 总资源数
|
|
|
+ Map<String, BigDecimal> baselineWeighted = new LinkedHashMap<>();
|
|
|
+ Map<String, BigDecimal> loadWeighted = new LinkedHashMap<>();
|
|
|
+ Set<String> dataSources = new LinkedHashSet<>();
|
|
|
+ List<Long> usedSiteIds = new ArrayList<>();
|
|
|
+
|
|
|
+ for (Map.Entry<Long, Long> entry : siteResourceCount.entrySet()) {
|
|
|
+ Long siteId = entry.getKey();
|
|
|
+ BigDecimal weight = BigDecimal.valueOf(entry.getValue());
|
|
|
+ SiteBaselineVO baseline = baselineService.getSiteBaseline(siteId, responseStartTime);
|
|
|
+ if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ usedSiteIds.add(siteId);
|
|
|
+ if (StringUtils.hasText(baseline.getDataSource())) {
|
|
|
+ dataSources.add(baseline.getDataSource());
|
|
|
+ }
|
|
|
+ for (BaselinePointVO point : baseline.getPoints()) {
|
|
|
+ if (!StringUtils.hasText(point.getTime())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ baselineWeighted.merge(point.getTime(),
|
|
|
+ nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
|
|
|
+ loadWeighted.merge(point.getTime(),
|
|
|
+ nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal divisor = BigDecimal.valueOf(resourceCount);
|
|
|
+ DrMonitorCurveVO curve = new DrMonitorCurveVO();
|
|
|
+ curve.setId(event.getId());
|
|
|
+ curve.setEventId(event.getEventId());
|
|
|
+ curve.setResponseDate(event.getStartTime().toLocalDate().format(DATE_FMT));
|
|
|
+ curve.setSiteIds(usedSiteIds);
|
|
|
+ curve.setDeclaredCapacityKw(declaredAvg);
|
|
|
+ curve.setActualResponseCapacityKw(actualAvg);
|
|
|
+ if (dataSources.isEmpty()) {
|
|
|
+ curve.setDataSource("mock");
|
|
|
+ } else if (dataSources.size() == 1) {
|
|
|
+ curve.setDataSource(dataSources.iterator().next());
|
|
|
+ } else {
|
|
|
+ curve.setDataSource("mixed");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> times = new ArrayList<>(baselineWeighted.keySet());
|
|
|
+ times.sort(Comparator.naturalOrder());
|
|
|
+ for (String time : times) {
|
|
|
+ DrMonitorCurveVO.Point point = new DrMonitorCurveVO.Point();
|
|
|
+ point.setTime(time);
|
|
|
+ point.setBaselineKw(baselineWeighted.getOrDefault(time, BigDecimal.ZERO)
|
|
|
+ .divide(divisor, SCALE, RoundingMode.HALF_UP));
|
|
|
+ point.setLoadKw(loadWeighted.getOrDefault(time, BigDecimal.ZERO)
|
|
|
+ .divide(divisor, SCALE, RoundingMode.HALF_UP));
|
|
|
+ point.setDeclaredCapacityKw(declaredAvg);
|
|
|
+ point.setActualResponseCapacityKw(actualAvg);
|
|
|
+ curve.getPoints().add(point);
|
|
|
+ }
|
|
|
+ return curve;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- loaders ----------
|
|
|
+
|
|
|
+ private void requireSiteIds(List<Long> siteIds) {
|
|
|
+ if (CollectionUtils.isEmpty(siteIds)) {
|
|
|
+ throw new BusinessException("站点ID不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)) {
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ 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()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
|
|
|
+ .in(VppDrEvent::getId, eventIds)
|
|
|
+ .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDrEvent::getTenantId, tenantId))
|
|
|
+ .stream()
|
|
|
+ .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);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppDrInvitation> listInvitations(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
|
|
|
+ if (CollectionUtils.isEmpty(eventIds) || CollectionUtils.isEmpty(siteIds)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ private DrMonitorRecordVO toRecordVo(VppDrEvent event, List<VppDrInvitation> invitations) {
|
|
|
+ DrMonitorRecordVO vo = new DrMonitorRecordVO();
|
|
|
+ BeanUtils.copyProperties(event, vo);
|
|
|
+ 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 (rateCount > 0) {
|
|
|
+ vo.setResponseCompletionRate(sum.divide(BigDecimal.valueOf(rateCount), SCALE, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+ vo.setQualified(allQualified && rateCount == invitations.size());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ return nz(sum).divide(BigDecimal.valueOf(divisor), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static LocalDateTime parseStart(String date) {
|
|
|
+ if (!StringUtils.hasText(date)) {
|
|
|
+ throw new BusinessException("开始日期不能为空");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDate.parse(date.trim(), DATE_FMT).atStartOfDay();
|
|
|
+ } catch (DateTimeParseException ex) {
|
|
|
+ throw new BusinessException("开始日期格式无效,应为 yyyy-MM-dd");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static LocalDateTime parseEnd(String date) {
|
|
|
+ if (!StringUtils.hasText(date)) {
|
|
|
+ throw new BusinessException("结束日期不能为空");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDate.parse(date.trim(), DATE_FMT).atTime(LocalTime.MAX);
|
|
|
+ } catch (DateTimeParseException ex) {
|
|
|
+ throw new BusinessException("结束日期格式无效,应为 yyyy-MM-dd");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal nz(BigDecimal value) {
|
|
|
+ return value == null ? BigDecimal.ZERO : value;
|
|
|
+ }
|
|
|
+}
|