|
|
@@ -0,0 +1,847 @@
|
|
|
+package com.usky.vpp.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+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.constant.VppDrEventType;
|
|
|
+import com.usky.vpp.constant.VppTsdbConstants;
|
|
|
+import com.usky.vpp.domain.DmpDevice;
|
|
|
+import com.usky.vpp.domain.DmpProductAttribute;
|
|
|
+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.VppSite;
|
|
|
+import com.usky.vpp.mapper.DmpDeviceMapper;
|
|
|
+import com.usky.vpp.mapper.DmpProductAttributeMapper;
|
|
|
+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.VppSiteMapper;
|
|
|
+import com.usky.vpp.service.VppEnergyStatService;
|
|
|
+import com.usky.vpp.service.VppTsdbQueryService;
|
|
|
+import com.usky.vpp.service.vo.EnergyStatItemVO;
|
|
|
+import com.usky.vpp.service.vo.EnergyStatPageVO;
|
|
|
+import com.usky.vpp.service.vo.EnergyStatSummaryVO;
|
|
|
+import com.usky.vpp.service.vo.MeterDataVO;
|
|
|
+import com.usky.vpp.util.VppAuditHelper;
|
|
|
+import com.usky.vpp.util.VppEnergyUsageHelper;
|
|
|
+import org.apache.poi.ss.usermodel.FillPatternType;
|
|
|
+import org.apache.poi.ss.usermodel.IndexedColors;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFCell;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFFont;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFRow;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.IOException;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.TreeMap;
|
|
|
+import java.util.TreeSet;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 能源统计:设备用电量来自 TSDB 累计电能差值;削峰/填谷按站点邀约实际响应量×时长分摊到设备。
|
|
|
+ * 电表详情:vpp_device.uuid → dmp_device.product_id → dmp_product_attribute.attribute_code → TSDB 历史。
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class VppEnergyStatServiceImpl implements VppEnergyStatService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(VppEnergyStatServiceImpl.class);
|
|
|
+ private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern(VppTsdbConstants.TIME_FORMAT);
|
|
|
+ private static final int SCALE = 4;
|
|
|
+ private static final int DEFAULT_CURRENT = 1;
|
|
|
+ private static final int DEFAULT_SIZE = 20;
|
|
|
+ private static final int MAX_SIZE = 100;
|
|
|
+ private static final List<String> POWER_METRIC_PRIORITY = buildPowerMetricPriority();
|
|
|
+ private static final List<String> ENERGY_METRICS = Collections.singletonList(VppTsdbConstants.METRIC_EPP);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VppDeviceMapper deviceMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppSiteMapper siteMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppCustomerMapper customerMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrEventMapper eventMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrInvitationMapper invitationMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppDrEvaluationMapper evaluationMapper;
|
|
|
+ @Autowired
|
|
|
+ private DmpDeviceMapper dmpDeviceMapper;
|
|
|
+ @Autowired
|
|
|
+ private DmpProductAttributeMapper dmpProductAttributeMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppTsdbQueryService tsdbQueryService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EnergyStatPageVO pageEnergy(String customerName, String siteName, String deviceName,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime,
|
|
|
+ Integer current, Integer size) {
|
|
|
+ LocalDateTime[] range = resolveTimeRange(startTime, endTime);
|
|
|
+ List<EnergyStatItemVO> items = listEnergyItems(customerName, siteName, deviceName, range[0], range[1]);
|
|
|
+ int pageNum = current == null || current < 1 ? DEFAULT_CURRENT : current;
|
|
|
+ int pageSize = size == null || size < 1 ? DEFAULT_SIZE : Math.min(size, MAX_SIZE);
|
|
|
+
|
|
|
+ EnergyStatPageVO result = new EnergyStatPageVO();
|
|
|
+ result.setSummary(buildSummary(items));
|
|
|
+ result.setPage(paginate(items, pageNum, pageSize));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exportEnergy(String customerName, String siteName, String deviceName,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime,
|
|
|
+ String fileName, HttpServletResponse response) {
|
|
|
+ LocalDateTime[] range = resolveTimeRange(startTime, endTime);
|
|
|
+ List<EnergyStatItemVO> items = listEnergyItems(customerName, siteName, deviceName, range[0], range[1]);
|
|
|
+ String name = StringUtils.hasText(fileName) ? fileName.trim() : "能源统计";
|
|
|
+ writeEnergyExcel(name, items, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public MeterDataVO getMeterData(Long deviceId, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ LocalDateTime[] range = resolveTimeRange(startTime, endTime);
|
|
|
+ return loadMeterData(requireDevice(deviceId), range[0], range[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exportMeterData(Long deviceId, LocalDateTime startTime, LocalDateTime endTime,
|
|
|
+ String fileName, HttpServletResponse response) {
|
|
|
+ MeterDataVO meterData = getMeterData(deviceId, startTime, endTime);
|
|
|
+ String name = StringUtils.hasText(fileName)
|
|
|
+ ? fileName.trim()
|
|
|
+ : (StringUtils.hasText(meterData.getDeviceName())
|
|
|
+ ? meterData.getDeviceName() + "-电表数据" : "电表数据");
|
|
|
+ writeMeterExcel(name, meterData, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 能源统计 ----------
|
|
|
+
|
|
|
+ private List<EnergyStatItemVO> listEnergyItems(String customerName, String siteName, String deviceName,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<VppDevice> devices = listMatchingDevices(customerName, siteName, deviceName);
|
|
|
+ if (devices.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, VppSite> siteMap = loadSiteMap(devices);
|
|
|
+ Map<Long, VppCustomer> customerMap = loadCustomerMap(siteMap.values());
|
|
|
+ Map<Long, PeakValley> peakValleyMap = loadPeakValleyByDevice(devices, startTime, endTime);
|
|
|
+ Map<String, BigDecimal> energyByUuid = loadRegulatedEnergy(devices, startTime, endTime);
|
|
|
+
|
|
|
+ List<EnergyStatItemVO> items = new ArrayList<>(devices.size());
|
|
|
+ for (VppDevice device : devices) {
|
|
|
+ VppSite site = device.getSiteId() == null ? null : siteMap.get(device.getSiteId());
|
|
|
+ VppCustomer customer = site == null || site.getCustomerId() == null
|
|
|
+ ? null : customerMap.get(site.getCustomerId());
|
|
|
+ PeakValley peakValley = peakValleyMap.getOrDefault(device.getId(), new PeakValley());
|
|
|
+ EnergyStatItemVO item = new EnergyStatItemVO();
|
|
|
+ item.setDeviceId(device.getId());
|
|
|
+ item.setDeviceUuid(device.getDeviceUuid());
|
|
|
+ item.setDeviceName(device.getDeviceName());
|
|
|
+ item.setSiteId(device.getSiteId());
|
|
|
+ item.setSiteName(site == null ? null : site.getSiteName());
|
|
|
+ item.setCustomerId(customer == null ? null : customer.getId());
|
|
|
+ item.setCustomerName(customer == null ? null : customer.getCustomerName());
|
|
|
+ item.setRegulatedEnergyKwh(nz(energyByUuid.get(trimUuid(device.getDeviceUuid()))));
|
|
|
+ item.setPeakShaveKwh(peakValley.peak);
|
|
|
+ item.setValleyFillKwh(peakValley.valley);
|
|
|
+ items.add(item);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VppDevice> listMatchingDevices(String customerName, String siteName, String deviceName) {
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ Set<Long> siteIds = resolveSiteIds(tenantId, customerName, siteName);
|
|
|
+ if (siteIds != null && siteIds.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<VppDevice> wrapper = new LambdaQueryWrapper<VppDevice>()
|
|
|
+ .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDevice::getTenantId, tenantId)
|
|
|
+ .in(siteIds != null, VppDevice::getSiteId, siteIds)
|
|
|
+ .orderByDesc(VppDevice::getCreateTime);
|
|
|
+ if (StringUtils.hasText(deviceName)) {
|
|
|
+ wrapper.like(VppDevice::getDeviceName, deviceName.trim());
|
|
|
+ }
|
|
|
+ return deviceMapper.selectList(wrapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return null 表示不按站点过滤;empty 表示无匹配站点
|
|
|
+ */
|
|
|
+ private Set<Long> resolveSiteIds(Integer tenantId, String customerName, String siteName) {
|
|
|
+ boolean filterCustomer = StringUtils.hasText(customerName);
|
|
|
+ boolean filterSite = StringUtils.hasText(siteName);
|
|
|
+ if (!filterCustomer && !filterSite) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Set<Long> customerIds = null;
|
|
|
+ if (filterCustomer) {
|
|
|
+ customerIds = customerMapper.selectList(new LambdaQueryWrapper<VppCustomer>()
|
|
|
+ .select(VppCustomer::getId)
|
|
|
+ .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppCustomer::getTenantId, tenantId)
|
|
|
+ .like(VppCustomer::getCustomerName, customerName.trim()))
|
|
|
+ .stream()
|
|
|
+ .map(VppCustomer::getId)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (customerIds.isEmpty()) {
|
|
|
+ return Collections.emptySet();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<VppSite> siteWrapper = new LambdaQueryWrapper<VppSite>()
|
|
|
+ .select(VppSite::getId)
|
|
|
+ .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppSite::getTenantId, tenantId)
|
|
|
+ .in(customerIds != null, VppSite::getCustomerId, customerIds);
|
|
|
+ if (filterSite) {
|
|
|
+ siteWrapper.like(VppSite::getSiteName, siteName.trim());
|
|
|
+ }
|
|
|
+ List<VppSite> sites = siteMapper.selectList(siteWrapper);
|
|
|
+ return sites.stream().map(VppSite::getId).collect(Collectors.toSet());
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, VppSite> loadSiteMap(List<VppDevice> devices) {
|
|
|
+ Set<Long> siteIds = devices.stream()
|
|
|
+ .map(VppDevice::getSiteId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (siteIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ return siteMapper.selectBatchIds(siteIds).stream()
|
|
|
+ .filter(site -> !VppAuditHelper.isDeleted(site.getDeleteFlag()))
|
|
|
+ .collect(Collectors.toMap(VppSite::getId, site -> site, (a, b) -> a));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, VppCustomer> loadCustomerMap(java.util.Collection<VppSite> sites) {
|
|
|
+ Set<Long> customerIds = sites.stream()
|
|
|
+ .map(VppSite::getCustomerId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (customerIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ return customerMapper.selectBatchIds(customerIds).stream()
|
|
|
+ .filter(customer -> !VppAuditHelper.isDeleted(customer.getDeleteFlag()))
|
|
|
+ .collect(Collectors.toMap(VppCustomer::getId, customer -> customer, (a, b) -> a));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, BigDecimal> loadRegulatedEnergy(List<VppDevice> devices,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<String> uuids = devices.stream()
|
|
|
+ .map(VppDevice::getDeviceUuid)
|
|
|
+ .map(this::trimUuid)
|
|
|
+ .filter(StringUtils::hasText)
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (uuids.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history =
|
|
|
+ tsdbQueryService.queryDeviceMetricHistory(uuids, startTime, endTime, ENERGY_METRICS);
|
|
|
+ Map<String, BigDecimal> result = new HashMap<>();
|
|
|
+ LocalDateTime endExclusive = endTime.plusSeconds(1);
|
|
|
+ for (String uuid : uuids) {
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap = history.get(uuid);
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> series = pickMetricSeries(metricMap, VppTsdbConstants.METRIC_EPP);
|
|
|
+ result.put(uuid, nz(VppEnergyUsageHelper.calcUsage(series, startTime, endExclusive)));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按站点邀约实际响应量 × 事件时长得到电量,再按设备额定功率分摊。
|
|
|
+ */
|
|
|
+ private Map<Long, PeakValley> loadPeakValleyByDevice(List<VppDevice> filteredDevices,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ Set<Long> siteIds = filteredDevices.stream()
|
|
|
+ .map(VppDevice::getSiteId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (siteIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Integer tenantId = SecurityUtils.getTenantId();
|
|
|
+ List<VppDrEvent> events = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
|
|
|
+ .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDrEvent::getTenantId, tenantId)
|
|
|
+ .ne(VppDrEvent::getEventStatus, VppDrEventStatus.CANCELLED)
|
|
|
+ .and(w -> w.isNull(VppDrEvent::getStartTime).or().le(VppDrEvent::getStartTime, endTime))
|
|
|
+ .and(w -> w.isNull(VppDrEvent::getEndTime).or().ge(VppDrEvent::getEndTime, startTime)));
|
|
|
+ if (events.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<Long, VppDrEvent> eventMap = events.stream()
|
|
|
+ .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
|
|
|
+ Set<Long> eventIds = eventMap.keySet();
|
|
|
+ List<VppDrInvitation> invitations = 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)
|
|
|
+ .and(w -> w.isNull(VppDrInvitation::getResponseStatus)
|
|
|
+ .or()
|
|
|
+ .ne(VppDrInvitation::getResponseStatus, VppDrEventStatus.CANCELLED)));
|
|
|
+ if (invitations.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<Long, VppDrEvaluation> evaluationMap = 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));
|
|
|
+
|
|
|
+ List<VppDevice> siteDevices = deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
|
|
|
+ .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(tenantId != null, VppDevice::getTenantId, tenantId)
|
|
|
+ .in(VppDevice::getSiteId, siteIds));
|
|
|
+ Map<Long, List<VppDevice>> devicesBySite = siteDevices.stream()
|
|
|
+ .filter(d -> d.getSiteId() != null)
|
|
|
+ .collect(Collectors.groupingBy(VppDevice::getSiteId));
|
|
|
+
|
|
|
+ Map<Long, PeakValley> acc = new HashMap<>();
|
|
|
+ for (VppDrInvitation invitation : invitations) {
|
|
|
+ VppDrEvent event = eventMap.get(invitation.getDrEventId());
|
|
|
+ if (event == null || !overlaps(event, invitation, startTime, endTime)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ BigDecimal energy = resolveInvitationEnergyKwh(invitation, event, evaluationMap.get(event.getId()));
|
|
|
+ if (energy.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<VppDevice> targets = devicesBySite.getOrDefault(invitation.getSiteId(), Collections.emptyList());
|
|
|
+ if (targets.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int eventType = resolveEventType(event, invitation);
|
|
|
+ Map<Long, BigDecimal> shares = allocateByRatedPower(targets, energy);
|
|
|
+ for (Map.Entry<Long, BigDecimal> entry : shares.entrySet()) {
|
|
|
+ PeakValley bucket = acc.computeIfAbsent(entry.getKey(), key -> new PeakValley());
|
|
|
+ if (eventType == VppDrEventType.VALLEY) {
|
|
|
+ bucket.valley = bucket.valley.add(entry.getValue());
|
|
|
+ } else {
|
|
|
+ bucket.peak = bucket.peak.add(entry.getValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (PeakValley value : acc.values()) {
|
|
|
+ value.peak = value.peak.setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ value.valley = value.valley.setScale(SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return acc;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Long, BigDecimal> allocateByRatedPower(List<VppDevice> devices, BigDecimal energy) {
|
|
|
+ Map<Long, BigDecimal> weights = new LinkedHashMap<>();
|
|
|
+ BigDecimal total = BigDecimal.ZERO;
|
|
|
+ for (VppDevice device : devices) {
|
|
|
+ BigDecimal weight = device.getRatedPowerKw() != null
|
|
|
+ && device.getRatedPowerKw().compareTo(BigDecimal.ZERO) > 0
|
|
|
+ ? device.getRatedPowerKw() : BigDecimal.ONE;
|
|
|
+ weights.put(device.getId(), weight);
|
|
|
+ total = total.add(weight);
|
|
|
+ }
|
|
|
+ Map<Long, BigDecimal> shares = new HashMap<>();
|
|
|
+ if (total.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return shares;
|
|
|
+ }
|
|
|
+ for (Map.Entry<Long, BigDecimal> entry : weights.entrySet()) {
|
|
|
+ shares.put(entry.getKey(),
|
|
|
+ energy.multiply(entry.getValue()).divide(total, 8, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+ return shares;
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal resolveInvitationEnergyKwh(VppDrInvitation invitation, VppDrEvent event,
|
|
|
+ VppDrEvaluation evaluation) {
|
|
|
+ BigDecimal kw = invitation.getActualResponseCapacityKw() != null
|
|
|
+ ? nz(invitation.getActualResponseCapacityKw())
|
|
|
+ : nz(invitation.getEstimatedResponseCapacityKw());
|
|
|
+ BigDecimal hours = resolveDurationHour(event, invitation, evaluation);
|
|
|
+ if (kw.compareTo(BigDecimal.ZERO) <= 0 || hours.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ return kw.multiply(hours);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal resolveDurationHour(VppDrEvent event, VppDrInvitation invitation,
|
|
|
+ VppDrEvaluation evaluation) {
|
|
|
+ if (evaluation != null && evaluation.getResponseDurationMin() != null
|
|
|
+ && evaluation.getResponseDurationMin() > 0) {
|
|
|
+ return BigDecimal.valueOf(evaluation.getResponseDurationMin())
|
|
|
+ .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ LocalDateTime start = event.getStartTime();
|
|
|
+ LocalDateTime end = event.getEndTime();
|
|
|
+ if (start == null && invitation.getExecuteStartDate() != null) {
|
|
|
+ start = invitation.getExecuteStartDate().atStartOfDay();
|
|
|
+ }
|
|
|
+ if (end == null && invitation.getExecuteEndDate() != null) {
|
|
|
+ end = invitation.getExecuteEndDate().atTime(23, 59, 59);
|
|
|
+ }
|
|
|
+ if (start != null && end != null && end.isAfter(start)) {
|
|
|
+ long minutes = ChronoUnit.MINUTES.between(start, end);
|
|
|
+ if (minutes > 0) {
|
|
|
+ return BigDecimal.valueOf(minutes)
|
|
|
+ .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean overlaps(VppDrEvent event, VppDrInvitation invitation,
|
|
|
+ LocalDateTime queryStart, LocalDateTime queryEnd) {
|
|
|
+ LocalDateTime start = event.getStartTime();
|
|
|
+ LocalDateTime end = event.getEndTime();
|
|
|
+ if (start == null && invitation.getExecuteStartDate() != null) {
|
|
|
+ start = invitation.getExecuteStartDate().atStartOfDay();
|
|
|
+ }
|
|
|
+ if (end == null && invitation.getExecuteEndDate() != null) {
|
|
|
+ end = invitation.getExecuteEndDate().atTime(23, 59, 59);
|
|
|
+ }
|
|
|
+ if (start == null || end == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return !start.isAfter(queryEnd) && !end.isBefore(queryStart);
|
|
|
+ }
|
|
|
+
|
|
|
+ private int resolveEventType(VppDrEvent event, VppDrInvitation invitation) {
|
|
|
+ if (event.getEventType() != null) {
|
|
|
+ return event.getEventType();
|
|
|
+ }
|
|
|
+ if (invitation.getTransactionType() != null) {
|
|
|
+ return invitation.getTransactionType();
|
|
|
+ }
|
|
|
+ return VppDrEventType.PEAK;
|
|
|
+ }
|
|
|
+
|
|
|
+ private EnergyStatSummaryVO buildSummary(List<EnergyStatItemVO> items) {
|
|
|
+ EnergyStatSummaryVO summary = new EnergyStatSummaryVO();
|
|
|
+ summary.setDeviceCount((long) items.size());
|
|
|
+ summary.setSiteCount(items.stream().map(EnergyStatItemVO::getSiteId).filter(Objects::nonNull).distinct().count());
|
|
|
+ BigDecimal regulated = BigDecimal.ZERO;
|
|
|
+ BigDecimal peak = BigDecimal.ZERO;
|
|
|
+ BigDecimal valley = BigDecimal.ZERO;
|
|
|
+ for (EnergyStatItemVO item : items) {
|
|
|
+ regulated = regulated.add(nz(item.getRegulatedEnergyKwh()));
|
|
|
+ peak = peak.add(nz(item.getPeakShaveKwh()));
|
|
|
+ valley = valley.add(nz(item.getValleyFillKwh()));
|
|
|
+ }
|
|
|
+ summary.setRegulatedEnergyKwh(regulated.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ summary.setPeakShaveKwh(peak.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ summary.setValleyFillKwh(valley.setScale(SCALE, RoundingMode.HALF_UP));
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPage<EnergyStatItemVO> paginate(List<EnergyStatItemVO> items, int current, int size) {
|
|
|
+ int from = Math.min((current - 1) * size, items.size());
|
|
|
+ int to = Math.min(from + size, items.size());
|
|
|
+ return new CommonPage<>(new ArrayList<>(items.subList(from, to)), (long) items.size(), current, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 电表详情 ----------
|
|
|
+
|
|
|
+ private VppDevice requireDevice(Long deviceId) {
|
|
|
+ if (deviceId == null) {
|
|
|
+ throw new BusinessException("设备ID不能为空");
|
|
|
+ }
|
|
|
+ VppDevice device = deviceMapper.selectById(deviceId);
|
|
|
+ if (device == null || VppAuditHelper.isDeleted(device.getDeleteFlag())) {
|
|
|
+ throw new BusinessException("设备不存在");
|
|
|
+ }
|
|
|
+ return device;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MeterDataVO loadMeterData(VppDevice device, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ MeterDataVO vo = new MeterDataVO();
|
|
|
+ vo.setDeviceId(device.getId());
|
|
|
+ vo.setDeviceUuid(device.getDeviceUuid());
|
|
|
+ vo.setDeviceName(device.getDeviceName());
|
|
|
+ vo.setSiteId(device.getSiteId());
|
|
|
+ fillMeterHeader(vo, device);
|
|
|
+
|
|
|
+ String uuid = trimUuid(device.getDeviceUuid());
|
|
|
+ if (!StringUtils.hasText(uuid)) {
|
|
|
+ throw new BusinessException("设备未绑定物联网 UUID,无法查询电表数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ DmpDevice dmpDevice = dmpDeviceMapper.selectOne(new LambdaQueryWrapper<DmpDevice>()
|
|
|
+ .eq(DmpDevice::getDeviceUuid, uuid)
|
|
|
+ .eq(DmpDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (dmpDevice == null || dmpDevice.getProductId() == null) {
|
|
|
+ throw new BusinessException("物联网设备不存在或未关联产品,无法查询电表数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<DmpProductAttribute> attributes = dmpProductAttributeMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<DmpProductAttribute>()
|
|
|
+ .eq(DmpProductAttribute::getProductId, dmpDevice.getProductId())
|
|
|
+ .eq(DmpProductAttribute::getDeleteFlag, VppAuditHelper.NOT_DELETED)
|
|
|
+ .eq(dmpDevice.getTenantId() != null, DmpProductAttribute::getTenantId, dmpDevice.getTenantId())
|
|
|
+ .isNotNull(DmpProductAttribute::getAttributeCode)
|
|
|
+ .ne(DmpProductAttribute::getAttributeCode, "")
|
|
|
+ .orderByAsc(DmpProductAttribute::getId));
|
|
|
+ attributes = attributes.stream()
|
|
|
+ .filter(attr -> StringUtils.hasText(attr.getAttributeCode())
|
|
|
+ && !"ts".equalsIgnoreCase(attr.getAttributeCode().trim()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ attributes = sortAttributes(attributes);
|
|
|
+
|
|
|
+ List<MeterDataVO.MeterColumnVO> columns = new ArrayList<>();
|
|
|
+ List<String> metrics = new ArrayList<>();
|
|
|
+ for (DmpProductAttribute attribute : attributes) {
|
|
|
+ String code = attribute.getAttributeCode().trim();
|
|
|
+ metrics.add(code);
|
|
|
+ MeterDataVO.MeterColumnVO column = new MeterDataVO.MeterColumnVO();
|
|
|
+ column.setAttributeCode(code);
|
|
|
+ column.setAttributeName(attribute.getAttributeName());
|
|
|
+ column.setAttributeUnit(attribute.getAttributeUnit());
|
|
|
+ column.setHeader(buildColumnHeader(code, attribute.getAttributeName(), attribute.getAttributeUnit()));
|
|
|
+ columns.add(column);
|
|
|
+ }
|
|
|
+ vo.setColumns(columns);
|
|
|
+
|
|
|
+ if (metrics.isEmpty()) {
|
|
|
+ log.warn("产品无可用属性, productId={}, deviceUuid={}", dmpDevice.getProductId(), uuid);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history =
|
|
|
+ tsdbQueryService.queryDeviceMetricHistory(
|
|
|
+ Collections.singletonList(uuid), startTime, endTime, metrics);
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap = history.getOrDefault(uuid, Collections.emptyMap());
|
|
|
+
|
|
|
+ String powerMetric = resolvePowerMetric(columns, metricMap);
|
|
|
+ vo.setPowerMetric(powerMetric);
|
|
|
+ if (powerMetric != null) {
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ if (powerMetric.equalsIgnoreCase(column.getAttributeCode())) {
|
|
|
+ vo.setPowerMetricName(column.getAttributeName());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> powerSeries = pickMetricSeries(metricMap, powerMetric);
|
|
|
+ if (powerSeries != null) {
|
|
|
+ List<MeterDataVO.MeterCurvePointVO> curve = new ArrayList<>();
|
|
|
+ for (Map.Entry<LocalDateTime, BigDecimal> point : powerSeries.entrySet()) {
|
|
|
+ MeterDataVO.MeterCurvePointVO curvePoint = new MeterDataVO.MeterCurvePointVO();
|
|
|
+ curvePoint.setTime(point.getKey().format(TIME_FMT));
|
|
|
+ curvePoint.setValue(point.getValue());
|
|
|
+ curve.add(curvePoint);
|
|
|
+ }
|
|
|
+ vo.setCurve(curve);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ vo.setRecords(buildMeterRecords(columns, metricMap));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillMeterHeader(MeterDataVO vo, VppDevice device) {
|
|
|
+ if (device.getSiteId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ VppSite site = siteMapper.selectById(device.getSiteId());
|
|
|
+ if (site == null || VppAuditHelper.isDeleted(site.getDeleteFlag())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ vo.setSiteName(site.getSiteName());
|
|
|
+ if (site.getCustomerId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ vo.setCustomerId(site.getCustomerId());
|
|
|
+ VppCustomer customer = customerMapper.selectById(site.getCustomerId());
|
|
|
+ if (customer != null && !VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
|
|
|
+ vo.setCustomerName(customer.getCustomerName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<MeterDataVO.MeterRecordVO> buildMeterRecords(List<MeterDataVO.MeterColumnVO> columns,
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap) {
|
|
|
+ TreeSet<LocalDateTime> timestamps = new TreeSet<>();
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> seriesByCode = new HashMap<>();
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> series = pickMetricSeries(metricMap, column.getAttributeCode());
|
|
|
+ if (series == null) {
|
|
|
+ series = new TreeMap<>();
|
|
|
+ }
|
|
|
+ seriesByCode.put(column.getAttributeCode(), series);
|
|
|
+ timestamps.addAll(series.keySet());
|
|
|
+ }
|
|
|
+ List<MeterDataVO.MeterRecordVO> records = new ArrayList<>();
|
|
|
+ for (LocalDateTime ts : timestamps) {
|
|
|
+ MeterDataVO.MeterRecordVO record = new MeterDataVO.MeterRecordVO();
|
|
|
+ record.setCollectTime(ts.format(TIME_FMT));
|
|
|
+ Map<String, BigDecimal> values = new LinkedHashMap<>();
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ values.put(column.getAttributeCode(), seriesByCode.get(column.getAttributeCode()).get(ts));
|
|
|
+ }
|
|
|
+ record.setValues(values);
|
|
|
+ records.add(record);
|
|
|
+ }
|
|
|
+ return records;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<DmpProductAttribute> sortAttributes(List<DmpProductAttribute> attributes) {
|
|
|
+ Map<String, Integer> order = new HashMap<>();
|
|
|
+ int idx = 0;
|
|
|
+ for (String code : POWER_METRIC_PRIORITY) {
|
|
|
+ order.put(code.toLowerCase(), idx++);
|
|
|
+ }
|
|
|
+ for (String code : Arrays.asList("epp", "ep", "energy", "eday", "ua", "u", "voltage", "ia", "i", "current")) {
|
|
|
+ order.putIfAbsent(code, idx++);
|
|
|
+ }
|
|
|
+ List<DmpProductAttribute> sorted = new ArrayList<>(attributes);
|
|
|
+ sorted.sort((a, b) -> {
|
|
|
+ int oa = order.getOrDefault(a.getAttributeCode().trim().toLowerCase(), Integer.MAX_VALUE);
|
|
|
+ int ob = order.getOrDefault(b.getAttributeCode().trim().toLowerCase(), Integer.MAX_VALUE);
|
|
|
+ if (oa != ob) {
|
|
|
+ return Integer.compare(oa, ob);
|
|
|
+ }
|
|
|
+ Integer ida = a.getId() == null ? Integer.MAX_VALUE : a.getId();
|
|
|
+ Integer idb = b.getId() == null ? Integer.MAX_VALUE : b.getId();
|
|
|
+ return ida.compareTo(idb);
|
|
|
+ });
|
|
|
+ return sorted;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolvePowerMetric(List<MeterDataVO.MeterColumnVO> columns,
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap) {
|
|
|
+ for (String candidate : POWER_METRIC_PRIORITY) {
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ if (candidate.equalsIgnoreCase(column.getAttributeCode())) {
|
|
|
+ return column.getAttributeCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ String name = column.getAttributeName() == null ? "" : column.getAttributeName();
|
|
|
+ if (name.contains("有功功率")) {
|
|
|
+ return column.getAttributeCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!CollectionUtils.isEmpty(metricMap)) {
|
|
|
+ for (MeterDataVO.MeterColumnVO column : columns) {
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> series = pickMetricSeries(metricMap, column.getAttributeCode());
|
|
|
+ if (series != null && !series.isEmpty()) {
|
|
|
+ return column.getAttributeCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return columns.isEmpty() ? null : columns.get(0).getAttributeCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- Excel ----------
|
|
|
+
|
|
|
+ private void writeEnergyExcel(String fileName, List<EnergyStatItemVO> items, HttpServletResponse response) {
|
|
|
+ try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
|
|
+ XSSFSheet sheet = workbook.createSheet("能源统计");
|
|
|
+ XSSFCellStyle headerStyle = headerStyle(workbook);
|
|
|
+ String[] headers = {
|
|
|
+ "序号", "客户名称", "站点名称", "设备名称",
|
|
|
+ "调节用电量(kWh)", "削峰量(kWh)", "填谷量(kWh)"
|
|
|
+ };
|
|
|
+ XSSFRow headerRow = sheet.createRow(0);
|
|
|
+ for (int i = 0; i < headers.length; i++) {
|
|
|
+ XSSFCell cell = headerRow.createCell(i);
|
|
|
+ cell.setCellValue(headers[i]);
|
|
|
+ cell.setCellStyle(headerStyle);
|
|
|
+ }
|
|
|
+ int rowIdx = 1;
|
|
|
+ for (EnergyStatItemVO item : items) {
|
|
|
+ XSSFRow row = sheet.createRow(rowIdx++);
|
|
|
+ setCell(row, 0, rowIdx - 1);
|
|
|
+ setCell(row, 1, item.getCustomerName());
|
|
|
+ setCell(row, 2, item.getSiteName());
|
|
|
+ setCell(row, 3, item.getDeviceName());
|
|
|
+ setCell(row, 4, numberStr(item.getRegulatedEnergyKwh()));
|
|
|
+ setCell(row, 5, numberStr(item.getPeakShaveKwh()));
|
|
|
+ setCell(row, 6, numberStr(item.getValleyFillKwh()));
|
|
|
+ }
|
|
|
+ autosize(sheet, headers.length);
|
|
|
+ writeExcelResponse(fileName, workbook, response);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new BusinessException("导出能源统计失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeMeterExcel(String fileName, MeterDataVO meterData, HttpServletResponse response) {
|
|
|
+ try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
|
|
+ XSSFSheet sheet = workbook.createSheet("电表数据");
|
|
|
+ XSSFCellStyle headerStyle = headerStyle(workbook);
|
|
|
+ List<MeterDataVO.MeterColumnVO> columns = meterData.getColumns() == null
|
|
|
+ ? Collections.emptyList() : meterData.getColumns();
|
|
|
+
|
|
|
+ XSSFRow headerRow = sheet.createRow(0);
|
|
|
+ XSSFCell timeHeader = headerRow.createCell(0);
|
|
|
+ timeHeader.setCellValue("collectTime(采集时间)");
|
|
|
+ timeHeader.setCellStyle(headerStyle);
|
|
|
+ for (int i = 0; i < columns.size(); i++) {
|
|
|
+ XSSFCell cell = headerRow.createCell(i + 1);
|
|
|
+ cell.setCellValue(columns.get(i).getHeader());
|
|
|
+ cell.setCellStyle(headerStyle);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<MeterDataVO.MeterRecordVO> records = meterData.getRecords() == null
|
|
|
+ ? Collections.emptyList() : meterData.getRecords();
|
|
|
+ int rowIdx = 1;
|
|
|
+ for (MeterDataVO.MeterRecordVO record : records) {
|
|
|
+ XSSFRow row = sheet.createRow(rowIdx++);
|
|
|
+ setCell(row, 0, record.getCollectTime());
|
|
|
+ Map<String, BigDecimal> values = record.getValues() == null
|
|
|
+ ? Collections.emptyMap() : record.getValues();
|
|
|
+ for (int i = 0; i < columns.size(); i++) {
|
|
|
+ setCell(row, i + 1, numberStr(values.get(columns.get(i).getAttributeCode())));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ autosize(sheet, columns.size() + 1);
|
|
|
+ writeExcelResponse(fileName, workbook, response);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new BusinessException("导出电表数据失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private XSSFCellStyle headerStyle(XSSFWorkbook workbook) {
|
|
|
+ XSSFCellStyle headerStyle = workbook.createCellStyle();
|
|
|
+ headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
|
|
+ headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
|
|
+ XSSFFont headerFont = workbook.createFont();
|
|
|
+ headerFont.setBold(true);
|
|
|
+ headerStyle.setFont(headerFont);
|
|
|
+ return headerStyle;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void autosize(XSSFSheet sheet, int columnCount) {
|
|
|
+ for (int i = 0; i < columnCount; i++) {
|
|
|
+ sheet.autoSizeColumn(i);
|
|
|
+ int width = sheet.getColumnWidth(i);
|
|
|
+ if (width < 4000) {
|
|
|
+ sheet.setColumnWidth(i, 4000);
|
|
|
+ } else if (width > 15000) {
|
|
|
+ sheet.setColumnWidth(i, 15000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeExcelResponse(String fileName, XSSFWorkbook workbook, HttpServletResponse response)
|
|
|
+ throws IOException {
|
|
|
+ String encodedFileName = URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
|
|
|
+ .replace("+", "%20");
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
|
+ response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
|
|
|
+ + "; filename*=UTF-8''" + encodedFileName);
|
|
|
+ workbook.write(response.getOutputStream());
|
|
|
+ response.getOutputStream().flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- helpers ----------
|
|
|
+
|
|
|
+ private LocalDateTime[] resolveTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ LocalDateTime start = startTime != null ? startTime : LocalDate.now().atStartOfDay();
|
|
|
+ LocalDateTime end = endTime != null ? endTime : LocalDate.now().atTime(23, 59, 59);
|
|
|
+ if (start.isAfter(end)) {
|
|
|
+ throw new BusinessException("开始时间不能晚于结束时间");
|
|
|
+ }
|
|
|
+ return new LocalDateTime[]{start, end};
|
|
|
+ }
|
|
|
+
|
|
|
+ private TreeMap<LocalDateTime, BigDecimal> pickMetricSeries(
|
|
|
+ Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap, String metric) {
|
|
|
+ if (CollectionUtils.isEmpty(metricMap) || !StringUtils.hasText(metric)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
|
|
|
+ if (series != null) {
|
|
|
+ return series;
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, TreeMap<LocalDateTime, BigDecimal>> entry : metricMap.entrySet()) {
|
|
|
+ if (metric.equalsIgnoreCase(entry.getKey())) {
|
|
|
+ return entry.getValue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildColumnHeader(String code, String name, String unit) {
|
|
|
+ StringBuilder header = new StringBuilder(code);
|
|
|
+ header.append("(");
|
|
|
+ if (StringUtils.hasText(name)) {
|
|
|
+ header.append(name.trim());
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(unit)) {
|
|
|
+ if (StringUtils.hasText(name)) {
|
|
|
+ header.append("/");
|
|
|
+ }
|
|
|
+ header.append(unit.trim());
|
|
|
+ }
|
|
|
+ if (!StringUtils.hasText(name) && !StringUtils.hasText(unit)) {
|
|
|
+ header.append("属性");
|
|
|
+ }
|
|
|
+ header.append(")");
|
|
|
+ return header.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String trimUuid(String uuid) {
|
|
|
+ return StringUtils.hasText(uuid) ? uuid.trim() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void setCell(XSSFRow row, int col, Object value) {
|
|
|
+ row.createCell(col).setCellValue(value == null ? "" : String.valueOf(value));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String numberStr(Object value) {
|
|
|
+ return value == null ? "" : value.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal nz(BigDecimal value) {
|
|
|
+ return value == null ? BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP) : value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<String> buildPowerMetricPriority() {
|
|
|
+ List<String> codes = new ArrayList<>();
|
|
|
+ codes.add(VppTsdbConstants.METRIC_P);
|
|
|
+ codes.addAll(VppTsdbConstants.ACTIVE_POWER_METRICS);
|
|
|
+ return codes;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class PeakValley {
|
|
|
+ private BigDecimal peak = BigDecimal.ZERO;
|
|
|
+ private BigDecimal valley = BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+}
|