From d2ab6f7153e604bac7bc4ad58f27f368b65d8a1e Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期二, 16 六月 2026 13:54:58 +0800
Subject: [PATCH] feat: 添加能耗数据综合分析功能,支持按天和周维度的趋势分析

---
 src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java |  375 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 375 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java b/src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java
new file mode 100644
index 0000000..4ff014f
--- /dev/null
+++ b/src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java
@@ -0,0 +1,375 @@
+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; }
+    }
+}

--
Gitblit v1.9.3