package com.ruoyi.http.util;
|
|
import com.ruoyi.http.vo.StatisticEleRecordVo;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.time.YearMonth;
|
import java.time.format.DateTimeFormatter;
|
import java.util.Comparator;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
/**
|
* 电量统计聚合工具(基于小时数据向上汇总)
|
*/
|
public final class StatisticEleAggregateUtil {
|
|
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
private static final int CONSUMPTION_SCALE = StatisticEleReadingUtil.CONSUMPTION_SCALE;
|
private static final int SUMMARY_SCALE = 2;
|
|
private StatisticEleAggregateUtil() {
|
}
|
|
public static final Function<String, String> HOUR_TO_DAY = tk -> tk != null && tk.length() >= 8 ? tk.substring(0, 8) : null;
|
public static final Function<String, String> HOUR_TO_HOUR = tk -> normalizeHourKey(tk);
|
public static final Function<String, String> HOUR_TO_MONTH = tk -> tk != null && tk.length() >= 6 ? tk.substring(0, 6) : null;
|
public static final Function<String, String> HOUR_TO_YEAR = tk -> tk != null && tk.length() >= 4 ? tk.substring(0, 4) : null;
|
public static final Function<String, String> HOUR_TO_QUARTER = tk -> {
|
String monthKey = HOUR_TO_MONTH.apply(tk);
|
return monthKey != null ? toQuarterKey(monthKey) : null;
|
};
|
public static final Function<String, String> HOUR_TO_WEEK = StatisticEleAnalyticsUtil.HOUR_TO_WEEK;
|
|
/**
|
* 按时间桶汇总(多电表合并,用于图表)
|
*/
|
public static List<StatisticEleRecordVo> aggregateHourToBuckets(
|
List<StatisticEleRecordVo> hourRecords, Function<String, String> bucketFn) {
|
Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
|
for (StatisticEleRecordVo record : hourRecords) {
|
String bucket = bucketFn.apply(record.getTimeKey());
|
if (bucket == null) {
|
continue;
|
}
|
mergeInto(map, bucket, null, record);
|
}
|
return sorted(map);
|
}
|
|
/**
|
* 按时间桶+电表汇总(用于明细)
|
*/
|
public static List<StatisticEleRecordVo> aggregateHourPerMeter(
|
List<StatisticEleRecordVo> hourRecords, Function<String, String> bucketFn) {
|
Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
|
for (StatisticEleRecordVo record : hourRecords) {
|
String bucket = bucketFn.apply(record.getTimeKey());
|
if (bucket == null || record.getMeterId() == null) {
|
continue;
|
}
|
String key = bucket + "_" + record.getMeterId();
|
mergeInto(map, key, bucket, record);
|
StatisticEleRecordVo agg = map.get(key);
|
agg.setMeterId(record.getMeterId());
|
mergeMeterInfo(agg, record);
|
}
|
return sorted(map);
|
}
|
|
private static void mergeMeterInfo(StatisticEleRecordVo agg, StatisticEleRecordVo record) {
|
if (hasText(record.getMeterName())) {
|
agg.setMeterName(record.getMeterName());
|
}
|
if (hasText(record.getAddress())) {
|
agg.setAddress(record.getAddress());
|
}
|
mergeTimeRange(agg, record);
|
}
|
|
private static void mergeTimeRange(StatisticEleRecordVo agg, StatisticEleRecordVo record) {
|
if (hasText(record.getStartTime())) {
|
if (!hasText(agg.getStartTime()) || record.getStartTime().compareTo(agg.getStartTime()) < 0) {
|
agg.setStartTime(record.getStartTime());
|
}
|
}
|
if (hasText(record.getEndTime())) {
|
if (!hasText(agg.getEndTime()) || record.getEndTime().compareTo(agg.getEndTime()) > 0) {
|
agg.setEndTime(record.getEndTime());
|
}
|
}
|
}
|
|
/** 汇总后若起止时间为空,按 timeKey 推导 */
|
private static void fillTimeRangeIfEmpty(StatisticEleRecordVo vo) {
|
if (hasText(vo.getStartTime()) && hasText(vo.getEndTime())) {
|
return;
|
}
|
String timeKey = vo.getTimeKey();
|
if (!hasText(timeKey)) {
|
return;
|
}
|
if (timeKey.contains("Q")) {
|
String[] parts = timeKey.split("Q");
|
if (parts.length != 2) {
|
return;
|
}
|
int year = Integer.parseInt(parts[0]);
|
int quarter = Integer.parseInt(parts[1]);
|
int startMonth = (quarter - 1) * 3 + 1;
|
int endMonth = startMonth + 2;
|
YearMonth endYm = YearMonth.of(year, endMonth);
|
vo.setStartTime(String.format("%04d-%02d-01 00:00:00", year, startMonth));
|
vo.setEndTime(String.format("%04d-%02d-%02d 23:59:59", year, endMonth, endYm.lengthOfMonth()));
|
return;
|
}
|
if (timeKey.length() == 4) {
|
vo.setStartTime(timeKey + "-01-01 00:00:00");
|
vo.setEndTime(timeKey + "-12-31 23:59:59");
|
return;
|
}
|
if (timeKey.length() == 6) {
|
YearMonth ym = YearMonth.parse(timeKey, DateTimeFormatter.ofPattern("yyyyMM"));
|
vo.setStartTime(String.format("%04d-%02d-01 00:00:00", ym.getYear(), ym.getMonthValue()));
|
vo.setEndTime(String.format("%04d-%02d-%02d 23:59:59",
|
ym.getYear(), ym.getMonthValue(), ym.lengthOfMonth()));
|
return;
|
}
|
if (timeKey.length() >= 8) {
|
String day = timeKey.substring(0, 8);
|
vo.setStartTime(toDateTime(day, "00:00:00"));
|
vo.setEndTime(toDateTime(day, "23:59:59"));
|
}
|
}
|
|
private static String toDateTime(String yyyyMMdd, String time) {
|
return yyyyMMdd.substring(0, 4) + "-"
|
+ yyyyMMdd.substring(4, 6) + "-"
|
+ yyyyMMdd.substring(6, 8) + " " + time;
|
}
|
|
private static boolean hasText(String value) {
|
return value != null && !value.isBlank();
|
}
|
|
/**
|
* 兼容:按 timeKey 直接汇总
|
*/
|
public static List<StatisticEleRecordVo> aggregateByTimeKey(List<StatisticEleRecordVo> records) {
|
Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
|
for (StatisticEleRecordVo record : records) {
|
String key = record.getTimeKey();
|
if (key == null) {
|
continue;
|
}
|
mergeInto(map, key, key, record);
|
}
|
return sorted(map);
|
}
|
|
public static String toQuarterKey(String monthOrDayKey) {
|
if (monthOrDayKey == null || monthOrDayKey.length() < 6) {
|
return null;
|
}
|
int year = Integer.parseInt(monthOrDayKey.substring(0, 4));
|
int month = Integer.parseInt(monthOrDayKey.substring(4, 6));
|
int quarter = (month - 1) / 3 + 1;
|
return year + "Q" + quarter;
|
}
|
|
/**
|
* 统计维度 -> 小时 time_key 查询范围
|
*/
|
public static HourRange toHourQueryRange(String dimension, String startTime, String endTime) {
|
return switch (dimension) {
|
case "hour" -> new HourRange(startTime, endTime);
|
case "day" -> new HourRange(startTime + "00", endTime + "23");
|
case "week" -> new HourRange(startTime + "00", endTime + "23");
|
case "month" -> new HourRange(startTime + "0100", endTime + lastDayOfMonth(endTime) + "23");
|
case "year" -> new HourRange(startTime + "010100", endTime + "123123");
|
case "quarter" -> new HourRange(
|
startTime.length() >= 8 ? startTime.substring(0, 8) + "00" : startTime + "00",
|
endTime.length() >= 8 ? endTime.substring(0, 8) + "23" : endTime + "23");
|
default -> throw new IllegalArgumentException("不支持的维度: " + dimension);
|
};
|
}
|
|
public static HourRange yesterdayHourRange() {
|
String day = LocalDate.now().minusDays(1).format(DAY_FMT);
|
return new HourRange(normalizeQueryStartTimeKey(day + "00"), normalizeQueryEndTimeKey(day + "23"));
|
}
|
|
/** 查询起始 time_key(统一 12 位,兼容 10 位小时键) */
|
public static String normalizeQueryStartTimeKey(String timeKey) {
|
if (timeKey == null || timeKey.isBlank()) {
|
return timeKey;
|
}
|
if (timeKey.length() == 8) {
|
return timeKey + "0000";
|
}
|
if (timeKey.length() == 10) {
|
return timeKey + "00";
|
}
|
return timeKey.length() > 12 ? timeKey.substring(0, 12) : timeKey;
|
}
|
|
/** 查询结束 time_key(统一 12 位,兼容 10 位小时键) */
|
public static String normalizeQueryEndTimeKey(String timeKey) {
|
if (timeKey == null || timeKey.isBlank()) {
|
return timeKey;
|
}
|
if (timeKey.length() == 8) {
|
return timeKey + "2359";
|
}
|
if (timeKey.length() == 10) {
|
return timeKey + "59";
|
}
|
return timeKey.length() > 12 ? timeKey.substring(0, 12) : timeKey;
|
}
|
|
/** 明细记录总用电量(与数据采集页求和方式一致) */
|
public static double sumRecordsTotal(List<StatisticEleRecordVo> records) {
|
BigDecimal total = records.stream()
|
.map(StatisticEleRecordVo::getTotalConsumption)
|
.filter(v -> v != null)
|
.map(BigDecimal::valueOf)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
return roundSummary(total);
|
}
|
|
public static Function<String, String> bucketFn(String dimension) {
|
return switch (dimension) {
|
case "hour" -> HOUR_TO_HOUR;
|
case "day" -> HOUR_TO_DAY;
|
case "week" -> HOUR_TO_WEEK;
|
case "month" -> HOUR_TO_MONTH;
|
case "quarter" -> HOUR_TO_QUARTER;
|
case "year" -> HOUR_TO_YEAR;
|
default -> StatisticEleAggregateUtil::normalizeHourKey;
|
};
|
}
|
|
public static String normalizeHourKey(String timeKey) {
|
if (timeKey == null) {
|
return null;
|
}
|
return timeKey.length() >= 10 ? timeKey.substring(0, 10) : timeKey;
|
}
|
|
public static StatisticEleSummaryMetrics calcMetrics(List<StatisticEleRecordVo> buckets) {
|
StatisticEleSummaryMetrics metrics = new StatisticEleSummaryMetrics();
|
metrics.setRecordCount(buckets.size());
|
if (buckets.isEmpty()) {
|
metrics.setTotalConsumption(0.0);
|
metrics.setAvgConsumption(0.0);
|
metrics.setMaxConsumption(0.0);
|
metrics.setMinConsumption(0.0);
|
return metrics;
|
}
|
List<BigDecimal> values = buckets.stream()
|
.map(StatisticEleRecordVo::getTotalConsumption)
|
.filter(v -> v != null)
|
.map(BigDecimal::valueOf)
|
.collect(Collectors.toList());
|
BigDecimal total = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
metrics.setTotalConsumption(roundSummary(total));
|
metrics.setAvgConsumption(roundSummary(total.divide(
|
BigDecimal.valueOf(values.size()), CONSUMPTION_SCALE, RoundingMode.HALF_UP)));
|
metrics.setMaxConsumption(roundSummary(values.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO)));
|
metrics.setMinConsumption(roundSummary(values.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO)));
|
return metrics;
|
}
|
|
private static void mergeInto(Map<String, StatisticEleRecordVo> map, String mapKey,
|
String timeKey, StatisticEleRecordVo record) {
|
StatisticEleRecordVo agg = map.computeIfAbsent(mapKey, k -> {
|
StatisticEleRecordVo vo = new StatisticEleRecordVo();
|
vo.setTimeKey(timeKey != null ? timeKey : k);
|
vo.setTotalConsumption(0.0);
|
vo.setSharpConsumption(0.0);
|
vo.setPeakConsumption(0.0);
|
vo.setFlatConsumption(0.0);
|
vo.setValleyConsumption(0.0);
|
return vo;
|
});
|
agg.setTotalConsumption(add(agg.getTotalConsumption(), record.getTotalConsumption()));
|
agg.setSharpConsumption(add(agg.getSharpConsumption(), record.getSharpConsumption()));
|
agg.setPeakConsumption(add(agg.getPeakConsumption(), record.getPeakConsumption()));
|
agg.setFlatConsumption(add(agg.getFlatConsumption(), record.getFlatConsumption()));
|
agg.setValleyConsumption(add(agg.getValleyConsumption(), record.getValleyConsumption()));
|
}
|
|
private static List<StatisticEleRecordVo> sorted(Map<String, StatisticEleRecordVo> map) {
|
return map.values().stream()
|
.peek(vo -> {
|
normalizeConsumptions(vo);
|
fillTimeRangeIfEmpty(vo);
|
})
|
.sorted(Comparator.comparing(StatisticEleRecordVo::getTimeKey))
|
.collect(Collectors.toList());
|
}
|
|
private static String lastDayOfMonth(String yyyyMM) {
|
YearMonth ym = YearMonth.parse(yyyyMM, DateTimeFormatter.ofPattern("yyyyMM"));
|
return yyyyMM + String.format("%02d", ym.lengthOfMonth());
|
}
|
|
private static Double add(Double a, Double b) {
|
BigDecimal sum = BigDecimal.valueOf(a == null ? 0.0 : a)
|
.add(BigDecimal.valueOf(b == null ? 0.0 : b));
|
return roundConsumption(sum);
|
}
|
|
/** 统一电量字段精度(明细展示) */
|
public static void normalizeConsumptions(StatisticEleRecordVo vo) {
|
if (vo == null) {
|
return;
|
}
|
vo.setTotalConsumption(roundConsumption(vo.getTotalConsumption()));
|
vo.setSharpConsumption(roundConsumption(vo.getSharpConsumption()));
|
vo.setPeakConsumption(roundConsumption(vo.getPeakConsumption()));
|
vo.setFlatConsumption(roundConsumption(vo.getFlatConsumption()));
|
vo.setValleyConsumption(roundConsumption(vo.getValleyConsumption()));
|
}
|
|
public static void normalizeConsumptions(List<StatisticEleRecordVo> records) {
|
if (records == null) {
|
return;
|
}
|
records.forEach(vo -> normalizeConsumptions(vo));
|
}
|
|
private static Double roundConsumption(Double value) {
|
if (value == null) {
|
return null;
|
}
|
return roundConsumption(BigDecimal.valueOf(value));
|
}
|
|
private static Double roundConsumption(BigDecimal value) {
|
if (value == null) {
|
return null;
|
}
|
return value.setScale(CONSUMPTION_SCALE, RoundingMode.HALF_UP).doubleValue();
|
}
|
|
private static double roundSummary(BigDecimal value) {
|
return value.setScale(SUMMARY_SCALE, RoundingMode.HALF_UP).doubleValue();
|
}
|
|
public record HourRange(String startTime, String endTime) {}
|
|
public static class StatisticEleSummaryMetrics {
|
private Double totalConsumption;
|
private Double avgConsumption;
|
private Double maxConsumption;
|
private Double minConsumption;
|
private Integer recordCount;
|
|
public Double getTotalConsumption() { return totalConsumption; }
|
public void setTotalConsumption(Double v) { this.totalConsumption = v; }
|
public Double getAvgConsumption() { return avgConsumption; }
|
public void setAvgConsumption(Double v) { this.avgConsumption = v; }
|
public Double getMaxConsumption() { return maxConsumption; }
|
public void setMaxConsumption(Double v) { this.maxConsumption = v; }
|
public Double getMinConsumption() { return minConsumption; }
|
public void setMinConsumption(Double v) { this.minConsumption = v; }
|
public Integer getRecordCount() { return recordCount; }
|
public void setRecordCount(Integer v) { this.recordCount = v; }
|
}
|
}
|