|
|
@@ -14,6 +14,19 @@ 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;
|
|
|
@@ -21,6 +34,8 @@ 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;
|
|
|
@@ -40,6 +55,23 @@ 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
|
|
|
@@ -267,6 +299,26 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
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);
|
|
|
+ }
|
|
|
+
|
|
|
// ==================== 能源报表私有方法 ====================
|
|
|
|
|
|
/**
|
|
|
@@ -281,6 +333,11 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
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) {
|
|
|
@@ -311,14 +368,16 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
historyRequestVO.setStartTime(request.getStartTime());
|
|
|
historyRequestVO.setEndTime(request.getEndTime());
|
|
|
historyRequestVO.setMetrics(request.getFuncList() != null && !request.getFuncList().isEmpty()
|
|
|
- ? request.getFuncList().stream().map(EnergyReportRequest.FuncItem::getIdentifier).collect(Collectors.toList())
|
|
|
+ ? 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.warn("历史数据为空,设备uuid:{}", deviceUuids);
|
|
|
log.info("历史数据接口返回:{}", listHistoryData);
|
|
|
|
|
|
// 如果有数据,解析TSDB返回的结果
|
|
|
@@ -331,6 +390,8 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
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;
|
|
|
@@ -345,110 +406,358 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
|
|
|
// 如果没有功能点列表,为设备创建一个默认记录
|
|
|
if (request.getFuncList() == null || request.getFuncList().isEmpty()) {
|
|
|
- Map<String, String> timeData;
|
|
|
- String total;
|
|
|
-
|
|
|
- if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
- // 从TSDB数据中获取
|
|
|
- timeData = buildTimeDataFromTsdb(tsdbDataMap.get(deviceUuid), request.getTimeType(), startTime, endTime);
|
|
|
- total = calculateTotal(timeData);
|
|
|
+ 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 {
|
|
|
- // 生成空数据结构
|
|
|
- timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
|
|
|
- total = "-";
|
|
|
+ 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));
|
|
|
}
|
|
|
-
|
|
|
- EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
|
|
|
- item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
|
|
|
- item.setDeviceName(device.getName());
|
|
|
- item.setCommAddress(device.getCommAddress());
|
|
|
- item.setIdentifier("-");
|
|
|
- item.setTotal(total);
|
|
|
- item.setTimeData(timeData);
|
|
|
-
|
|
|
- deviceValueList.add(item);
|
|
|
} else {
|
|
|
- // 为每个功能点创建一条记录,添加到当前设备的子列表中
|
|
|
for (EnergyReportRequest.FuncItem func : request.getFuncList()) {
|
|
|
if (func == null || StringUtils.isEmpty(func.getIdentifier())) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- Map<String, String> timeData;
|
|
|
- String total;
|
|
|
-
|
|
|
- // 如果有历史数据,从真实数据中查询;否则使用空值
|
|
|
- if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
|
|
|
- // 从TSDB数据中获取指定功能点的数据
|
|
|
- timeData = buildTimeDataFromTsdbForMetric(
|
|
|
- tsdbDataMap.get(deviceUuid),
|
|
|
- func.getIdentifier(),
|
|
|
- request.getTimeType(),
|
|
|
- startTime,
|
|
|
- endTime);
|
|
|
- total = calculateTotal(timeData);
|
|
|
+ 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 {
|
|
|
- // 生成空数据结构
|
|
|
- timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
|
|
|
- total = "0";
|
|
|
+ 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));
|
|
|
}
|
|
|
-
|
|
|
- EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
|
|
|
- item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
|
|
|
- item.setDeviceName(device.getName());
|
|
|
- item.setCommAddress(device.getCommAddress());
|
|
|
- item.setIdentifier(func.getIdentifierName() != null ? func.getIdentifierName() : func.getIdentifier());
|
|
|
- item.setTotal(total);
|
|
|
- item.setTimeData(timeData);
|
|
|
-
|
|
|
- deviceValueList.add(item);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 将当前设备的所有功能点数据添加到外层列表
|
|
|
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) {
|
|
|
- String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
|
|
|
- data.put(key, "0"); // 空值填充
|
|
|
+ data.put(toEnergyTimeDataKey(label), ENERGY_ZERO);
|
|
|
}
|
|
|
-
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 计算合计值
|
|
|
+ * 计算合计:累计类为各时段差值之和,瞬时类为查询区间内全量读数平均
|
|
|
*/
|
|
|
- private String calculateTotal(Map<String, String> timeData) {
|
|
|
+ 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 "0";
|
|
|
+ return ENERGY_NO_DATA;
|
|
|
}
|
|
|
|
|
|
BigDecimal sum = BigDecimal.ZERO;
|
|
|
- boolean hasData = false;
|
|
|
+ boolean hasNumeric = false;
|
|
|
+ boolean allMissing = true;
|
|
|
for (String value : timeData.values()) {
|
|
|
- if (!"-".equals(value)) {
|
|
|
- try {
|
|
|
- sum = sum.add(new BigDecimal(value));
|
|
|
- hasData = true;
|
|
|
- } catch (NumberFormatException e) {
|
|
|
- // 忽略无效数据
|
|
|
- }
|
|
|
+ if (ENERGY_NO_DATA.equals(value)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ allMissing = false;
|
|
|
+ try {
|
|
|
+ sum = sum.add(new BigDecimal(value));
|
|
|
+ hasNumeric = true;
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ // 忽略无效数据
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return hasData ? sum.toString() : "-";
|
|
|
+ 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数据解析方法 ====================
|
|
|
@@ -514,28 +823,16 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从TSDB数据中构建时间数据(所有功能点)
|
|
|
+ * 从TSDB数据中构建时间数据(取第一个功能点)
|
|
|
*/
|
|
|
private Map<String, String> buildTimeDataFromTsdb(Map<String, List<Map<String, Object>>> metricDataMap,
|
|
|
String timeType, LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
- Map<String, String> timeData = new LinkedHashMap<>();
|
|
|
-
|
|
|
- List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
|
|
|
- for (String label : timeLabels) {
|
|
|
- String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
|
|
|
-
|
|
|
- // 如果有数据,取第一个功能点的数据作为示例
|
|
|
- if (!metricDataMap.isEmpty()) {
|
|
|
- // 获取第一个功能点的数据
|
|
|
- List<Map<String, Object>> firstMetricItems = metricDataMap.values().iterator().next();
|
|
|
- String value = findValueByTimestamp(firstMetricItems, label, timeType, startTime, endTime);
|
|
|
- timeData.put(key, value);
|
|
|
- } else {
|
|
|
- timeData.put(key, "-");
|
|
|
- }
|
|
|
+ if (metricDataMap == null || metricDataMap.isEmpty()) {
|
|
|
+ return generateEmptyTimeData(timeType, startTime, endTime);
|
|
|
}
|
|
|
-
|
|
|
- return timeData;
|
|
|
+ Map.Entry<String, List<Map<String, Object>>> firstMetric = metricDataMap.entrySet().iterator().next();
|
|
|
+ return buildEnergyTimeDataForMetricItems(
|
|
|
+ firstMetric.getValue(), firstMetric.getKey(), timeType, startTime, endTime);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -546,90 +843,15 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
String timeType,
|
|
|
LocalDateTime startTime,
|
|
|
LocalDateTime endTime) {
|
|
|
- Map<String, String> timeData = new LinkedHashMap<>();
|
|
|
-
|
|
|
- List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
|
|
|
-
|
|
|
- // 获取指定功能点的数据
|
|
|
- List<Map<String, Object>> metricItems = metricDataMap.get(metric);
|
|
|
-
|
|
|
- for (String label : timeLabels) {
|
|
|
- String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
|
|
|
-
|
|
|
- if (metricItems != null && !metricItems.isEmpty()) {
|
|
|
- String value = findValueByTimestamp(metricItems, label, timeType, startTime, endTime);
|
|
|
- timeData.put(key, value);
|
|
|
- } else {
|
|
|
- timeData.put(key, "-");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return timeData;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 根据时间标签查找对应的值
|
|
|
- * 按小时查询时,将小时范围划分为时间段,匹配对应时间点的数据
|
|
|
- */
|
|
|
- private String findValueByTimestamp(List<Map<String, Object>> metricItems,
|
|
|
- String timeLabel,
|
|
|
- String timeType,
|
|
|
- LocalDateTime startTime,
|
|
|
- LocalDateTime endTime) {
|
|
|
+ List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(metricDataMap, metric);
|
|
|
if (metricItems == null || metricItems.isEmpty()) {
|
|
|
- return "-";
|
|
|
- }
|
|
|
-
|
|
|
- // 解析时间标签,找到对应的时间点
|
|
|
- // 例如:"1时" -> 需要从数据中找到该小时范围内的第一个数据点
|
|
|
- try {
|
|
|
- int hour = Integer.parseInt(timeLabel.replace("时", ""));
|
|
|
- LocalDateTime targetTime = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
|
|
|
- LocalDateTime nextTime = targetTime.plusHours(1);
|
|
|
-
|
|
|
- // 查找该时间范围内的数据
|
|
|
- for (Map<String, Object> item : metricItems) {
|
|
|
- if (item.containsKey("timestamp") && item.containsKey("value")) {
|
|
|
- String timestampStr = (String) item.get("timestamp");
|
|
|
- if (StringUtils.hasText(timestampStr)) {
|
|
|
- LocalDateTime itemTime = parseDateTime(timestampStr);
|
|
|
- if (itemTime != null && !itemTime.isBefore(targetTime) && itemTime.isBefore(nextTime)) {
|
|
|
- // 找到匹配的数据,进行单位转换和精度处理
|
|
|
- String valueStr = (String) item.get("value");
|
|
|
- if (StringUtils.hasText(valueStr)) {
|
|
|
- try {
|
|
|
- BigDecimal value = new BigDecimal(valueStr);
|
|
|
- // 除以10000后保留两位小数
|
|
|
- value = value.divide(new BigDecimal("10000")).setScale(2, java.math.RoundingMode.HALF_UP);
|
|
|
- return value.toString();
|
|
|
- } catch (NumberFormatException e) {
|
|
|
- return "-";
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (NumberFormatException e) {
|
|
|
- // 如果解析失败,返回第一个数据点的值
|
|
|
- if (!metricItems.isEmpty()) {
|
|
|
- Map<String, Object> firstItem = metricItems.get(0);
|
|
|
- if (firstItem.containsKey("value")) {
|
|
|
- String valueStr = (String) firstItem.get("value");
|
|
|
- if (StringUtils.hasText(valueStr)) {
|
|
|
- try {
|
|
|
- BigDecimal value = new BigDecimal(valueStr);
|
|
|
- value = value.divide(new BigDecimal("10000")).setScale(2, java.math.RoundingMode.HALF_UP);
|
|
|
- return value.toString();
|
|
|
- } catch (NumberFormatException ex) {
|
|
|
- return "-";
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ Map<String, String> empty = new LinkedHashMap<>();
|
|
|
+ for (TimeSegmentBound segment : buildReportTimeSegments(timeType, startTime, endTime)) {
|
|
|
+ empty.put(toEnergyTimeDataKey(segment.getLabel()), ENERGY_NO_DATA);
|
|
|
}
|
|
|
+ return empty;
|
|
|
}
|
|
|
-
|
|
|
- return "-";
|
|
|
+ return buildEnergyTimeDataForMetricItems(metricItems, metric, timeType, startTime, endTime);
|
|
|
}
|
|
|
|
|
|
// ==================== 分项报表私有方法 ====================
|
|
|
@@ -967,8 +1189,8 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
}
|
|
|
|
|
|
private TreeMap<LocalDateTime, Double> extractSegmentReadings(TreeMap<LocalDateTime, Double> source,
|
|
|
- LocalDateTime startInclusive,
|
|
|
- LocalDateTime endExclusive) {
|
|
|
+ LocalDateTime startInclusive,
|
|
|
+ LocalDateTime endExclusive) {
|
|
|
TreeMap<LocalDateTime, Double> result = new TreeMap<>();
|
|
|
if (source == null || source.isEmpty() || !startInclusive.isBefore(endExclusive)) {
|
|
|
return result;
|
|
|
@@ -1438,6 +1660,248 @@ public class EmsReportServiceImpl implements EmsReportService {
|
|
|
|| 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() {
|