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/StatisticEleAnalyticsUtil.java |  288 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 288 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/http/util/StatisticEleAnalyticsUtil.java b/src/main/java/com/ruoyi/http/util/StatisticEleAnalyticsUtil.java
new file mode 100644
index 0000000..4a469ea
--- /dev/null
+++ b/src/main/java/com/ruoyi/http/util/StatisticEleAnalyticsUtil.java
@@ -0,0 +1,288 @@
+package com.ruoyi.http.util;
+
+import com.ruoyi.http.vo.StatisticEleComparisonVo;
+import com.ruoyi.http.vo.StatisticEleRecordVo;
+import com.ruoyi.http.vo.StatisticEleSplitItemVo;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.WeekFields;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * 鑳借�楃患鍚堝垎鏋愯绠�
+ */
+public final class StatisticEleAnalyticsUtil {
+
+    private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
+    private static final int SUMMARY_SCALE = 2;
+    /** 鐧界彮 08:00-19:59 */
+    private static final int DAY_SHIFT_START = 8;
+    private static final int DAY_SHIFT_END = 19;
+
+    private StatisticEleAnalyticsUtil() {
+    }
+
+    public static final Function<String, String> HOUR_TO_WEEK = tk -> {
+        if (tk == null || tk.length() < 8) {
+            return null;
+        }
+        LocalDate date = LocalDate.parse(tk.substring(0, 8), DAY_FMT);
+        WeekFields wf = WeekFields.of(Locale.CHINA);
+        int week = date.get(wf.weekOfWeekBasedYear());
+        int year = date.get(wf.weekBasedYear());
+        return String.format("%04dW%02d", year, week);
+    };
+
+    public static Function<String, String> trendBucketFn(String granularity) {
+        return switch (granularity) {
+            case "hour" -> StatisticEleAggregateUtil.HOUR_TO_HOUR;
+            case "week" -> HOUR_TO_WEEK;
+            case "month" -> StatisticEleAggregateUtil.HOUR_TO_MONTH;
+            case "year" -> StatisticEleAggregateUtil.HOUR_TO_YEAR;
+            default -> StatisticEleAggregateUtil.HOUR_TO_DAY;
+        };
+    }
+
+    /**
+     * 鍛ㄦ湡绱鏉ヨ嚜姹囨�绘《锛屾瀬鍊�/鍧囧�煎缁堟潵鑷皬鏃舵《銆�
+     */
+    public static StatisticEleAggregateUtil.StatisticEleSummaryMetrics calcCombinedMetrics(
+            List<StatisticEleRecordVo> hourRecords,
+            List<StatisticEleRecordVo> periodChartRecords) {
+        List<StatisticEleRecordVo> hourlyBuckets = StatisticEleAggregateUtil.aggregateHourToBuckets(
+                hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
+        StatisticEleAggregateUtil.StatisticEleSummaryMetrics hourMetrics =
+                StatisticEleAggregateUtil.calcMetrics(hourlyBuckets);
+        StatisticEleAggregateUtil.StatisticEleSummaryMetrics periodMetrics =
+                StatisticEleAggregateUtil.calcMetrics(periodChartRecords);
+
+        StatisticEleAggregateUtil.StatisticEleSummaryMetrics result =
+                new StatisticEleAggregateUtil.StatisticEleSummaryMetrics();
+        result.setRecordCount(periodMetrics.getRecordCount());
+        result.setTotalConsumption(hourMetrics.getTotalConsumption());
+        result.setAvgConsumption(hourMetrics.getAvgConsumption());
+        result.setMaxConsumption(hourMetrics.getMaxConsumption());
+        result.setMinConsumption(hourMetrics.getMinConsumption());
+        return result;
+    }
+
+    public static double calcLoadRate(Double avgHourly, Double maxHourly) {
+        if (avgHourly == null || maxHourly == null || maxHourly <= 0) {
+            return 0.0;
+        }
+        return roundSummary(BigDecimal.valueOf(avgHourly)
+                .divide(BigDecimal.valueOf(maxHourly), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
+                .multiply(BigDecimal.valueOf(100)));
+    }
+
+    public static List<StatisticEleSplitItemVo> calcPeriodSplits(List<StatisticEleRecordVo> hourRecords) {
+        double sharp = sumField(hourRecords, StatisticEleRecordVo::getSharpConsumption);
+        double peak = sumField(hourRecords, StatisticEleRecordVo::getPeakConsumption);
+        double flat = sumField(hourRecords, StatisticEleRecordVo::getFlatConsumption);
+        double valley = sumField(hourRecords, StatisticEleRecordVo::getValleyConsumption);
+        double total = sharp + peak + flat + valley;
+        if (total <= 0) {
+            total = sumField(hourRecords, StatisticEleRecordVo::getTotalConsumption);
+        }
+        List<StatisticEleSplitItemVo> items = new ArrayList<>();
+        addSplitItem(items, "灏�", sharp, total);
+        addSplitItem(items, "宄�", peak, total);
+        addSplitItem(items, "骞�", flat, total);
+        addSplitItem(items, "璋�", valley, total);
+        return items.stream().filter(i -> i.getConsumption() != null && i.getConsumption() > 0).toList();
+    }
+
+    public static List<StatisticEleSplitItemVo> calcShiftSplits(List<StatisticEleRecordVo> hourRecords) {
+        Map<String, Double> map = new LinkedHashMap<>();
+        map.put("鐧界彮", 0.0);
+        map.put("澶滅彮", 0.0);
+        for (StatisticEleRecordVo record : hourRecords) {
+            int hour = parseHour(record.getTimeKey());
+            if (hour < 0) {
+                continue;
+            }
+            double val = safe(record.getTotalConsumption());
+            if (hour >= DAY_SHIFT_START && hour <= DAY_SHIFT_END) {
+                map.merge("鐧界彮", val, Double::sum);
+            } else {
+                map.merge("澶滅彮", val, Double::sum);
+            }
+        }
+        return toSplitItems(map);
+    }
+
+    public static List<StatisticEleSplitItemVo> calcDayTypeSplits(List<StatisticEleRecordVo> hourRecords) {
+        Map<String, Double> map = new LinkedHashMap<>();
+        map.put("宸ヤ綔鏃�", 0.0);
+        map.put("浼戞伅鏃�", 0.0);
+        for (StatisticEleRecordVo record : hourRecords) {
+            LocalDate date = parseDate(record.getTimeKey());
+            if (date == null) {
+                continue;
+            }
+            double val = safe(record.getTotalConsumption());
+            DayOfWeek dow = date.getDayOfWeek();
+            if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
+                map.merge("浼戞伅鏃�", val, Double::sum);
+            } else {
+                map.merge("宸ヤ綔鏃�", val, Double::sum);
+            }
+        }
+        return toSplitItems(map);
+    }
+
+    public static StatisticEleComparisonVo buildComparison(
+            String type, String label, Double currentTotal, Double compareTotal) {
+        StatisticEleComparisonVo vo = new StatisticEleComparisonVo();
+        vo.setType(type);
+        vo.setLabel(label);
+        vo.setCurrentTotal(round(currentTotal));
+        vo.setCompareTotal(round(compareTotal));
+        double delta = round(safe(currentTotal) - safe(compareTotal));
+        vo.setDelta(delta);
+        if (compareTotal != null && compareTotal > 0) {
+            vo.setChangeRate(roundSummary(BigDecimal.valueOf(delta)
+                    .divide(BigDecimal.valueOf(compareTotal), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100))));
+        } else {
+            vo.setChangeRate(currentTotal != null && currentTotal > 0 ? 100.0 : 0.0);
+        }
+        return vo;
+    }
+
+    public record DateBounds(LocalDate start, LocalDate end) {}
+
+    public static DateBounds resolveBounds(String dimension, String startTime, String endTime) {
+        LocalDate start = resolveStartDate(dimension, startTime);
+        LocalDate end = resolveEndDate(dimension, endTime);
+        if (start.isAfter(end)) {
+            LocalDate tmp = start;
+            start = end;
+            end = tmp;
+        }
+        return new DateBounds(start, end);
+    }
+
+    public static DateBounds shiftChain(DateBounds bounds) {
+        long days = ChronoUnit.DAYS.between(bounds.start(), bounds.end()) + 1;
+        return new DateBounds(bounds.start().minusDays(days), bounds.start().minusDays(1));
+    }
+
+    public static DateBounds shiftYoy(DateBounds bounds) {
+        return new DateBounds(bounds.start().minusYears(1), bounds.end().minusYears(1));
+    }
+
+    public static String defaultTrendGranularity(String dimension, boolean singleDay) {
+        if (singleDay) {
+            return "hour";
+        }
+        return switch (dimension) {
+            case "week", "day" -> "day";
+            case "month", "quarter" -> "week";
+            case "year" -> "month";
+            default -> "day";
+        };
+    }
+
+    private static LocalDate resolveStartDate(String dimension, String startTime) {
+        return switch (dimension) {
+            case "year" -> LocalDate.of(Integer.parseInt(startTime), 1, 1);
+            case "month" -> YearMonth.parse(startTime, DateTimeFormatter.ofPattern("yyyyMM")).atDay(1);
+            default -> LocalDate.parse(normalizeDayKey(startTime), DAY_FMT);
+        };
+    }
+
+    private static LocalDate resolveEndDate(String dimension, String endTime) {
+        return switch (dimension) {
+            case "year" -> LocalDate.of(Integer.parseInt(endTime), 12, 31);
+            case "month" -> YearMonth.parse(endTime, DateTimeFormatter.ofPattern("yyyyMM")).atEndOfMonth();
+            default -> LocalDate.parse(normalizeDayKey(endTime), DAY_FMT);
+        };
+    }
+
+    private static String normalizeDayKey(String timeKey) {
+        if (timeKey == null) {
+            return LocalDate.now().format(DAY_FMT);
+        }
+        if (timeKey.length() == 4) {
+            return timeKey + "0101";
+        }
+        if (timeKey.length() == 6) {
+            return timeKey + "01";
+        }
+        return timeKey.length() >= 8 ? timeKey.substring(0, 8) : timeKey;
+    }
+
+    private static List<StatisticEleSplitItemVo> toSplitItems(Map<String, Double> map) {
+        double total = map.values().stream().mapToDouble(v -> v).sum();
+        List<StatisticEleSplitItemVo> items = new ArrayList<>();
+        map.forEach((name, val) -> addSplitItem(items, name, val, total));
+        return items;
+    }
+
+    private static void addSplitItem(List<StatisticEleSplitItemVo> items, String name, double val, double total) {
+        if (val <= 0) {
+            return;
+        }
+        StatisticEleSplitItemVo item = new StatisticEleSplitItemVo();
+        item.setName(name);
+        item.setConsumption(round(val));
+        item.setRatio(total > 0 ? roundSummary(BigDecimal.valueOf(val)
+                .divide(BigDecimal.valueOf(total), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
+                .multiply(BigDecimal.valueOf(100))) : 0.0);
+        items.add(item);
+    }
+
+    private static double sumField(List<StatisticEleRecordVo> records,
+                                   Function<StatisticEleRecordVo, Double> getter) {
+        return records.stream().map(getter).mapToDouble(StatisticEleAnalyticsUtil::safe).sum();
+    }
+
+    private static int parseHour(String timeKey) {
+        if (timeKey == null || timeKey.length() < 10) {
+            return -1;
+        }
+        try {
+            return Integer.parseInt(timeKey.substring(8, 10));
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    private static LocalDate parseDate(String timeKey) {
+        if (timeKey == null || timeKey.length() < 8) {
+            return null;
+        }
+        try {
+            return LocalDate.parse(timeKey.substring(0, 8), DAY_FMT);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static double safe(Double v) {
+        return v == null ? 0.0 : v;
+    }
+
+    private static double round(Double v) {
+        if (v == null) {
+            return 0.0;
+        }
+        return roundSummary(BigDecimal.valueOf(v));
+    }
+
+    private static double roundSummary(BigDecimal value) {
+        return value.setScale(SUMMARY_SCALE, RoundingMode.HALF_UP).doubleValue();
+    }
+}

--
Gitblit v1.9.3