|
|
@@ -1,11 +1,598 @@
|
|
|
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.VppSocialBenefitConstants;
|
|
|
+import com.usky.vpp.domain.VppCustomer;
|
|
|
+import com.usky.vpp.domain.VppDevice;
|
|
|
+import com.usky.vpp.domain.VppDrEvaluation;
|
|
|
+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.domain.VppSite;
|
|
|
+import com.usky.vpp.mapper.VppCustomerMapper;
|
|
|
+import com.usky.vpp.mapper.VppDeviceMapper;
|
|
|
+import com.usky.vpp.mapper.VppDrEvaluationMapper;
|
|
|
+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.mapper.VppSiteMapper;
|
|
|
+import com.usky.vpp.service.VppCapabilityEvalService;
|
|
|
import com.usky.vpp.service.VppDashboardService;
|
|
|
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
|
|
|
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
|
|
|
+import com.usky.vpp.service.vo.CapabilityLoadPointVO;
|
|
|
+import com.usky.vpp.service.vo.DashboardCurveVO;
|
|
|
+import com.usky.vpp.service.vo.DashboardMapVO;
|
|
|
+import com.usky.vpp.service.vo.DashboardSummaryVO;
|
|
|
+import com.usky.vpp.util.VppAuditHelper;
|
|
|
+import com.usky.vpp.util.VppCapabilityEvalHelper;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.Year;
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
+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;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
- * VppDashboardService 默认实现(待按业务完善)
|
|
|
+ * 运行管理监控大屏
|
|
|
+ * <p>租户隔离;DR 相关指标仅统计 event_status=已结束 的历史事件。</p>
|
|
|
*/
|
|
|
@Service
|
|
|
public class VppDashboardServiceImpl implements VppDashboardService {
|
|
|
+
|
|
|
+ private static final int EVENT_STATUS_ENDED = 3;
|
|
|
+ private static final int EVENT_TYPE_PEAK = 1;
|
|
|
+ private static final int EVENT_TYPE_VALLEY = 2;
|
|
|
+ private static final int REPLY_ACCEPT = 1;
|
|
|
+ private static final int COMM_ONLINE = 1;
|
|
|
+ private static final int COMM_OFFLINE = 0;
|
|
|
+ private static final int RUN_FAULT = 2;
|
|
|
+ private static final int IS_CONTROL = 1;
|
|
|
+ private static final int IS_WINNING = 1;
|
|
|
+ private static final int SCALE = 4;
|
|
|
+ private static final int MONEY_SCALE = 2;
|
|
|
+ private static final BigDecimal KW_PER_MW = new BigDecimal("1000");
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VppSiteMapper siteMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppCustomerMapper customerMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppResourcePointMapper resourcePointMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDeviceMapper deviceMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrEventMapper eventMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrEvaluationMapper evaluationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrInvitationMapper invitationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrParticipationMapper participationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppCapabilityEvalService capabilityEvalService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DashboardSummaryVO getSummary() {
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ DashboardSummaryVO vo = new DashboardSummaryVO();
|
|
|
+
|
|
|
+ List<VppSite> sites = listSites(tenantId);
|
|
|
+ List<VppResourcePoint> resources = listResources(tenantId);
|
|
|
+ List<VppDevice> devices = listDevices(tenantId);
|
|
|
+ List<VppDrEvent> endedEvents = listEndedEvents(tenantId);
|
|
|
+ Map<Long, VppDrEvaluation> evaluationMap = loadEvaluationMap(endedEvents);
|
|
|
+ List<VppDrInvitation> endedInvitations = listInvitationsByEvents(endedEvents, tenantId);
|
|
|
+ Map<Long, List<VppDrInvitation>> invitationsByEvent = endedInvitations.stream()
|
|
|
+ .filter(i -> i.getDrEventId() != null)
|
|
|
+ .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));
|
|
|
+
|
|
|
+ fillAccess(vo.getAccess(), sites, resources);
|
|
|
+ fillBasic(vo.getBasic(), tenantId, resources, endedEvents, evaluationMap, invitationsByEvent);
|
|
|
+ fillDeviceStatus(vo.getDeviceStatus(), devices);
|
|
|
+ fillSubsidy(vo.getSubsidy(), evaluationMap);
|
|
|
+ fillYearRegulation(vo.getYearRegulation(), tenantId, endedEvents, evaluationMap, invitationsByEvent);
|
|
|
+ fillSocialBenefit(vo.getSocialBenefit(), endedEvents, evaluationMap, invitationsByEvent);
|
|
|
+ fillEarningsRank(vo, sites, resources, endedEvents, evaluationMap, endedInvitations);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DashboardCurveVO getCurve(String date) {
|
|
|
+ CapabilityLoadCurveVO source = capabilityEvalService.getLoadCurve(null, date);
|
|
|
+ DashboardCurveVO curve = new DashboardCurveVO();
|
|
|
+ curve.setStatDate(source.getDate());
|
|
|
+ curve.setDataSource(source.getDataSource());
|
|
|
+ if (!CollectionUtils.isEmpty(source.getPoints())) {
|
|
|
+ List<DashboardCurveVO.Point> points = new ArrayList<>(source.getPoints().size());
|
|
|
+ for (CapabilityLoadPointVO p : source.getPoints()) {
|
|
|
+ DashboardCurveVO.Point point = new DashboardCurveVO.Point();
|
|
|
+ point.setTime(p.getTime());
|
|
|
+ point.setBaselineKw(p.getBaselineLoadKw());
|
|
|
+ point.setLoadKw(p.getActualLoadKw());
|
|
|
+ points.add(point);
|
|
|
+ }
|
|
|
+ curve.setPoints(points);
|
|
|
+ }
|
|
|
+ return curve;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DashboardMapVO getMap() {
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ List<VppSite> sites = listSites(tenantId);
|
|
|
+ List<VppResourcePoint> resources = listResources(tenantId);
|
|
|
+ Map<Long, BigDecimal> capacityBySite = new HashMap<>();
|
|
|
+ for (VppResourcePoint resource : resources) {
|
|
|
+ if (resource.getSiteId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ BigDecimal cap = nz(resource.getCapacityKw());
|
|
|
+ capacityBySite.merge(resource.getSiteId(), cap, BigDecimal::add);
|
|
|
+ }
|
|
|
+
|
|
|
+ DashboardMapVO map = new DashboardMapVO();
|
|
|
+ for (VppSite site : sites) {
|
|
|
+ if (site.getLongitude() == null || site.getLatitude() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ DashboardMapVO.SitePoint point = new DashboardMapVO.SitePoint();
|
|
|
+ point.setSiteId(site.getId());
|
|
|
+ point.setSiteName(site.getSiteName());
|
|
|
+ point.setLongitude(site.getLongitude());
|
|
|
+ point.setLatitude(site.getLatitude());
|
|
|
+ point.setProvince(site.getProvince());
|
|
|
+ point.setCity(site.getCity());
|
|
|
+ point.setCapacityKw(capacityBySite.getOrDefault(site.getId(), BigDecimal.ZERO)
|
|
|
+ .setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ map.getSites().add(point);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- access / basic / device ----------
|
|
|
+
|
|
|
+ private void fillAccess(DashboardSummaryVO.Access access, List<VppSite> sites, List<VppResourcePoint> resources) {
|
|
|
+ access.setSiteCount((long) sites.size());
|
|
|
+ BigDecimal total = BigDecimal.ZERO;
|
|
|
+ BigDecimal pv = BigDecimal.ZERO;
|
|
|
+ BigDecimal ess = BigDecimal.ZERO;
|
|
|
+ BigDecimal adjustable = BigDecimal.ZERO;
|
|
|
+ for (VppResourcePoint resource : resources) {
|
|
|
+ BigDecimal cap = nz(resource.getCapacityKw());
|
|
|
+ total = total.add(cap);
|
|
|
+ String category = VppCapabilityEvalHelper.resolveCategory(resource.getResourceType());
|
|
|
+ if (VppCapabilityEvalHelper.CATEGORY_GENERATION.equals(category)) {
|
|
|
+ pv = pv.add(cap);
|
|
|
+ } else if (VppCapabilityEvalHelper.CATEGORY_STORAGE.equals(category)) {
|
|
|
+ ess = ess.add(cap);
|
|
|
+ }
|
|
|
+ if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
|
|
|
+ adjustable = adjustable.add(cap);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ access.setTotalCapacityKw(total.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ access.setPvCapacityKw(pv.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ access.setEssCapacityKw(ess.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ access.setAdjustableCapacityKw(adjustable.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillBasic(DashboardSummaryVO.Basic basic, Integer tenantId,
|
|
|
+ List<VppResourcePoint> resources,
|
|
|
+ List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
|
|
|
+ Map<Long, List<VppDrInvitation>> invitationsByEvent) {
|
|
|
+ long customerCount = customerMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<VppCustomer>()
|
|
|
+ .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppCustomer::getTenantId, tenantId));
|
|
|
+ basic.setCustomerCount(customerCount);
|
|
|
+
|
|
|
+ BigDecimal loadCapacity = BigDecimal.ZERO;
|
|
|
+ Set<Long> controlDeviceIds = new HashSet<>();
|
|
|
+ long controlResourceWithoutDevice = 0L;
|
|
|
+ BigDecimal maxUp = BigDecimal.ZERO;
|
|
|
+ BigDecimal maxDown = BigDecimal.ZERO;
|
|
|
+ for (VppResourcePoint resource : resources) {
|
|
|
+ if (VppCapabilityEvalHelper.CATEGORY_LOAD.equals(
|
|
|
+ VppCapabilityEvalHelper.resolveCategory(resource.getResourceType()))) {
|
|
|
+ loadCapacity = loadCapacity.add(nz(resource.getCapacityKw()));
|
|
|
+ }
|
|
|
+ if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
|
|
|
+ if (resource.getDeviceId() != null) {
|
|
|
+ controlDeviceIds.add(resource.getDeviceId());
|
|
|
+ } else {
|
|
|
+ controlResourceWithoutDevice++;
|
|
|
+ }
|
|
|
+ maxUp = maxUp.add(nz(resource.getMaxUpKw()));
|
|
|
+ maxDown = maxDown.add(nz(resource.getMinDownKw()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ basic.setLoadCapacityKw(loadCapacity.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ basic.setAdjustableDeviceCount(controlDeviceIds.size() + controlResourceWithoutDevice);
|
|
|
+
|
|
|
+ BigDecimal peak = BigDecimal.ZERO;
|
|
|
+ BigDecimal valley = BigDecimal.ZERO;
|
|
|
+ for (VppDrEvent event : endedEvents) {
|
|
|
+ BigDecimal responseKw = resolveEventActualResponseKw(
|
|
|
+ event, evaluationMap.get(event.getId()), invitationsByEvent.get(event.getId()));
|
|
|
+ if (Objects.equals(event.getEventType(), EVENT_TYPE_PEAK)) {
|
|
|
+ peak = peak.add(responseKw);
|
|
|
+ } else if (Objects.equals(event.getEventType(), EVENT_TYPE_VALLEY)) {
|
|
|
+ valley = valley.add(responseKw);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ basic.setPeakShaveTotalKw(peak.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ basic.setValleyFillTotalKw(valley.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+
|
|
|
+ CapabilityEvalSummaryVO capability = capabilityEvalService.getSummary(null);
|
|
|
+ if (capability != null) {
|
|
|
+ basic.setMaxUpKw(nz(capability.getMaxUpKw()).setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ basic.setMaxDownKw(nz(capability.getMaxDownKw()).setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ basic.setMaxUpKw(maxUp.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ basic.setMaxDownKw(maxDown.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillDeviceStatus(DashboardSummaryVO.DeviceStatus status, List<VppDevice> devices) {
|
|
|
+ long online = 0;
|
|
|
+ long offline = 0;
|
|
|
+ long fault = 0;
|
|
|
+ for (VppDevice device : devices) {
|
|
|
+ if (Objects.equals(device.getRunStatus(), RUN_FAULT)) {
|
|
|
+ fault++;
|
|
|
+ } else if (Objects.equals(device.getCommStatus(), COMM_OFFLINE)
|
|
|
+ || device.getCommStatus() == null) {
|
|
|
+ offline++;
|
|
|
+ } else if (Objects.equals(device.getCommStatus(), COMM_ONLINE)) {
|
|
|
+ online++;
|
|
|
+ } else {
|
|
|
+ offline++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ status.setTotal((long) devices.size());
|
|
|
+ status.setOnline(online);
|
|
|
+ status.setOffline(offline);
|
|
|
+ status.setFault(fault);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillSubsidy(DashboardSummaryVO.Subsidy subsidy, Map<Long, VppDrEvaluation> evaluationMap) {
|
|
|
+ BigDecimal total = BigDecimal.ZERO;
|
|
|
+ for (VppDrEvaluation evaluation : evaluationMap.values()) {
|
|
|
+ total = total.add(nz(evaluation.getSubsidyAmount()));
|
|
|
+ }
|
|
|
+ subsidy.setTotalAmount(total.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillYearRegulation(DashboardSummaryVO.YearRegulation yearRegulation, Integer tenantId,
|
|
|
+ List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
|
|
|
+ Map<Long, List<VppDrInvitation>> invitationsByEvent) {
|
|
|
+ int year = Year.now().getValue();
|
|
|
+ yearRegulation.setYear(year);
|
|
|
+ LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
|
|
|
+ LocalDateTime yearEnd = LocalDate.of(year, 12, 31).atTime(23, 59, 59);
|
|
|
+
|
|
|
+ List<VppDrEvent> yearEvents = endedEvents.stream()
|
|
|
+ .filter(e -> e.getStartTime() != null
|
|
|
+ && !e.getStartTime().isBefore(yearStart)
|
|
|
+ && !e.getStartTime().isAfter(yearEnd))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ Set<Long> yearEventIds = yearEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
|
|
|
+
|
|
|
+ List<VppDrInvitation> yearInvitations = yearEventIds.isEmpty()
|
|
|
+ ? Collections.emptyList()
|
|
|
+ : yearEventIds.stream()
|
|
|
+ .flatMap(id -> invitationsByEvent.getOrDefault(id, Collections.emptyList()).stream())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ List<BigDecimal> deviationRates = new ArrayList<>();
|
|
|
+ List<BigDecimal> completionRates = new ArrayList<>();
|
|
|
+ BigDecimal effectiveKw = BigDecimal.ZERO;
|
|
|
+ BigDecimal totalClearedKw = BigDecimal.ZERO;
|
|
|
+ BigDecimal durationHour = BigDecimal.ZERO;
|
|
|
+ boolean anyWinning = yearInvitations.stream()
|
|
|
+ .anyMatch(i -> Objects.equals(i.getIsWinningBid(), IS_WINNING));
|
|
|
+
|
|
|
+ for (VppDrInvitation invitation : yearInvitations) {
|
|
|
+ BigDecimal actual = resolveInvitationActualResponseKw(invitation);
|
|
|
+ BigDecimal cleared = nz(invitation.getActualClearedCapacityKw());
|
|
|
+ BigDecimal inviteScale = nz(invitation.getDemandCapacityKw());
|
|
|
+
|
|
|
+ if (cleared.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ BigDecimal absDiff = actual.subtract(cleared).abs();
|
|
|
+ BigDecimal rate = BigDecimal.ONE.subtract(
|
|
|
+ absDiff.divide(cleared, SCALE, RoundingMode.HALF_UP));
|
|
|
+ if (rate.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
+ rate = BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ deviationRates.add(rate);
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal completion = invitation.getResponseCompletionRate();
|
|
|
+ if (completion == null && inviteScale.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ completion = actual.divide(inviteScale, SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ if (completion != null) {
|
|
|
+ completionRates.add(completion);
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean countAsEffective = anyWinning
|
|
|
+ ? Objects.equals(invitation.getIsWinningBid(), IS_WINNING)
|
|
|
+ : actual.compareTo(BigDecimal.ZERO) > 0;
|
|
|
+ if (countAsEffective) {
|
|
|
+ effectiveKw = effectiveKw.add(actual);
|
|
|
+ }
|
|
|
+ totalClearedKw = totalClearedKw.add(cleared.compareTo(BigDecimal.ZERO) > 0 ? cleared : inviteScale);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (VppDrEvent event : yearEvents) {
|
|
|
+ durationHour = durationHour.add(resolveDurationHour(event, evaluationMap.get(event.getId())));
|
|
|
+ // 无邀约时回退事件出清规模
|
|
|
+ if (!invitationsByEvent.containsKey(event.getId())
|
|
|
+ || invitationsByEvent.get(event.getId()).isEmpty()) {
|
|
|
+ totalClearedKw = totalClearedKw.add(nz(event.getClearedCapacityKw()).compareTo(BigDecimal.ZERO) > 0
|
|
|
+ ? nz(event.getClearedCapacityKw())
|
|
|
+ : nz(event.getTargetCapacityKw()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ yearRegulation.setExecutionDeviationRate(avg(deviationRates));
|
|
|
+ yearRegulation.setResponseCompletionRate(avg(completionRates));
|
|
|
+ yearRegulation.setEffectiveCapacityMw(toMw(effectiveKw));
|
|
|
+ yearRegulation.setTotalCapacityMw(toMw(totalClearedKw));
|
|
|
+ yearRegulation.setTotalDurationHour(durationHour.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ yearRegulation.setInvitationCount((long) yearInvitations.size());
|
|
|
+ yearRegulation.setParticipationCount(yearInvitations.stream()
|
|
|
+ .filter(i -> Objects.equals(i.getReplyStatus(), REPLY_ACCEPT))
|
|
|
+ .count());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillSocialBenefit(DashboardSummaryVO.SocialBenefit socialBenefit,
|
|
|
+ List<VppDrEvent> endedEvents,
|
|
|
+ Map<Long, VppDrEvaluation> evaluationMap,
|
|
|
+ Map<Long, List<VppDrInvitation>> invitationsByEvent) {
|
|
|
+ BigDecimal responseMwh = BigDecimal.ZERO;
|
|
|
+ for (VppDrEvent event : endedEvents) {
|
|
|
+ VppDrEvaluation evaluation = evaluationMap.get(event.getId());
|
|
|
+ BigDecimal kw = resolveEventActualResponseKw(event, evaluation, invitationsByEvent.get(event.getId()));
|
|
|
+ BigDecimal hours = resolveDurationHour(event, evaluation);
|
|
|
+ responseMwh = responseMwh.add(kw.multiply(hours).divide(KW_PER_MW, 8, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal carbon = responseMwh.multiply(VppSocialBenefitConstants.CARBON_TON_PER_MWH)
|
|
|
+ .setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal coal = responseMwh.multiply(VppSocialBenefitConstants.COAL_TON_PER_MWH)
|
|
|
+ .setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ long trees = carbon.multiply(VppSocialBenefitConstants.TREES_PER_CARBON_TON)
|
|
|
+ .setScale(0, RoundingMode.HALF_UP).longValue();
|
|
|
+ long greenCert = responseMwh.multiply(VppSocialBenefitConstants.GREEN_CERT_PER_MWH)
|
|
|
+ .setScale(0, RoundingMode.HALF_UP).longValue();
|
|
|
+
|
|
|
+ socialBenefit.setCarbonReductionTon(carbon);
|
|
|
+ socialBenefit.setCoalSavedTon(coal);
|
|
|
+ socialBenefit.setEquivalentTrees(trees);
|
|
|
+ socialBenefit.setEquivalentGreenCert(greenCert);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillEarningsRank(DashboardSummaryVO vo, List<VppSite> sites,
|
|
|
+ List<VppResourcePoint> resources,
|
|
|
+ List<VppDrEvent> endedEvents,
|
|
|
+ Map<Long, VppDrEvaluation> evaluationMap,
|
|
|
+ List<VppDrInvitation> endedInvitations) {
|
|
|
+ if (endedEvents.isEmpty()) {
|
|
|
+ vo.setEarningsRank(emptyRank(sites));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<Long> eventIds = endedEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
|
|
|
+ Map<Long, Long> resourceSiteMap = resources.stream()
|
|
|
+ .filter(r -> r.getId() != null && r.getSiteId() != null)
|
|
|
+ .collect(Collectors.toMap(VppResourcePoint::getId, VppResourcePoint::getSiteId, (a, b) -> a));
|
|
|
+
|
|
|
+ List<VppDrParticipation> participations = participationMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<VppDrParticipation>()
|
|
|
+ .in(VppDrParticipation::getEventId, eventIds)
|
|
|
+ .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
|
|
|
+
|
|
|
+ Map<Long, Set<Long>> eventSiteIds = new HashMap<>();
|
|
|
+ for (VppDrParticipation participation : participations) {
|
|
|
+ Long siteId = resourceSiteMap.get(participation.getResourceId());
|
|
|
+ if (siteId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ eventSiteIds.computeIfAbsent(participation.getEventId(), k -> new LinkedHashSet<>()).add(siteId);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (VppDrInvitation invitation : endedInvitations) {
|
|
|
+ if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ eventSiteIds.computeIfAbsent(invitation.getDrEventId(), k -> new LinkedHashSet<>())
|
|
|
+ .add(invitation.getSiteId());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, BigDecimal> earningsBySite = new HashMap<>();
|
|
|
+ for (VppSite site : sites) {
|
|
|
+ earningsBySite.put(site.getId(), BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+ for (VppDrEvent event : endedEvents) {
|
|
|
+ VppDrEvaluation evaluation = evaluationMap.get(event.getId());
|
|
|
+ BigDecimal amount = evaluation != null ? nz(evaluation.getSubsidyAmount()) : BigDecimal.ZERO;
|
|
|
+ if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Set<Long> siteIds = eventSiteIds.getOrDefault(event.getId(), Collections.emptySet());
|
|
|
+ if (siteIds.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ BigDecimal share = amount.divide(BigDecimal.valueOf(siteIds.size()), 8, RoundingMode.HALF_UP);
|
|
|
+ for (Long siteId : siteIds) {
|
|
|
+ earningsBySite.merge(siteId, share, BigDecimal::add);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, String> siteNameMap = sites.stream()
|
|
|
+ .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
|
|
|
+ List<DashboardSummaryVO.EarningsRankItem> rank = earningsBySite.entrySet().stream()
|
|
|
+ .map(e -> {
|
|
|
+ DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
|
|
|
+ item.setSiteId(e.getKey());
|
|
|
+ item.setSiteName(siteNameMap.getOrDefault(e.getKey(), String.valueOf(e.getKey())));
|
|
|
+ item.setEarnings(e.getValue().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
|
+ return item;
|
|
|
+ })
|
|
|
+ .sorted(Comparator.comparing(DashboardSummaryVO.EarningsRankItem::getEarnings).reversed())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ vo.setEarningsRank(rank);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<DashboardSummaryVO.EarningsRankItem> emptyRank(List<VppSite> sites) {
|
|
|
+ return sites.stream().map(site -> {
|
|
|
+ DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
|
|
|
+ item.setSiteId(site.getId());
|
|
|
+ item.setSiteName(site.getSiteName());
|
|
|
+ item.setEarnings(BigDecimal.ZERO.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
|
|
+ return item;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- data loaders ----------
|
|
|
+
|
|
|
+ private List<VppSite> listSites(Integer tenantId) {
|
|
|
+ return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
|
|
|
+ .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppSite::getTenantId, tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppResourcePoint> listResources(Integer tenantId) {
|
|
|
+ return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
|
|
|
+ .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppDevice> listDevices(Integer tenantId) {
|
|
|
+ return deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
|
|
|
+ .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDevice::getTenantId, tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppDrEvent> listEndedEvents(Integer tenantId) {
|
|
|
+ return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
|
|
|
+ .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
|
|
|
+ .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppDrInvitation> listInvitationsByEvents(List<VppDrEvent> events, Integer tenantId) {
|
|
|
+ if (events.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
|
|
|
+ return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
|
|
|
+ .in(VppDrInvitation::getDrEventId, eventIds)
|
|
|
+ .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, VppDrEvaluation> loadEvaluationMap(List<VppDrEvent> events) {
|
|
|
+ if (events.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
|
|
|
+ return evaluationMapper.selectList(new LambdaQueryWrapper<VppDrEvaluation>()
|
|
|
+ .in(VppDrEvaluation::getEventId, eventIds)
|
|
|
+ .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED))
|
|
|
+ .stream()
|
|
|
+ .collect(Collectors.toMap(VppDrEvaluation::getEventId, e -> e, (a, b) -> a));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- helpers ----------
|
|
|
+
|
|
|
+ private static BigDecimal resolveEventActualResponseKw(VppDrEvent event, VppDrEvaluation evaluation,
|
|
|
+ List<VppDrInvitation> invitations) {
|
|
|
+ if (!CollectionUtils.isEmpty(invitations)) {
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ boolean any = false;
|
|
|
+ for (VppDrInvitation invitation : invitations) {
|
|
|
+ BigDecimal kw = resolveInvitationActualResponseKw(invitation);
|
|
|
+ if (kw.compareTo(BigDecimal.ZERO) > 0
|
|
|
+ || invitation.getActualResponseCapacityKw() != null
|
|
|
+ || invitation.getEstimatedResponseCapacityKw() != null) {
|
|
|
+ sum = sum.add(kw);
|
|
|
+ any = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (any) {
|
|
|
+ return sum;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (evaluation != null && evaluation.getActualCapacityKw() != null) {
|
|
|
+ return nz(evaluation.getActualCapacityKw());
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal resolveInvitationActualResponseKw(VppDrInvitation invitation) {
|
|
|
+ if (invitation.getActualResponseCapacityKw() != null) {
|
|
|
+ return nz(invitation.getActualResponseCapacityKw());
|
|
|
+ }
|
|
|
+ if (invitation.getEstimatedResponseCapacityKw() != null) {
|
|
|
+ return nz(invitation.getEstimatedResponseCapacityKw());
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal resolveDurationHour(VppDrEvent event, VppDrEvaluation evaluation) {
|
|
|
+ if (evaluation != null && evaluation.getResponseDurationMin() != null
|
|
|
+ && evaluation.getResponseDurationMin() > 0) {
|
|
|
+ return BigDecimal.valueOf(evaluation.getResponseDurationMin())
|
|
|
+ .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ if (event.getStartTime() != null && event.getEndTime() != null
|
|
|
+ && event.getEndTime().isAfter(event.getStartTime())) {
|
|
|
+ long minutes = ChronoUnit.MINUTES.between(event.getStartTime(), event.getEndTime());
|
|
|
+ if (minutes > 0) {
|
|
|
+ return BigDecimal.valueOf(minutes)
|
|
|
+ .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal avg(List<BigDecimal> values) {
|
|
|
+ if (values == null || values.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ for (BigDecimal value : values) {
|
|
|
+ sum = sum.add(value);
|
|
|
+ }
|
|
|
+ return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal toMw(BigDecimal kw) {
|
|
|
+ return nz(kw).divide(KW_PER_MW, SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal nz(BigDecimal value) {
|
|
|
+ return value == null ? BigDecimal.ZERO : value;
|
|
|
+ }
|
|
|
}
|