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 HOUR_TO_DAY = tk -> tk != null && tk.length() >= 8 ? tk.substring(0, 8) : null; public static final Function HOUR_TO_HOUR = tk -> normalizeHourKey(tk); public static final Function HOUR_TO_MONTH = tk -> tk != null && tk.length() >= 6 ? tk.substring(0, 6) : null; public static final Function HOUR_TO_YEAR = tk -> tk != null && tk.length() >= 4 ? tk.substring(0, 4) : null; public static final Function HOUR_TO_QUARTER = tk -> { String monthKey = HOUR_TO_MONTH.apply(tk); return monthKey != null ? toQuarterKey(monthKey) : null; }; /** * 按时间桶汇总(多电表合并,用于图表) */ public static List aggregateHourToBuckets( List hourRecords, Function bucketFn) { Map 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 aggregateHourPerMeter( List hourRecords, Function bucketFn) { Map 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()); agg.setAddress(record.getAddress()); agg.setCollectorNo(record.getCollectorNo()); } return sorted(map); } /** * 兼容:按 timeKey 直接汇总 */ public static List aggregateByTimeKey(List records) { Map 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 "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 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 bucketFn(String dimension) { return switch (dimension) { case "hour" -> HOUR_TO_HOUR; case "day" -> HOUR_TO_DAY; 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 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 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 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 sorted(Map map) { return map.values().stream() .peek(vo -> normalizeConsumptions(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 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; } } }