|
|
@@ -1,15 +1,32 @@
|
|
|
package com.usky.ems.service.impl;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
+import com.usky.common.core.bean.ApiResult;
|
|
|
+import com.usky.common.core.exception.BusinessException;
|
|
|
import com.usky.common.security.utils.SecurityUtils;
|
|
|
-import com.usky.ems.domain.DmpDevice;
|
|
|
-import com.usky.ems.domain.EmsDevice;
|
|
|
-import com.usky.ems.domain.EmsDeviceFunction;
|
|
|
-import com.usky.ems.mapper.DmpDeviceMapper;
|
|
|
-import com.usky.ems.mapper.EmsDeviceFunctionMapper;
|
|
|
-import com.usky.ems.mapper.EmsDeviceMapper;
|
|
|
+import com.usky.demo.RemoteTsdbProxyService;
|
|
|
+import com.usky.demo.domain.HistorysInnerRequestVO;
|
|
|
+import com.usky.demo.domain.HistorysInnerResultVO;
|
|
|
+import com.usky.demo.domain.MetricVO;
|
|
|
+import com.usky.ems.domain.*;
|
|
|
+import com.usky.ems.mapper.*;
|
|
|
import com.usky.ems.service.EmsReportService;
|
|
|
import com.usky.ems.service.vo.*;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.poi.ss.usermodel.BorderStyle;
|
|
|
+import org.apache.poi.ss.usermodel.Cell;
|
|
|
+import org.apache.poi.ss.usermodel.CellStyle;
|
|
|
+import org.apache.poi.ss.usermodel.FillPatternType;
|
|
|
+import org.apache.poi.ss.usermodel.Font;
|
|
|
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
|
|
+import org.apache.poi.ss.usermodel.IndexedColors;
|
|
|
+import org.apache.poi.ss.usermodel.Row;
|
|
|
+import org.apache.poi.ss.usermodel.Sheet;
|
|
|
+import org.apache.poi.ss.usermodel.VerticalAlignment;
|
|
|
+import org.apache.poi.ss.usermodel.Workbook;
|
|
|
+import org.apache.poi.ss.util.CellRangeAddress;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.util.StringUtils;
|
|
|
@@ -17,7 +34,15 @@ import org.springframework.util.StringUtils;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
import java.io.IOException;
|
|
|
import java.io.OutputStream;
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.LocalTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -25,14 +50,46 @@ import java.util.stream.Collectors;
|
|
|
* 统计报表服务实现(设备列表基于 leo 设备与属性点位,统计与导出为占位/预留)
|
|
|
*/
|
|
|
@Service
|
|
|
+@Slf4j
|
|
|
public class EmsReportServiceImpl implements EmsReportService {
|
|
|
|
|
|
private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
|
|
|
|
|
|
+ private static final String ENERGY_NO_DATA = "-";
|
|
|
+ private static final String ENERGY_ZERO = "0.00";
|
|
|
+
|
|
|
+ private static final Set<String> CUMULATIVE_ENERGY_METRICS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
+ "totalactiveenergyf",
|
|
|
+ "totalactiveenergyr",
|
|
|
+ "totalreactiveenergyf",
|
|
|
+ "totalreactiveenergyr"
|
|
|
+ )));
|
|
|
+
|
|
|
+ private enum MetricAggregateType {
|
|
|
+ /** 累计表计:时段内末次读数 - 首次读数 */
|
|
|
+ CUMULATIVE_DELTA,
|
|
|
+ /** 瞬时量:时段内算术平均 */
|
|
|
+ INSTANTANEOUS_AVG
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+
|
|
|
@Autowired
|
|
|
private DmpDeviceMapper dmpDeviceMapper;
|
|
|
@Autowired
|
|
|
private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
|
|
|
+ @Autowired
|
|
|
+ private RemoteTsdbProxyService remoteTsdbProxyService;
|
|
|
+ @Autowired
|
|
|
+ private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
|
|
|
+ @Autowired
|
|
|
+ private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
|
|
|
+ @Autowired
|
|
|
+ private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
|
|
|
+ @Autowired
|
|
|
+ private BaseSpaceMapper baseSpaceMapper;
|
|
|
+ @Autowired
|
|
|
+ private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
|
|
|
|
|
|
private String energyTypeName(Long energyTypeId) {
|
|
|
if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
|
|
|
@@ -41,7 +98,7 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
|
|
|
@Override
|
|
|
public EmsReportDevicesResponse getEnergyDevices(Long energyTypeId, String keyword, Long projectId) {
|
|
|
- LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag,0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
|
|
|
+ LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag, 0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
|
|
|
if (StringUtils.hasText(keyword)) {
|
|
|
q.and(w -> w.like(DmpDevice::getDeviceName, keyword));
|
|
|
}
|
|
|
@@ -150,4 +207,1722 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
throw new RuntimeException("导出失败", e);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public EnergyReportResponse getEnergyReport(EnergyReportRequest request) {
|
|
|
+ if (request == null || request.getDeviceList() == null || request.getDeviceList().isEmpty()) {
|
|
|
+ return createEmptyEnergyReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析时间
|
|
|
+ LocalDateTime startTime = parseDateTime(request.getStartTime());
|
|
|
+ LocalDateTime endTime = parseDateTime(request.getEndTime());
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return createEmptyEnergyReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验时间参数
|
|
|
+ validateTimeParams(startTime, endTime, request.getTimeType());
|
|
|
+
|
|
|
+ // 生成列定义
|
|
|
+ List<EnergyReportResponse.ColumnItem> columnList = generateEnergyReportColumns(request.getTimeType(), startTime, endTime);
|
|
|
+
|
|
|
+ // 生成数据列表(按设备分组的二维数组)
|
|
|
+ List<List<EnergyReportResponse.ValueItem>> valueList = generateEnergyReportData(request, startTime, endTime);
|
|
|
+
|
|
|
+ EnergyReportResponse response = new EnergyReportResponse();
|
|
|
+ response.setColumnList(columnList);
|
|
|
+ response.setValueList(valueList);
|
|
|
+
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ItemReportResponse getItemReport(ItemReportRequest request) {
|
|
|
+ if (request == null || request.getItemCodes() == null || request.getItemCodes().isEmpty()) {
|
|
|
+ return createEmptyItemReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析时间
|
|
|
+ LocalDateTime startTime = parseDateTime(request.getStartTime());
|
|
|
+ LocalDateTime endTime = parseDateTime(request.getEndTime());
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return createEmptyItemReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验时间参数
|
|
|
+ validateTimeParams(startTime, endTime, request.getTimeType());
|
|
|
+
|
|
|
+ // 生成标题列
|
|
|
+ List<ItemReportResponse.TitleItem> titleList = generateItemReportTitles(request.getTimeType(), startTime, endTime);
|
|
|
+
|
|
|
+ // 生成数据列表
|
|
|
+ List<Map<String, Object>> dataList = generateItemReportData(request, startTime, endTime);
|
|
|
+
|
|
|
+ ItemReportResponse response = new ItemReportResponse();
|
|
|
+ response.setTitleList(titleList);
|
|
|
+ response.setDataList(dataList);
|
|
|
+
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public SpaceReportResponse getSpaceReport(SpaceReportRequest request) {
|
|
|
+ if (request == null || request.getSpaces() == null || request.getSpaces().isEmpty()) {
|
|
|
+ return createEmptySpaceReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析时间
|
|
|
+ LocalDateTime startTime = parseDateTime(request.getStartTime());
|
|
|
+ LocalDateTime endTime = parseDateTime(request.getEndTime());
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return createEmptySpaceReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验时间参数
|
|
|
+ validateTimeParams(startTime, endTime, request.getTimeType());
|
|
|
+
|
|
|
+ // 生成列定义
|
|
|
+ List<EnergyReportResponse.ColumnItem> columnList = generateSpaceReportColumns(request.getTimeType(), startTime, endTime);
|
|
|
+
|
|
|
+ // 生成数据列表(按区域/空间分组的二维数组)
|
|
|
+ List<List<Map<String, Object>>> valueList = generateSpaceReportData(request, startTime, endTime);
|
|
|
+
|
|
|
+ // 计算总计
|
|
|
+ Number total = calculateSpaceReportTotal(valueList);
|
|
|
+
|
|
|
+ SpaceReportResponse response = new SpaceReportResponse();
|
|
|
+ response.setColumnList(columnList);
|
|
|
+ response.setValueList(valueList);
|
|
|
+ response.setTotal(total);
|
|
|
+
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exportEnergyReport(EnergyReportRequest request, HttpServletResponse response) {
|
|
|
+ EnergyReportResponse report = getEnergyReport(request);
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
|
|
|
+ ? report.getColumnList() : Collections.emptyList();
|
|
|
+ List<List<EnergyReportResponse.ValueItem>> groupedRows = report.getValueList() != null
|
|
|
+ ? report.getValueList() : Collections.emptyList();
|
|
|
+ writeEnergyReportExcel(response, buildExportFileName("能源报表"), columns, groupedRows);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exportSpaceReport(SpaceReportRequest request, HttpServletResponse response) {
|
|
|
+ SpaceReportResponse report = getSpaceReport(request);
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
|
|
|
+ ? report.getColumnList() : Collections.emptyList();
|
|
|
+ List<List<Map<String, Object>>> groupedRows = report.getValueList() != null
|
|
|
+ ? report.getValueList() : Collections.emptyList();
|
|
|
+ writeSpaceReportExcel(response, buildExportFileName("区域报表"), columns, groupedRows);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 能源报表私有方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成能源报表列定义
|
|
|
+ */
|
|
|
+ private List<EnergyReportResponse.ColumnItem> generateEnergyReportColumns(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();
|
|
|
+
|
|
|
+ // 固定列
|
|
|
+ columns.add(createColumn("设备名称", "deviceName", true));
|
|
|
+ columns.add(createColumn("通讯地址", "commAddress", true));
|
|
|
+ columns.add(createColumn("功能点", "identifier", true));
|
|
|
+ columns.add(createColumn("合计", "total", true));
|
|
|
+
|
|
|
+ // 自定义查询仅返回合计,不生成动态时间列
|
|
|
+ if (isCustomTimeType(timeType)) {
|
|
|
+ return columns;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动态时间列
|
|
|
+ List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ columns.add(createColumn(label, "_" + label.replace("时", "").replace("日", "").replace("月", ""), false));
|
|
|
+ }
|
|
|
+
|
|
|
+ return columns;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成能源报表数据(按设备分组的二维数组)
|
|
|
+ * 外层 List 代表不同的设备
|
|
|
+ * 内层 List 包含该设备下所有请求的功能点(属性)数据
|
|
|
+ */
|
|
|
+ private List<List<EnergyReportResponse.ValueItem>> generateEnergyReportData(EnergyReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<List<EnergyReportResponse.ValueItem>> groupedValueList = new ArrayList<>();
|
|
|
+
|
|
|
+ // 获取设备uuid列表
|
|
|
+ List<Integer> deviceIds = request.getDeviceList().stream()
|
|
|
+ .filter(device -> device != null && device.getId() != null)
|
|
|
+ .map(EnergyReportRequest.DeviceItem::getId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<String> deviceUuids = getUuidList(deviceIds);
|
|
|
+
|
|
|
+ // 查询tsdb历史数据
|
|
|
+ HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
|
|
|
+ historyRequestVO.setDeviceuuid(deviceUuids);
|
|
|
+ historyRequestVO.setStartTime(request.getStartTime());
|
|
|
+ historyRequestVO.setEndTime(request.getEndTime());
|
|
|
+ historyRequestVO.setMetrics(request.getFuncList() != null && !request.getFuncList().isEmpty()
|
|
|
+ ? request.getFuncList().stream().map(EnergyReportRequest.FuncItem::getIdentifier)
|
|
|
+ .map(String::toLowerCase)
|
|
|
+ .collect(Collectors.toList())
|
|
|
+ : new ArrayList<>());
|
|
|
+ log.info("历史数据接口请求:{}", historyRequestVO);
|
|
|
+ ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
|
|
|
+
|
|
|
+ // 判断历史数据是否为空
|
|
|
+ boolean hasData = !isHistoryDataEmpty(listHistoryData);
|
|
|
+ //log.warn("历史数据为空,设备uuid:{}", deviceUuids);
|
|
|
+ log.info("历史数据接口返回:{}", listHistoryData);
|
|
|
+
|
|
|
+ // 如果有数据,解析TSDB返回的结果
|
|
|
+ Map<String, Map<String, List<Map<String, Object>>>> tsdbDataMap = new HashMap<>();
|
|
|
+ Map<String, String> deviceIdMap = new HashMap<>(); // deviceuuid -> device_id
|
|
|
+ if (hasData) {
|
|
|
+ tsdbDataMap = parseTsdbData(listHistoryData.getData());
|
|
|
+ deviceIdMap = extractDeviceIdMap(listHistoryData.getData());
|
|
|
+ }
|
|
|
+ log.info("设备id映射:{}", deviceIdMap);
|
|
|
+ log.info("tsdb数据:{}", tsdbDataMap);
|
|
|
+
|
|
|
+ boolean customQuery = isCustomTimeType(request.getTimeType());
|
|
|
+
|
|
|
+ for (EnergyReportRequest.DeviceItem device : request.getDeviceList()) {
|
|
|
+ if (device == null || StringUtils.isEmpty(device.getId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 为当前设备创建一个子列表
|
|
|
+ List<EnergyReportResponse.ValueItem> deviceValueList = new ArrayList<>();
|
|
|
+
|
|
|
+ // 获取设备UUID用于匹配TSDB数据
|
|
|
+ DmpDevice dmpDevice = dmpDeviceMapper.selectById(device.getId());
|
|
|
+ String deviceUuid = dmpDevice != null ? dmpDevice.getDeviceUuid() : null;
|
|
|
+
|
|
|
+ // 如果没有功能点列表,为设备创建一个默认记录
|
|
|
+ if (request.getFuncList() == null || request.getFuncList().isEmpty()) {
|
|
|
+ if (customQuery) {
|
|
|
+ String total = ENERGY_NO_DATA;
|
|
|
+ if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
+ Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
|
|
|
+ Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
|
|
|
+ total = calculateCustomPeriodTotal(
|
|
|
+ firstMetric.getValue(), firstMetric.getKey(), startTime, endTime);
|
|
|
+ }
|
|
|
+ deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, null));
|
|
|
+ } else {
|
|
|
+ Map<String, String> timeData;
|
|
|
+ String total;
|
|
|
+ if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
+ Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
|
|
|
+ Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
|
|
|
+ timeData = buildEnergyTimeDataForMetricItems(
|
|
|
+ firstMetric.getValue(), firstMetric.getKey(),
|
|
|
+ request.getTimeType(), startTime, endTime);
|
|
|
+ total = calculateReportTotal(
|
|
|
+ timeData, firstMetric.getKey(), firstMetric.getValue(), startTime, endTime);
|
|
|
+ } else {
|
|
|
+ timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
|
|
|
+ total = ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, timeData));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ for (EnergyReportRequest.FuncItem func : request.getFuncList()) {
|
|
|
+ if (func == null || StringUtils.isEmpty(func.getIdentifier())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String identifier = func.getIdentifierName() != null ? func.getIdentifierName() : func.getIdentifier();
|
|
|
+ if (customQuery) {
|
|
|
+ String total = ENERGY_NO_DATA;
|
|
|
+ if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
+ List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(
|
|
|
+ tsdbDataMap.get(deviceUuid), func.getIdentifier());
|
|
|
+ total = calculateCustomPeriodTotal(
|
|
|
+ metricItems, func.getIdentifier(), startTime, endTime);
|
|
|
+ }
|
|
|
+ deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, null));
|
|
|
+ } else {
|
|
|
+ Map<String, String> timeData;
|
|
|
+ String total;
|
|
|
+ List<Map<String, Object>> metricItems = null;
|
|
|
+ if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
+ metricItems = getMetricItemsIgnoreCase(tsdbDataMap.get(deviceUuid), func.getIdentifier());
|
|
|
+ timeData = buildTimeDataFromTsdbForMetric(
|
|
|
+ tsdbDataMap.get(deviceUuid),
|
|
|
+ func.getIdentifier(),
|
|
|
+ request.getTimeType(),
|
|
|
+ startTime,
|
|
|
+ endTime);
|
|
|
+ total = calculateReportTotal(
|
|
|
+ timeData, func.getIdentifier(), metricItems, startTime, endTime);
|
|
|
+ } else {
|
|
|
+ timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
|
|
|
+ total = ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, timeData));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ groupedValueList.add(deviceValueList);
|
|
|
+ }
|
|
|
+
|
|
|
+ return groupedValueList;
|
|
|
+ }
|
|
|
+
|
|
|
+ private EnergyReportResponse.ValueItem buildEnergyValueItem(EnergyReportRequest.DeviceItem device,
|
|
|
+ String deviceUuid,
|
|
|
+ Map<String, String> deviceIdMap,
|
|
|
+ String identifier,
|
|
|
+ String total,
|
|
|
+ Map<String, String> timeData) {
|
|
|
+ EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
|
|
|
+ item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
|
|
|
+ item.setDeviceName(device.getName());
|
|
|
+ item.setCommAddress(device.getCommAddress());
|
|
|
+ item.setIdentifier(identifier);
|
|
|
+ item.setTotal(total);
|
|
|
+ item.setTimeData(timeData);
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isCustomTimeType(String timeType) {
|
|
|
+ return timeType != null && "custom".equalsIgnoreCase(timeType);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自定义查询合计:按指标类型聚合(累计类求各分段差值之和,瞬时类求全区间平均)
|
|
|
+ */
|
|
|
+ private String calculateCustomPeriodTotal(List<Map<String, Object>> metricItems,
|
|
|
+ String metricIdentifier,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ if (metricItems == null || metricItems.isEmpty()) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ String segmentTimeType = resolveSegmentTimeTypeForCustom(startTime, endTime);
|
|
|
+ if ("custom_range".equals(segmentTimeType)) {
|
|
|
+ return calculateEnergySegmentValue(metricItems, metricIdentifier, startTime, endTime, true);
|
|
|
+ }
|
|
|
+ Map<String, String> timeData = buildEnergyTimeDataForMetricItems(
|
|
|
+ metricItems, metricIdentifier, segmentTimeType, startTime, endTime);
|
|
|
+ return calculateReportTotal(timeData, metricIdentifier, metricItems, startTime, endTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自定义查询推断分段粒度,保证与 month/date/year 的合计逻辑一致
|
|
|
+ */
|
|
|
+ private String resolveSegmentTimeTypeForCustom(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ if (startTime.toLocalDate().equals(endTime.toLocalDate())) {
|
|
|
+ return "date";
|
|
|
+ }
|
|
|
+ if (startTime.getYear() == endTime.getYear() && startTime.getMonthValue() == endTime.getMonthValue()) {
|
|
|
+ return "month";
|
|
|
+ }
|
|
|
+ if (startTime.getYear() == endTime.getYear()) {
|
|
|
+ return "year";
|
|
|
+ }
|
|
|
+ return "custom_range";
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, String> buildEnergyTimeDataForMetricItems(List<Map<String, Object>> metricItems,
|
|
|
+ String metricIdentifier,
|
|
|
+ String timeType,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ Map<String, String> timeData = new LinkedHashMap<>();
|
|
|
+ List<TimeSegmentBound> segments = buildReportTimeSegments(timeType, startTime, endTime);
|
|
|
+ for (TimeSegmentBound segment : segments) {
|
|
|
+ String key = toEnergyTimeDataKey(segment.getLabel());
|
|
|
+ timeData.put(key, calculateEnergySegmentValue(
|
|
|
+ metricItems, metricIdentifier,
|
|
|
+ segment.getStartInclusive(), segment.getEndExclusive(), false));
|
|
|
+ }
|
|
|
+ return timeData;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toEnergyTimeDataKey(String label) {
|
|
|
+ return "_" + label.replace("时", "").replace("日", "").replace("月", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ private MetricAggregateType resolveMetricAggregateType(String identifier) {
|
|
|
+ if (!StringUtils.hasText(identifier)) {
|
|
|
+ return MetricAggregateType.INSTANTANEOUS_AVG;
|
|
|
+ }
|
|
|
+ if (CUMULATIVE_ENERGY_METRICS.contains(identifier.toLowerCase())) {
|
|
|
+ return MetricAggregateType.CUMULATIVE_DELTA;
|
|
|
+ }
|
|
|
+ return MetricAggregateType.INSTANTANEOUS_AVG;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单个时间段聚合:累计类取差值,瞬时类取平均(date 按小时、month 按天、year 按月)
|
|
|
+ */
|
|
|
+ private String calculateEnergySegmentValue(List<Map<String, Object>> metricItems,
|
|
|
+ String metricIdentifier,
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endExclusive,
|
|
|
+ boolean endInclusive) {
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> readings = endInclusive
|
|
|
+ ? parseMetricReadingsInRangeInclusive(metricItems, startInclusive, endExclusive)
|
|
|
+ : parseMetricReadingsInRange(metricItems, startInclusive, endExclusive);
|
|
|
+ if (readings.isEmpty()) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
|
|
|
+ return calculateCumulativeSegmentDelta(readings);
|
|
|
+ }
|
|
|
+ return calculateInstantaneousSegmentAverage(readings);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 累计表计:时段末次读数 - 首次读数(至少 2 条读数) */
|
|
|
+ private String calculateCumulativeSegmentDelta(TreeMap<LocalDateTime, BigDecimal> readings) {
|
|
|
+ if (readings.size() < 2) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ BigDecimal first = readings.firstEntry().getValue();
|
|
|
+ BigDecimal last = readings.lastEntry().getValue();
|
|
|
+ BigDecimal usage = last.subtract(first);
|
|
|
+ if (usage.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
+ usage = last;
|
|
|
+ }
|
|
|
+ return formatEnergyReportValue(usage);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 瞬时量:时段内读数算术平均 */
|
|
|
+ private String calculateInstantaneousSegmentAverage(TreeMap<LocalDateTime, BigDecimal> readings) {
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ for (BigDecimal value : readings.values()) {
|
|
|
+ sum = sum.add(value);
|
|
|
+ }
|
|
|
+ return formatEnergyReportValue(sum.divide(BigDecimal.valueOf(readings.size()), 4, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatEnergyReportValue(BigDecimal value) {
|
|
|
+ return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> getMetricItemsIgnoreCase(Map<String, List<Map<String, Object>>> metricDataMap,
|
|
|
+ String identifier) {
|
|
|
+ if (metricDataMap == null || !StringUtils.hasText(identifier)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<Map<String, Object>> items = metricDataMap.get(identifier);
|
|
|
+ if (items != null) {
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, List<Map<String, Object>>> entry : metricDataMap.entrySet()) {
|
|
|
+ if (identifier.equalsIgnoreCase(entry.getKey())) {
|
|
|
+ return entry.getValue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRange(List<Map<String, Object>> metricItems,
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endExclusive) {
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
|
|
|
+ if (metricItems == null || metricItems.isEmpty() || !startInclusive.isBefore(endExclusive)) {
|
|
|
+ return readings;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> item : metricItems) {
|
|
|
+ if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
|
|
|
+ if (timestamp == null || timestamp.isBefore(startInclusive) || !timestamp.isBefore(endExclusive)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ readings.put(timestamp, new BigDecimal(item.get("value").toString()));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析能源报表历史读数失败:{}", item, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return readings;
|
|
|
+ }
|
|
|
+
|
|
|
+ private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRangeInclusive(List<Map<String, Object>> metricItems,
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endInclusive) {
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
|
|
|
+ if (metricItems == null || metricItems.isEmpty()) {
|
|
|
+ return readings;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> item : metricItems) {
|
|
|
+ if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
|
|
|
+ if (timestamp == null || timestamp.isBefore(startInclusive) || timestamp.isAfter(endInclusive)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ readings.put(timestamp, new BigDecimal(item.get("value").toString()));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析能源报表历史读数失败:{}", item, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return readings;
|
|
|
+ }
|
|
|
+
|
|
|
+ private LocalDateTime parseMetricTimestamp(Object timestampObj) {
|
|
|
+ if (timestampObj == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String timestampStr = timestampObj.toString();
|
|
|
+ if (timestampStr.endsWith(".0")) {
|
|
|
+ timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
|
|
|
+ }
|
|
|
+ return parseDateTime(timestampStr);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成空的时间数据结构(用于无数据时填充)
|
|
|
+ */
|
|
|
+ private Map<String, String> generateEmptyTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ Map<String, String> data = new LinkedHashMap<>();
|
|
|
+ List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ data.put(toEnergyTimeDataKey(label), ENERGY_ZERO);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算合计:累计类为各时段差值之和,瞬时类为查询区间内全量读数平均
|
|
|
+ */
|
|
|
+ private String calculateReportTotal(Map<String, String> timeData,
|
|
|
+ String metricIdentifier,
|
|
|
+ List<Map<String, Object>> metricItems,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
|
|
|
+ return calculateCumulativeTotal(timeData);
|
|
|
+ }
|
|
|
+ return calculateInstantaneousPeriodAverage(metricItems, startTime, endTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 累计类合计:各时段差值之和
|
|
|
+ */
|
|
|
+ private String calculateCumulativeTotal(Map<String, String> timeData) {
|
|
|
+ if (timeData == null || timeData.isEmpty()) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ boolean hasNumeric = false;
|
|
|
+ boolean allMissing = true;
|
|
|
+ for (String value : timeData.values()) {
|
|
|
+ if (ENERGY_NO_DATA.equals(value)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ allMissing = false;
|
|
|
+ try {
|
|
|
+ sum = sum.add(new BigDecimal(value));
|
|
|
+ hasNumeric = true;
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ // 忽略无效数据
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (allMissing) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ return hasNumeric ? formatEnergyReportValue(sum) : ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 瞬时类合计:查询区间内所有读数的算术平均
|
|
|
+ */
|
|
|
+ private String calculateInstantaneousPeriodAverage(List<Map<String, Object>> metricItems,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ if (metricItems == null || metricItems.isEmpty()) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ TreeMap<LocalDateTime, BigDecimal> readings = parseMetricReadingsInRangeInclusive(
|
|
|
+ metricItems, startTime, endTime);
|
|
|
+ if (readings.isEmpty()) {
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ return calculateInstantaneousSegmentAverage(readings);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== TSDB数据解析方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析TSDB返回的数据
|
|
|
+ * 返回结构:Map<deviceuuid, Map<metric, List<metricItems>>>
|
|
|
+ */
|
|
|
+ private Map<String, Map<String, List<Map<String, Object>>>> parseTsdbData(List<HistorysInnerResultVO> tsdbResultList) {
|
|
|
+ Map<String, Map<String, List<Map<String, Object>>>> dataMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (tsdbResultList == null || tsdbResultList.isEmpty()) {
|
|
|
+ return dataMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (HistorysInnerResultVO result : tsdbResultList) {
|
|
|
+ if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String deviceUuid = result.getDeviceuuid();
|
|
|
+ Map<String, List<Map<String, Object>>> metricMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (result.getMetrics() != null && !result.getMetrics().isEmpty()) {
|
|
|
+ for (MetricVO metricVO : result.getMetrics()) {
|
|
|
+ if (metricVO != null && StringUtils.hasText(metricVO.getMetric())) {
|
|
|
+ metricMap.put(metricVO.getMetric(), metricVO.getMetricItems());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dataMap.put(deviceUuid, metricMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ return dataMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从TSDB返回结果中提取device_id映射
|
|
|
+ * 返回结构:Map<deviceuuid, device_id>
|
|
|
+ */
|
|
|
+ private Map<String, String> extractDeviceIdMap(List<HistorysInnerResultVO> tsdbResultList) {
|
|
|
+ Map<String, String> deviceIdMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (tsdbResultList == null || tsdbResultList.isEmpty()) {
|
|
|
+ return deviceIdMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (HistorysInnerResultVO result : tsdbResultList) {
|
|
|
+ if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String deviceUuid = result.getDeviceuuid();
|
|
|
+ // 从tags中获取device_id
|
|
|
+ if (result.getTags() != null && result.getTags().containsKey("device_id")) {
|
|
|
+ String deviceId = (String) result.getTags().get("device_id");
|
|
|
+ deviceIdMap.put(deviceUuid, deviceId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return deviceIdMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从TSDB数据中构建时间数据(取第一个功能点)
|
|
|
+ */
|
|
|
+ private Map<String, String> buildTimeDataFromTsdb(Map<String, List<Map<String, Object>>> metricDataMap,
|
|
|
+ String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ if (metricDataMap == null || metricDataMap.isEmpty()) {
|
|
|
+ return generateEmptyTimeData(timeType, startTime, endTime);
|
|
|
+ }
|
|
|
+ Map.Entry<String, List<Map<String, Object>>> firstMetric = metricDataMap.entrySet().iterator().next();
|
|
|
+ return buildEnergyTimeDataForMetricItems(
|
|
|
+ firstMetric.getValue(), firstMetric.getKey(), timeType, startTime, endTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从TSDB数据中构建指定功能点的时间数据
|
|
|
+ */
|
|
|
+ private Map<String, String> buildTimeDataFromTsdbForMetric(Map<String, List<Map<String, Object>>> metricDataMap,
|
|
|
+ String metric,
|
|
|
+ String timeType,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(metricDataMap, metric);
|
|
|
+ if (metricItems == null || metricItems.isEmpty()) {
|
|
|
+ Map<String, String> empty = new LinkedHashMap<>();
|
|
|
+ for (TimeSegmentBound segment : buildReportTimeSegments(timeType, startTime, endTime)) {
|
|
|
+ empty.put(toEnergyTimeDataKey(segment.getLabel()), ENERGY_NO_DATA);
|
|
|
+ }
|
|
|
+ return empty;
|
|
|
+ }
|
|
|
+ return buildEnergyTimeDataForMetricItems(metricItems, metric, timeType, startTime, endTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 分项报表私有方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成分项报表标题列
|
|
|
+ */
|
|
|
+ private List<ItemReportResponse.TitleItem> generateItemReportTitles(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<ItemReportResponse.TitleItem> titles = new ArrayList<>();
|
|
|
+
|
|
|
+ // 固定列
|
|
|
+ titles.add(createTitleItem("name", true, "分项名称", null));
|
|
|
+ titles.add(createTitleItem("total", true, "合计", null));
|
|
|
+
|
|
|
+ // 动态时间列
|
|
|
+ List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
|
|
|
+ int index = 1;
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ titles.add(createTitleItem("_" + index, false, label, index));
|
|
|
+ index++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return titles;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成分项报表数据
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> generateItemReportData(ItemReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<Map<String, Object>> dataList = new ArrayList<>();
|
|
|
+
|
|
|
+ for (ItemReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
|
|
|
+ if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
+ row.put("name", itemCode.getName());
|
|
|
+
|
|
|
+ // 查询分项能耗数据(模拟)
|
|
|
+ Map<String, Number> timeData = queryItemEnergyData(itemCode.getCode(), request.getSpaceId(),
|
|
|
+ startTime, endTime, request.getTimeType());
|
|
|
+
|
|
|
+ // 计算合计
|
|
|
+ Number total = calculateItemTotal(timeData);
|
|
|
+ row.put("total", total);
|
|
|
+
|
|
|
+ // 添加时间列数据
|
|
|
+ int index = 1;
|
|
|
+ for (Number value : timeData.values()) {
|
|
|
+ row.put("_" + index, value);
|
|
|
+ index++;
|
|
|
+ }
|
|
|
+
|
|
|
+ dataList.add(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ return dataList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询分项能耗数据(模拟实现)
|
|
|
+ */
|
|
|
+ private Map<String, Number> queryItemEnergyData(String itemCode, Long spaceId,
|
|
|
+ LocalDateTime startTime, LocalDateTime endTime, String dateType) {
|
|
|
+ Map<String, Number> data = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ // TODO: 实际应从数据库查询真实数据
|
|
|
+ List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ data.put(label, 0); // 模拟数据为0
|
|
|
+ }
|
|
|
+
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算分项合计
|
|
|
+ */
|
|
|
+ private Number calculateItemTotal(Map<String, Number> timeData) {
|
|
|
+ if (timeData == null || timeData.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return sumUsageValues(timeData.values());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 区域报表私有方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成区域报表列定义
|
|
|
+ */
|
|
|
+ private List<EnergyReportResponse.ColumnItem> generateSpaceReportColumns(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();
|
|
|
+
|
|
|
+ // 固定列
|
|
|
+ columns.add(createColumn("区域名称", "spaceName", true));
|
|
|
+ columns.add(createColumn("分项名称", "itemName", true));
|
|
|
+ columns.add(createColumn("合计", "total", true));
|
|
|
+
|
|
|
+ // 动态时间列(prop 与前端约定:date/month 保留「时/日」后缀,year 去掉「月」)
|
|
|
+ List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ columns.add(createColumn(label, toSpaceReportColumnProp(label, dateType), false));
|
|
|
+ }
|
|
|
+
|
|
|
+ return columns;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成区域报表数据(按区域/空间分组的二维数组)
|
|
|
+ * 外层 List 代表不同的区域
|
|
|
+ * 内层 List 包含该区域下所有设备的详细数据记录
|
|
|
+ */
|
|
|
+ private List<List<Map<String, Object>>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<List<Map<String, Object>>> groupedValueList = new ArrayList<>();
|
|
|
+ String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
|
|
|
+ List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
|
|
|
+
|
|
|
+ for (SpaceReportRequest.SpaceItem space : request.getSpaces()) {
|
|
|
+ if (space == null || space.getId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Map<String, Number>> itemCodeTimeData = querySpaceItemEnergyData(space, request, startTime, endTime);
|
|
|
+ List<Map<String, Object>> spaceValueList = new ArrayList<>();
|
|
|
+
|
|
|
+ for (SpaceReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
|
|
|
+ if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Number> timeData = itemCodeTimeData.getOrDefault(
|
|
|
+ buildSpaceItemKey(itemCode),
|
|
|
+ createEmptySpaceTimeData(timeType, startTime, endTime));
|
|
|
+
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
+ row.put("spaceName", space.getName());
|
|
|
+ row.put("itemName", itemCode.getName());
|
|
|
+
|
|
|
+ List<Number> periodValues = new ArrayList<>();
|
|
|
+ for (String label : timeLabels) {
|
|
|
+ Number value = timeData.getOrDefault(label, 0);
|
|
|
+ Number roundedValue = toUsageDecimal(value);
|
|
|
+ row.put(toSpaceReportValueKey(label), roundedValue.doubleValue());
|
|
|
+ periodValues.add(roundedValue);
|
|
|
+ }
|
|
|
+ row.put("total", sumUsageValues(periodValues));
|
|
|
+ spaceValueList.add(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ groupedValueList.add(spaceValueList);
|
|
|
+ }
|
|
|
+
|
|
|
+ return groupedValueList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询区域分项能耗数据,按分项(code+name)-> 时间标签 -> 用量 返回
|
|
|
+ */
|
|
|
+ private Map<String, Map<String, Number>> querySpaceItemEnergyData(SpaceReportRequest.SpaceItem space,
|
|
|
+ SpaceReportRequest request,
|
|
|
+ LocalDateTime startTime,
|
|
|
+ LocalDateTime endTime) {
|
|
|
+ String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
|
|
|
+ List<TimeSegmentBound> timeSegments = buildReportTimeSegments(timeType, startTime, endTime);
|
|
|
+
|
|
|
+ Map<String, Map<String, Number>> result = new LinkedHashMap<>();
|
|
|
+ List<SpaceReportRequest.ItemCodeItem> requestedItems = request.getItemCodes() == null
|
|
|
+ ? Collections.emptyList()
|
|
|
+ : request.getItemCodes().stream()
|
|
|
+ .filter(item -> item != null && StringUtils.hasText(item.getCode()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ for (SpaceReportRequest.ItemCodeItem item : requestedItems) {
|
|
|
+ result.put(buildSpaceItemKey(item), createEmptySpaceTimeData(timeType, startTime, endTime));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (space == null || space.getId() == null || requestedItems.isEmpty()) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Long> spaceIds = spaceIds(Collections.singletonList(space.getId()));
|
|
|
+ List<String> gatewayUuids = getGatewayUuidsBySpaceIds(spaceIds).stream()
|
|
|
+ .map(BaseSpaceGateway::getGatewayUuid)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<String> spaceDeviceUuids = getDeviceUuidsByGatewayIds(gatewayUuids).stream()
|
|
|
+ .map(DmpDevice::getDeviceUuid)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ if (spaceDeviceUuids.isEmpty()) {
|
|
|
+ log.warn("区域下无设备,spaceId:{}", space.getId());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ Integer energyType = getEnergyTypeByDeviceUuid(spaceDeviceUuids.get(0));
|
|
|
+ Long productId = getProductIdByEnergyType(energyType);
|
|
|
+ if (productId != 360L) {
|
|
|
+ throw new BusinessException("暂不支持该设备类型");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, List<String>> itemCodeToDeviceUuids = getItemCodeByDeviceUuid(spaceDeviceUuids);
|
|
|
+
|
|
|
+ List<String> metrics = Collections.singletonList("totalactiveenergyf");
|
|
|
+ HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
|
|
|
+ historyRequestVO.setDeviceuuid(spaceDeviceUuids);
|
|
|
+ historyRequestVO.setStartTime(request.getStartTime());
|
|
|
+ historyRequestVO.setEndTime(request.getEndTime());
|
|
|
+ historyRequestVO.setMetrics(metrics);
|
|
|
+ log.info("历史数据接口请求:{}", historyRequestVO);
|
|
|
+ ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
|
|
|
+ log.info("历史数据接口返回:{}", listHistoryData);
|
|
|
+
|
|
|
+ if (isHistoryDataEmpty(listHistoryData)) {
|
|
|
+ log.warn("历史数据为空,设备uuid:{}", spaceDeviceUuids);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap =
|
|
|
+ parseDeviceMetricTimeValueMap(listHistoryData.getData());
|
|
|
+
|
|
|
+ for (SpaceReportRequest.ItemCodeItem itemCodeItem : requestedItems) {
|
|
|
+ List<String> itemDeviceUuids = resolveSpaceItemDeviceUuids(spaceDeviceUuids, itemCodeToDeviceUuids, itemCodeItem);
|
|
|
+ if (itemDeviceUuids.isEmpty()) {
|
|
|
+ log.warn("分项未匹配到区域设备,spaceId:{},itemCode:{},itemName:{}",
|
|
|
+ space.getId(), itemCodeItem.getCode(), itemCodeItem.getName());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Number> timeUsageMap = result.get(buildSpaceItemKey(itemCodeItem));
|
|
|
+ for (String deviceUuid : itemDeviceUuids) {
|
|
|
+ Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
|
|
|
+ if (metricTimeValueMap == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
|
|
|
+ if (timeValueMap == null || timeValueMap.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (TimeSegmentBound segment : timeSegments) {
|
|
|
+ double usage = calculateSegmentUsage(timeValueMap, segment.getStartInclusive(), segment.getEndExclusive());
|
|
|
+ timeUsageMap.put(segment.getLabel(), addUsage(timeUsageMap.get(segment.getLabel()), usage).doubleValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildSpaceItemKey(SpaceReportRequest.ItemCodeItem item) {
|
|
|
+ return item.getCode() + "::" + (item.getName() != null ? item.getName() : "");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析分项对应设备:优先按分项名称解析编码,再按 code 匹配,返回区域内全部关联设备
|
|
|
+ */
|
|
|
+ private List<String> resolveSpaceItemDeviceUuids(List<String> spaceDeviceUuids,
|
|
|
+ Map<String, List<String>> itemCodeToDeviceUuids,
|
|
|
+ SpaceReportRequest.ItemCodeItem itemCodeItem) {
|
|
|
+ Set<String> spaceDeviceSet = new HashSet<>(spaceDeviceUuids);
|
|
|
+ String requestCode = itemCodeItem.getCode().trim();
|
|
|
+
|
|
|
+ String resolvedCode = resolveEnergyItemDbCode(requestCode, itemCodeItem.getName());
|
|
|
+ if (StringUtils.hasText(resolvedCode)) {
|
|
|
+ List<String> matched = findDevicesByItemCode(resolvedCode, itemCodeToDeviceUuids, spaceDeviceSet);
|
|
|
+ if (!matched.isEmpty()) {
|
|
|
+ return matched;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> matched = findDevicesByItemCode(requestCode, itemCodeToDeviceUuids, spaceDeviceSet);
|
|
|
+ if (!matched.isEmpty()) {
|
|
|
+ return matched;
|
|
|
+ }
|
|
|
+
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> findDevicesByItemCode(String itemCode,
|
|
|
+ Map<String, List<String>> itemCodeToDeviceUuids,
|
|
|
+ Set<String> spaceDeviceSet) {
|
|
|
+ List<String> matched = filterDevicesInSpace(itemCodeToDeviceUuids.get(itemCode), spaceDeviceSet);
|
|
|
+ if (!matched.isEmpty()) {
|
|
|
+ return matched;
|
|
|
+ }
|
|
|
+ return filterDevicesInSpace(
|
|
|
+ emsDeviceItemCodeMapper.selectDeviceUuidsByItemCode(SecurityUtils.getTenantId(), itemCode),
|
|
|
+ spaceDeviceSet);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> filterDevicesInSpace(List<String> deviceUuids, Set<String> spaceDeviceSet) {
|
|
|
+ if (deviceUuids == null || deviceUuids.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return deviceUuids.stream()
|
|
|
+ .filter(StringUtils::hasText)
|
|
|
+ .map(String::trim)
|
|
|
+ .filter(spaceDeviceSet::contains)
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将请求中的 code/identifier/name 解析为 ems_energy_item_code 表中的 code
|
|
|
+ */
|
|
|
+ private String resolveEnergyItemDbCode(String requestCode, String itemName) {
|
|
|
+ if (StringUtils.hasText(itemName)) {
|
|
|
+ EmsEnergyItemCode byName = emsEnergyItemCodeMapper.selectOne(
|
|
|
+ Wrappers.lambdaQuery(EmsEnergyItemCode.class)
|
|
|
+ .eq(EmsEnergyItemCode::getName, itemName)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (byName != null) {
|
|
|
+ return byName.getCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.hasText(requestCode)) {
|
|
|
+ EmsEnergyItemCode byCode = emsEnergyItemCodeMapper.selectOne(
|
|
|
+ Wrappers.lambdaQuery(EmsEnergyItemCode.class)
|
|
|
+ .eq(EmsEnergyItemCode::getCode, requestCode)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (byCode != null) {
|
|
|
+ return byCode.getCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ EmsEnergyItemCode byIdentifier = emsEnergyItemCodeMapper.selectOne(
|
|
|
+ Wrappers.lambdaQuery(EmsEnergyItemCode.class)
|
|
|
+ .eq(EmsEnergyItemCode::getIdentifier, requestCode)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (byIdentifier != null) {
|
|
|
+ return byIdentifier.getCode();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private TreeMap<LocalDateTime, Double> extractSegmentReadings(TreeMap<LocalDateTime, Double> source,
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endExclusive) {
|
|
|
+ TreeMap<LocalDateTime, Double> result = new TreeMap<>();
|
|
|
+ if (source == null || source.isEmpty() || !startInclusive.isBefore(endExclusive)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ for (Map.Entry<LocalDateTime, Double> entry : source.entrySet()) {
|
|
|
+ LocalDateTime ts = entry.getKey();
|
|
|
+ if (!ts.isBefore(startInclusive) && ts.isBefore(endExclusive)) {
|
|
|
+ result.put(ts, entry.getValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 TSDB 历史数据为 设备 -> 指标 -> 时间 -> 值 结构
|
|
|
+ */
|
|
|
+ private Map<String, Map<String, TreeMap<LocalDateTime, Double>>> parseDeviceMetricTimeValueMap(
|
|
|
+ List<HistorysInnerResultVO> historyDataList) {
|
|
|
+ Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (historyDataList == null || historyDataList.isEmpty()) {
|
|
|
+ return deviceMetricTimeValueMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (HistorysInnerResultVO result : historyDataList) {
|
|
|
+ if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String deviceUuid = result.getDeviceuuid();
|
|
|
+ Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap =
|
|
|
+ deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());
|
|
|
+
|
|
|
+ if (result.getMetrics() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (MetricVO metric : result.getMetrics()) {
|
|
|
+ if (metric == null || !StringUtils.hasText(metric.getMetric()) || metric.getMetricItems() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String metricName = metric.getMetric();
|
|
|
+ TreeMap<LocalDateTime, Double> timeValueMap =
|
|
|
+ metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());
|
|
|
+
|
|
|
+ for (Map<String, Object> point : metric.getMetricItems()) {
|
|
|
+ try {
|
|
|
+ String timestampStr = point.get("timestamp").toString();
|
|
|
+ if (timestampStr.endsWith(".0")) {
|
|
|
+ timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
|
|
|
+ }
|
|
|
+ LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DATE_TIME_FORMATTER);
|
|
|
+ Double value = Double.parseDouble(point.get("value").toString());
|
|
|
+ timeValueMap.put(timestamp, value);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析历史数据失败,设备:{},指标:{},值:{}",
|
|
|
+ deviceUuid, metricName, point.get("value"), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return deviceMetricTimeValueMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 时间段用量 = 该时间段内最后一个读数 - 第一个读数(至少 2 条读数)
|
|
|
+ */
|
|
|
+ private double calculateSegmentUsage(TreeMap<LocalDateTime, Double> timeValueMap,
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endExclusive) {
|
|
|
+ if (timeValueMap == null || timeValueMap.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ TreeMap<LocalDateTime, Double> segmentData = extractSegmentReadings(timeValueMap, startInclusive, endExclusive);
|
|
|
+ if (segmentData.size() < 2) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ double firstValue = segmentData.firstEntry().getValue();
|
|
|
+ double lastValue = segmentData.lastEntry().getValue();
|
|
|
+ return formatUsageValue(calculateReadingDelta(firstValue, lastValue));
|
|
|
+ }
|
|
|
+
|
|
|
+ private double calculateReadingDelta(double firstValue, double lastValue) {
|
|
|
+ double usage = lastValue - firstValue;
|
|
|
+ if (usage < 0) {
|
|
|
+ return lastValue;
|
|
|
+ }
|
|
|
+ return usage;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double formatUsageValue(double usage) {
|
|
|
+ return toUsageDecimal(usage).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final int USAGE_SCALE = 2;
|
|
|
+
|
|
|
+ private BigDecimal toUsageDecimal(Number value) {
|
|
|
+ if (value == null) {
|
|
|
+ return BigDecimal.ZERO.setScale(USAGE_SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return BigDecimal.valueOf(value.doubleValue()).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal addUsage(Number a, Number b) {
|
|
|
+ return toUsageDecimal(a).add(toUsageDecimal(b)).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double sumUsageValues(Iterable<? extends Number> values) {
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ if (values != null) {
|
|
|
+ for (Number value : values) {
|
|
|
+ sum = sum.add(toUsageDecimal(value));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return sum.setScale(USAGE_SCALE, RoundingMode.HALF_UP).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Number> createEmptySpaceTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ Map<String, Number> data = new LinkedHashMap<>();
|
|
|
+ for (String label : generateTimeLabels(timeType, startTime, endTime)) {
|
|
|
+ data.put(label, 0);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 报表时间分段(左闭右开),与 columnList 标签一一对应
|
|
|
+ */
|
|
|
+ private static class TimeSegmentBound {
|
|
|
+ private final String label;
|
|
|
+ private final LocalDateTime startInclusive;
|
|
|
+ private final LocalDateTime endExclusive;
|
|
|
+
|
|
|
+ TimeSegmentBound(String label, LocalDateTime startInclusive, LocalDateTime endExclusive) {
|
|
|
+ this.label = label;
|
|
|
+ this.startInclusive = startInclusive;
|
|
|
+ this.endExclusive = endExclusive;
|
|
|
+ }
|
|
|
+
|
|
|
+ String getLabel() {
|
|
|
+ return label;
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime getStartInclusive() {
|
|
|
+ return startInclusive;
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime getEndExclusive() {
|
|
|
+ return endExclusive;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<TimeSegmentBound> buildReportTimeSegments(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<TimeSegmentBound> segments = new ArrayList<>();
|
|
|
+ if (startTime == null || endTime == null || !startTime.isBefore(endTime)) {
|
|
|
+ return segments;
|
|
|
+ }
|
|
|
+
|
|
|
+ String type = timeType != null ? timeType.toLowerCase() : "date";
|
|
|
+ switch (type) {
|
|
|
+ case "date":
|
|
|
+ buildHourSegments(segments, startTime, endTime);
|
|
|
+ break;
|
|
|
+ case "month":
|
|
|
+ buildDaySegments(segments, startTime, endTime);
|
|
|
+ break;
|
|
|
+ case "year":
|
|
|
+ buildMonthSegments(segments, startTime, endTime);
|
|
|
+ break;
|
|
|
+ case "custom":
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ buildHourSegments(segments, startTime, endTime);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return segments;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** date:按小时分段 [HH:00, HH+1:00),与传参起止小时一致 */
|
|
|
+ private void buildHourSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ int startHour = startTime.getHour();
|
|
|
+ int endHour = endTime.getHour();
|
|
|
+ for (int hour = startHour; hour <= endHour; hour++) {
|
|
|
+ String label = hour + "时";
|
|
|
+ LocalDateTime hourStart = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
|
|
|
+ LocalDateTime hourEndExclusive = hourStart.plusHours(1);
|
|
|
+ LocalDateTime segmentStart = hourStart.isBefore(startTime) ? startTime : hourStart;
|
|
|
+ LocalDateTime segmentEndExclusive = hourEndExclusive.isAfter(endTime) ? endTime : hourEndExclusive;
|
|
|
+ if (segmentStart.isBefore(segmentEndExclusive)) {
|
|
|
+ segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
|
|
|
+ } else {
|
|
|
+ segments.add(new TimeSegmentBound(label, segmentStart, segmentStart.plusNanos(1)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** month:按自然日分段 [00:00, 次日00:00) */
|
|
|
+ private void buildDaySegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ LocalDate current = startTime.toLocalDate();
|
|
|
+ LocalDate lastDate = resolveLastDateInRange(startTime, endTime);
|
|
|
+ while (!current.isAfter(lastDate)) {
|
|
|
+ String label = String.format("%02d日", current.getDayOfMonth());
|
|
|
+ LocalDateTime dayStart = current.atStartOfDay();
|
|
|
+ LocalDateTime dayEndExclusive = current.plusDays(1).atStartOfDay();
|
|
|
+ LocalDateTime segmentStart = dayStart.isBefore(startTime) ? startTime : dayStart;
|
|
|
+ LocalDateTime segmentEndExclusive = dayEndExclusive.isAfter(endTime) ? endTime : dayEndExclusive;
|
|
|
+ if (segmentStart.isBefore(segmentEndExclusive)) {
|
|
|
+ segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
|
|
|
+ }
|
|
|
+ current = current.plusDays(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** year:按自然月分段 [月初00:00, 下月初00:00) */
|
|
|
+ private void buildMonthSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ int year = startTime.getYear();
|
|
|
+ int month = startTime.getMonthValue();
|
|
|
+ int endYear = endTime.getYear();
|
|
|
+ int endMonth = endTime.getMonthValue();
|
|
|
+
|
|
|
+ while (year < endYear || (year == endYear && month <= endMonth)) {
|
|
|
+ String label = month + "月";
|
|
|
+ LocalDateTime monthStart = LocalDateTime.of(year, month, 1, 0, 0, 0);
|
|
|
+ LocalDateTime monthEndExclusive = monthStart.plusMonths(1);
|
|
|
+ LocalDateTime segmentStart = monthStart.isBefore(startTime) ? startTime : monthStart;
|
|
|
+ LocalDateTime segmentEndExclusive = monthEndExclusive.isAfter(endTime) ? endTime : monthEndExclusive;
|
|
|
+ if (segmentStart.isBefore(segmentEndExclusive)) {
|
|
|
+ segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
|
|
|
+ }
|
|
|
+ month++;
|
|
|
+ if (month > 12) {
|
|
|
+ month = 1;
|
|
|
+ year++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 结束时间为某日 00:00:00 时,表示不含该日,最后一天取前一日
|
|
|
+ */
|
|
|
+ private LocalDate resolveLastDateInRange(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ LocalDate endDate = endTime.toLocalDate();
|
|
|
+ if (endTime.toLocalTime().equals(LocalTime.MIDNIGHT) && endDate.isAfter(startTime.toLocalDate())) {
|
|
|
+ return endDate.minusDays(1);
|
|
|
+ }
|
|
|
+ return endDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toSpaceReportColumnProp(String label, String timeType) {
|
|
|
+ String type = timeType != null ? timeType.toLowerCase() : "date";
|
|
|
+ if ("year".equals(type)) {
|
|
|
+ return "_" + label.replace("月", "");
|
|
|
+ }
|
|
|
+ return "_" + label;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toSpaceReportValueKey(String label) {
|
|
|
+ String numPart = label.replace("时", "").replace("日", "").replace("月", "");
|
|
|
+ try {
|
|
|
+ return "_" + Integer.parseInt(numPart);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return "_" + numPart;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询所有区域id(包含子区域id)
|
|
|
+ */
|
|
|
+ private List<Long> spaceIds(List<Long> spaceId) {
|
|
|
+ return baseSpaceMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<BaseSpace>()
|
|
|
+ .in(BaseSpace::getId, spaceId)
|
|
|
+ .or().in(BaseSpace::getParentId, spaceId)
|
|
|
+ ).stream().map(BaseSpace::getId).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过energy_type获取product_id
|
|
|
+ */
|
|
|
+ private Long getProductIdByEnergyType(Integer energyType) {
|
|
|
+ return emsProductEnergyTypeMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<EmsProductEnergyType>()
|
|
|
+ .eq(EmsProductEnergyType::getEnergyType, energyType)
|
|
|
+ ).getProductId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过device_uuid按item_code分组
|
|
|
+ */
|
|
|
+ private Map<String, List<String>> getItemCodeByDeviceUuid(List<String> deviceUuids) {
|
|
|
+ return emsDeviceItemCodeMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<EmsDeviceItemCode>()
|
|
|
+ .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
|
|
|
+ ).stream().collect(Collectors.groupingBy(
|
|
|
+ EmsDeviceItemCode::getItemCode,
|
|
|
+ Collectors.mapping(
|
|
|
+ EmsDeviceItemCode::getDeviceUuid,
|
|
|
+ Collectors.toList()
|
|
|
+ )
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过device_uuid获取energy_type
|
|
|
+ */
|
|
|
+ private Integer getEnergyTypeByDeviceUuid(String deviceUuid) {
|
|
|
+ return emsDeviceItemCodeMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<EmsDeviceItemCode>()
|
|
|
+ .eq(EmsDeviceItemCode::getDeviceUuid, deviceUuid)
|
|
|
+ ).get(0).getEnergyType();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据网关id获取设备UUID列表
|
|
|
+ */
|
|
|
+ private List<DmpDevice> getDeviceUuidsByGatewayIds(List<String> gatewayIds) {
|
|
|
+ return new ArrayList<>(dmpDeviceMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<DmpDevice>()
|
|
|
+ .in(DmpDevice::getGatewayUuid, gatewayIds)));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据区域id获取网关UUID列表
|
|
|
+ */
|
|
|
+ private List<BaseSpaceGateway> getGatewayUuidsBySpaceIds(List<Long> spaceIds) {
|
|
|
+ return new ArrayList<>(baseSpaceGatewayMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<BaseSpaceGateway>()
|
|
|
+ .in(BaseSpaceGateway::getSpaceId, spaceIds)));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算区域报表总计(适配二维数组结构)
|
|
|
+ */
|
|
|
+ private Number calculateSpaceReportTotal(List<List<Map<String, Object>>> groupedValueList) {
|
|
|
+ if (groupedValueList == null || groupedValueList.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Number> rowTotals = new ArrayList<>();
|
|
|
+ for (List<Map<String, Object>> group : groupedValueList) {
|
|
|
+ if (group == null || group.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (Map<String, Object> row : group) {
|
|
|
+ Object total = row.get("total");
|
|
|
+ if (total instanceof Number) {
|
|
|
+ rowTotals.add((Number) total);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return sumUsageValues(rowTotals);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 通用工具方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验时间参数
|
|
|
+ * 1. 结束时间不得早于开始时间
|
|
|
+ * 2. 按日查询:起止时间必须是同一天
|
|
|
+ * 3. 按月查询:起止时间必须是同一月
|
|
|
+ * 4. 按年查询:起止时间必须是同一年
|
|
|
+ */
|
|
|
+ private void validateTimeParams(LocalDateTime startTime, LocalDateTime endTime, String timeType) {
|
|
|
+ // 基本校验:结束时间不得早于开始时间
|
|
|
+ if (endTime.isBefore(startTime)) {
|
|
|
+ throw new BusinessException("结束时间不得早于开始时间");
|
|
|
+ }
|
|
|
+
|
|
|
+ String type = timeType != null ? timeType.toLowerCase() : "date";
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case "date":
|
|
|
+ // 按日查询:必须是同一天
|
|
|
+ if (!startTime.toLocalDate().isEqual(endTime.toLocalDate())) {
|
|
|
+ throw new BusinessException("按日查询时,起止时间必须是同一天");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "month":
|
|
|
+ // 按月查询:必须是同一月
|
|
|
+ if (startTime.getYear() != endTime.getYear() ||
|
|
|
+ startTime.getMonthValue() != endTime.getMonthValue()) {
|
|
|
+ throw new BusinessException("按月查询时,起止时间必须是同一月");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "year":
|
|
|
+ // 按年查询:必须是同一年
|
|
|
+ if (startTime.getYear() != endTime.getYear()) {
|
|
|
+ throw new BusinessException("按年查询时,起止时间必须是同一年");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "custom":
|
|
|
+ // 自定义查询:不进行时间校验
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new BusinessException("查询时间类型错误!请重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建列定义
|
|
|
+ */
|
|
|
+ private EnergyReportResponse.ColumnItem createColumn(String label, String prop, Boolean fixed) {
|
|
|
+ EnergyReportResponse.ColumnItem column = new EnergyReportResponse.ColumnItem();
|
|
|
+ column.setLabel(label);
|
|
|
+ column.setProp(prop);
|
|
|
+ column.setFixed(fixed);
|
|
|
+ column.setChildren(null);
|
|
|
+ return column;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建标题项
|
|
|
+ */
|
|
|
+ private ItemReportResponse.TitleItem createTitleItem(String prop, Boolean fixed, String label, Integer value) {
|
|
|
+ ItemReportResponse.TitleItem item = new ItemReportResponse.TitleItem();
|
|
|
+ item.setProp(prop);
|
|
|
+ item.setFixed(fixed);
|
|
|
+ item.setLabel(label);
|
|
|
+ item.setValue(value);
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成时间标签列表
|
|
|
+ * 规则:
|
|
|
+ * - 按日(date)查询 → 返回小时数据(根据起止时间的小时范围)
|
|
|
+ * - 按月(month)查询 → 返回日数据(01日-31日)
|
|
|
+ * - 按年(year)查询 → 返回月数据(1月-12月)
|
|
|
+ */
|
|
|
+ private List<String> generateTimeLabels(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+ return buildReportTimeSegments(timeType, startTime, endTime).stream()
|
|
|
+ .map(TimeSegmentBound::getLabel)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析日期时间字符串
|
|
|
+ */
|
|
|
+ private LocalDateTime parseDateTime(String dateTimeStr) {
|
|
|
+ if (!StringUtils.hasText(dateTimeStr)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取设备UUID列表
|
|
|
+ */
|
|
|
+ private List<String> getUuidList(List<Integer> deviceIds) {
|
|
|
+ return deviceIds.stream().map(deviceId -> {
|
|
|
+ DmpDevice device = dmpDeviceMapper.selectById(deviceId);
|
|
|
+ return device != null ? device.getDeviceUuid() : null;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isHistoryDataEmpty(ApiResult<?> result) {
|
|
|
+ return result == null
|
|
|
+ || !result.isSuccess()
|
|
|
+ || result.getStatus() != ApiResult.ResultStatus.SUCCESS
|
|
|
+ || result.getData() == null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 报表 Excel 导出 ====================
|
|
|
+
|
|
|
+ private String buildExportFileName(String prefix) {
|
|
|
+ return prefix + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveEnergyCellValue(EnergyReportResponse.ValueItem item,
|
|
|
+ EnergyReportResponse.ColumnItem column) {
|
|
|
+ if (item == null || column == null || !StringUtils.hasText(column.getProp())) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ String prop = column.getProp();
|
|
|
+ switch (prop) {
|
|
|
+ case "deviceName":
|
|
|
+ return formatExportCellValue(item.getDeviceName());
|
|
|
+ case "commAddress":
|
|
|
+ return formatExportCellValue(item.getCommAddress());
|
|
|
+ case "identifier":
|
|
|
+ return formatExportCellValue(item.getIdentifier());
|
|
|
+ case "total":
|
|
|
+ return formatExportCellValue(item.getTotal());
|
|
|
+ default:
|
|
|
+ Map<String, String> timeData = item.getTimeData();
|
|
|
+ if (timeData != null && timeData.containsKey(prop)) {
|
|
|
+ return formatExportCellValue(timeData.get(prop));
|
|
|
+ }
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveSpaceCellValue(Map<String, Object> rowMap, EnergyReportResponse.ColumnItem column) {
|
|
|
+ if (rowMap == null || column == null || !StringUtils.hasText(column.getProp())) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ Object value = rowMap.get(column.getProp());
|
|
|
+ if (value == null && StringUtils.hasText(column.getLabel())) {
|
|
|
+ value = rowMap.get(toSpaceReportValueKey(column.getLabel()));
|
|
|
+ }
|
|
|
+ return formatExportCellValue(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatExportCellValue(Object value) {
|
|
|
+ return value == null ? "" : value.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeEnergyReportExcel(HttpServletResponse response,
|
|
|
+ String fileName,
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns,
|
|
|
+ List<List<EnergyReportResponse.ValueItem>> groupedRows) {
|
|
|
+ try (Workbook workbook = new XSSFWorkbook()) {
|
|
|
+ Sheet sheet = workbook.createSheet("能源报表");
|
|
|
+ CellStyle headerStyle = createExportHeaderStyle(workbook);
|
|
|
+ CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
|
|
|
+ CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);
|
|
|
+
|
|
|
+ writeExportHeaderRow(sheet, columns, headerStyle);
|
|
|
+
|
|
|
+ int deviceNameCol = findColumnIndex(columns, "deviceName");
|
|
|
+ int commAddressCol = findColumnIndex(columns, "commAddress");
|
|
|
+ int rowIndex = 1;
|
|
|
+ for (List<EnergyReportResponse.ValueItem> deviceRows : groupedRows) {
|
|
|
+ if (deviceRows == null || deviceRows.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int groupStartRow = rowIndex;
|
|
|
+ for (EnergyReportResponse.ValueItem item : deviceRows) {
|
|
|
+ Row row = sheet.createRow(rowIndex++);
|
|
|
+ writeEnergyReportDataRow(row, item, columns, textStyle, numberStyle);
|
|
|
+ }
|
|
|
+ int groupEndRow = rowIndex - 1;
|
|
|
+ if (groupEndRow > groupStartRow) {
|
|
|
+ mergeExportColumn(sheet, groupStartRow, groupEndRow, deviceNameCol);
|
|
|
+ mergeExportColumn(sheet, groupStartRow, groupEndRow, commAddressCol);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ autoSizeExportColumns(sheet, columns.size());
|
|
|
+ writeExcelToResponse(response, fileName, workbook);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new BusinessException("导出失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeSpaceReportExcel(HttpServletResponse response,
|
|
|
+ String fileName,
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns,
|
|
|
+ List<List<Map<String, Object>>> groupedRows) {
|
|
|
+ try (Workbook workbook = new XSSFWorkbook()) {
|
|
|
+ Sheet sheet = workbook.createSheet("区域报表");
|
|
|
+ CellStyle headerStyle = createExportHeaderStyle(workbook);
|
|
|
+ CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
|
|
|
+ CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);
|
|
|
+
|
|
|
+ writeExportHeaderRow(sheet, columns, headerStyle);
|
|
|
+
|
|
|
+ int spaceNameCol = findColumnIndex(columns, "spaceName");
|
|
|
+ int rowIndex = 1;
|
|
|
+ for (List<Map<String, Object>> spaceRows : groupedRows) {
|
|
|
+ if (spaceRows == null || spaceRows.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int groupStartRow = rowIndex;
|
|
|
+ for (Map<String, Object> rowMap : spaceRows) {
|
|
|
+ Row row = sheet.createRow(rowIndex++);
|
|
|
+ writeSpaceReportDataRow(row, rowMap, columns, textStyle, numberStyle);
|
|
|
+ }
|
|
|
+ int groupEndRow = rowIndex - 1;
|
|
|
+ if (groupEndRow > groupStartRow) {
|
|
|
+ mergeExportColumn(sheet, groupStartRow, groupEndRow, spaceNameCol);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ autoSizeExportColumns(sheet, columns.size());
|
|
|
+ writeExcelToResponse(response, fileName, workbook);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new BusinessException("导出失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeExportHeaderRow(Sheet sheet,
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns,
|
|
|
+ CellStyle headerStyle) {
|
|
|
+ Row headerRow = sheet.createRow(0);
|
|
|
+ for (int i = 0; i < columns.size(); i++) {
|
|
|
+ Cell cell = headerRow.createCell(i);
|
|
|
+ cell.setCellValue(columns.get(i).getLabel());
|
|
|
+ cell.setCellStyle(headerStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeEnergyReportDataRow(Row row,
|
|
|
+ EnergyReportResponse.ValueItem item,
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns,
|
|
|
+ CellStyle textStyle,
|
|
|
+ CellStyle numberStyle) {
|
|
|
+ for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
|
|
|
+ EnergyReportResponse.ColumnItem column = columns.get(colIndex);
|
|
|
+ Cell cell = row.createCell(colIndex);
|
|
|
+ cell.setCellValue(resolveEnergyCellValue(item, column));
|
|
|
+ cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeSpaceReportDataRow(Row row,
|
|
|
+ Map<String, Object> rowMap,
|
|
|
+ List<EnergyReportResponse.ColumnItem> columns,
|
|
|
+ CellStyle textStyle,
|
|
|
+ CellStyle numberStyle) {
|
|
|
+ for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
|
|
|
+ EnergyReportResponse.ColumnItem column = columns.get(colIndex);
|
|
|
+ Cell cell = row.createCell(colIndex);
|
|
|
+ cell.setCellValue(resolveSpaceCellValue(rowMap, column));
|
|
|
+ cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isExportTextColumn(EnergyReportResponse.ColumnItem column) {
|
|
|
+ if (column == null || !StringUtils.hasText(column.getProp())) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ switch (column.getProp()) {
|
|
|
+ case "deviceName":
|
|
|
+ case "commAddress":
|
|
|
+ case "identifier":
|
|
|
+ case "spaceName":
|
|
|
+ case "itemName":
|
|
|
+ return true;
|
|
|
+ default:
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findColumnIndex(List<EnergyReportResponse.ColumnItem> columns, String prop) {
|
|
|
+ for (int i = 0; i < columns.size(); i++) {
|
|
|
+ if (prop.equals(columns.get(i).getProp())) {
|
|
|
+ return i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void mergeExportColumn(Sheet sheet, int firstRow, int lastRow, int columnIndex) {
|
|
|
+ if (columnIndex < 0 || firstRow >= lastRow) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, columnIndex, columnIndex));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void autoSizeExportColumns(Sheet sheet, int columnCount) {
|
|
|
+ for (int i = 0; i < columnCount; i++) {
|
|
|
+ sheet.autoSizeColumn(i);
|
|
|
+ int width = sheet.getColumnWidth(i);
|
|
|
+ sheet.setColumnWidth(i, Math.min(width + 512, 255 * 256));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeExcelToResponse(HttpServletResponse response, String fileName, Workbook workbook)
|
|
|
+ throws IOException {
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
+ response.setCharacterEncoding("utf-8");
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
+ "attachment; filename=\"" + encodeExportFileName(fileName) + ".xlsx\"");
|
|
|
+ workbook.write(response.getOutputStream());
|
|
|
+ response.getOutputStream().flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String encodeExportFileName(String fileName) {
|
|
|
+ try {
|
|
|
+ return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private CellStyle createExportHeaderStyle(Workbook workbook) {
|
|
|
+ CellStyle style = createBorderedCellStyle(workbook);
|
|
|
+ Font font = workbook.createFont();
|
|
|
+ font.setBold(true);
|
|
|
+ style.setFont(font);
|
|
|
+ style.setAlignment(HorizontalAlignment.CENTER);
|
|
|
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
+ style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
|
|
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
|
|
+ return style;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CellStyle createExportDataStyle(Workbook workbook, HorizontalAlignment alignment) {
|
|
|
+ CellStyle style = createBorderedCellStyle(workbook);
|
|
|
+ style.setAlignment(alignment);
|
|
|
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
|
|
|
+ return style;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CellStyle createBorderedCellStyle(Workbook workbook) {
|
|
|
+ CellStyle style = workbook.createCellStyle();
|
|
|
+ style.setBorderTop(BorderStyle.THIN);
|
|
|
+ style.setBorderBottom(BorderStyle.THIN);
|
|
|
+ style.setBorderLeft(BorderStyle.THIN);
|
|
|
+ style.setBorderRight(BorderStyle.THIN);
|
|
|
+ return style;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 空响应创建方法 ====================
|
|
|
+
|
|
|
+ private EnergyReportResponse createEmptyEnergyReport() {
|
|
|
+ EnergyReportResponse response = new EnergyReportResponse();
|
|
|
+ response.setColumnList(new ArrayList<>());
|
|
|
+ response.setValueList(new ArrayList<>());
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ItemReportResponse createEmptyItemReport() {
|
|
|
+ ItemReportResponse response = new ItemReportResponse();
|
|
|
+ response.setTitleList(new ArrayList<>());
|
|
|
+ response.setDataList(new ArrayList<>());
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ private SpaceReportResponse createEmptySpaceReport() {
|
|
|
+ SpaceReportResponse response = new SpaceReportResponse();
|
|
|
+ response.setColumnList(new ArrayList<>());
|
|
|
+ response.setValueList(new ArrayList<>());
|
|
|
+ response.setTotal(0);
|
|
|
+ return response;
|
|
|
+ }
|
|
|
}
|