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/service/impl/StatisticEleServiceImpl.java |  269 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 269 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java b/src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java
new file mode 100644
index 0000000..1d624d2
--- /dev/null
+++ b/src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java
@@ -0,0 +1,269 @@
+package com.ruoyi.http.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.http.HttpUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.http.config.TqdianbiaoConfig;
+import com.ruoyi.http.mapper.TqdianbiaoCollectorMapper;
+import com.ruoyi.http.mapper.TqdianbiaoEleRecordMapper;
+import com.ruoyi.http.mapper.TqdianbiaoMeterMapper;
+import com.ruoyi.http.mapper.TqdianbiaoSyncLogMapper;
+import com.ruoyi.http.pojo.TqdianbiaoCollector;
+import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
+import com.ruoyi.http.pojo.TqdianbiaoSyncLog;
+import com.ruoyi.http.service.StatisticEleService;
+import com.ruoyi.http.util.StatisticEleAggregateUtil;
+import com.ruoyi.http.util.StatisticEleAggregateUtil.HourRange;
+import com.ruoyi.http.util.StatisticEleAnalyticsUtil;
+import com.ruoyi.http.util.StatisticEleAnalyticsUtil.DateBounds;
+import com.ruoyi.http.vo.StatisticEleAnalyticsVo;
+import com.ruoyi.http.vo.StatisticEleRecordVo;
+import com.ruoyi.http.vo.StatisticEleSummaryVo;
+import com.ruoyi.http.vo.StatisticEleSyncStatusVo;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class StatisticEleServiceImpl implements StatisticEleService {
+
+    private static final Set<String> STAT_DIMENSIONS = Set.of("day", "week", "month", "quarter", "year");
+    private static final List<String> DATA_DIMENSIONS = List.of("hour", "manual");
+    private static final DateTimeFormatter LOG_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+    private final TqdianbiaoConfig config;
+    private final TqdianbiaoEleRecordMapper eleRecordMapper;
+    private final TqdianbiaoMeterMapper meterMapper;
+    private final TqdianbiaoCollectorMapper collectorMapper;
+    private final TqdianbiaoSyncLogMapper syncLogMapper;
+
+    @Override
+    public String fetchRawData(String dimension, String startTime, String endTime) {
+        if (!"hour".equals(dimension)) {
+            throw new ServiceException("浠呮敮鎸佹媺鍙栧皬鏃跺師濮嬫暟鎹�");
+        }
+        String url = config.getBaseUrl() + "/Api/StatisticEle/hour";
+        String param = String.format(
+                "auth=%s&start_time=%s&end_time=%s&ignore_radio=%d",
+                config.getAuth(), startTime, endTime, config.getIgnoreRadio()
+        );
+        log.warn("璋冪敤杩滅▼鐢佃〃鎺ュ彛(璋冭瘯): {}?{}", url, param);
+        return HttpUtils.sendGet(url, param);
+    }
+
+    @Override
+    public List<StatisticEleRecordVo> listRecords(String dimension, String startTime, String endTime, Integer ignoreRadio) {
+        if ("hour".equals(dimension) || "collection".equals(dimension)) {
+            return queryHourRecords(startTime, endTime);
+        }
+        return aggregateFromHour(dimension, startTime, endTime, true);
+    }
+
+    @Override
+    public StatisticEleSummaryVo getSummary(String dimension, String startTime, String endTime) {
+        if (!StringUtils.hasText(startTime) || !StringUtils.hasText(endTime)) {
+            throw new ServiceException("寮�濮嬫椂闂村拰缁撴潫鏃堕棿涓嶈兘涓虹┖");
+        }
+        if ("hour".equals(dimension)) {
+            List<StatisticEleRecordVo> hourRecords = queryHourRecords(startTime, endTime);
+            List<StatisticEleRecordVo> chartRecords = StatisticEleAggregateUtil.aggregateHourToBuckets(
+                    hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
+            return buildSummary(hourRecords, chartRecords, hourRecords);
+        }
+        if (!STAT_DIMENSIONS.contains(dimension)) {
+            throw new ServiceException("缁熻缁村害鏃犳晥锛屾敮鎸� hour/day/week/month/quarter/year");
+        }
+
+        if ("day".equals(dimension)) {
+            return getDayDimensionSummary(startTime, endTime);
+        }
+
+        HourRange range = StatisticEleAggregateUtil.toHourQueryRange(dimension, startTime, endTime);
+        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
+        List<StatisticEleRecordVo> detailRecords = aggregateFromHour(dimension, startTime, endTime, true);
+        List<StatisticEleRecordVo> chartRecords = aggregateFromHour(dimension, startTime, endTime, false);
+        return buildSummary(detailRecords, chartRecords, hourRecords);
+    }
+
+    @Override
+    public StatisticEleAnalyticsVo getAnalytics(String dimension, String startTime, String endTime, String trendGranularity) {
+        if (!StringUtils.hasText(startTime) || !StringUtils.hasText(endTime)) {
+            throw new ServiceException("寮�濮嬫椂闂村拰缁撴潫鏃堕棿涓嶈兘涓虹┖");
+        }
+        StatisticEleSummaryVo summary = getSummary(dimension, startTime, endTime);
+        HourRange range = StatisticEleAggregateUtil.toHourQueryRange(
+                normalizeAnalyticsDimension(dimension), startTime, endTime);
+        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
+        List<StatisticEleRecordVo> hourlyMerged = StatisticEleAggregateUtil.aggregateHourToBuckets(
+                hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
+
+        boolean singleDay = "day".equals(dimension) && startTime.equals(endTime);
+        String trend = StringUtils.hasText(trendGranularity)
+                ? trendGranularity
+                : StatisticEleAnalyticsUtil.defaultTrendGranularity(dimension, singleDay);
+
+        StatisticEleAnalyticsVo analytics = copyToAnalytics(summary);
+        analytics.setLoadRate(StatisticEleAnalyticsUtil.calcLoadRate(
+                summary.getAvgConsumption(), summary.getMaxConsumption()));
+        analytics.setPeriodSplits(StatisticEleAnalyticsUtil.calcPeriodSplits(hourlyMerged));
+        analytics.setShiftSplits(StatisticEleAnalyticsUtil.calcShiftSplits(hourlyMerged));
+        analytics.setDayTypeSplits(StatisticEleAnalyticsUtil.calcDayTypeSplits(hourlyMerged));
+        analytics.setTrendGranularity(trend);
+        analytics.setTrendRecords(StatisticEleAggregateUtil.aggregateHourToBuckets(
+                hourRecords, StatisticEleAnalyticsUtil.trendBucketFn(trend)));
+
+        DateBounds bounds = StatisticEleAnalyticsUtil.resolveBounds(dimension, startTime, endTime);
+        Double chainTotal = queryTotalByDayBounds(StatisticEleAnalyticsUtil.shiftChain(bounds));
+        Double yoyTotal = queryTotalByDayBounds(StatisticEleAnalyticsUtil.shiftYoy(bounds));
+        analytics.setChainComparison(StatisticEleAnalyticsUtil.buildComparison(
+                "chain", "鐜瘮涓婃湡", summary.getTotalConsumption(), chainTotal));
+        analytics.setYoyComparison(StatisticEleAnalyticsUtil.buildComparison(
+                "yoy", "鍚屾瘮鍘诲勾鍚屾湡", summary.getTotalConsumption(), yoyTotal));
+        return analytics;
+    }
+
+    private String normalizeAnalyticsDimension(String dimension) {
+        return STAT_DIMENSIONS.contains(dimension) ? dimension : "day";
+    }
+
+    private Double queryTotalByDayBounds(DateBounds bounds) {
+        String start = bounds.start().format(DAY_FMT);
+        String end = bounds.end().format(DAY_FMT);
+        HourRange range = StatisticEleAggregateUtil.toHourQueryRange("day", start, end);
+        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
+        if (hourRecords.isEmpty()) {
+            return 0.0;
+        }
+        return StatisticEleAnalyticsUtil.calcCombinedMetrics(
+                hourRecords,
+                StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, StatisticEleAggregateUtil.HOUR_TO_DAY)
+        ).getTotalConsumption();
+    }
+
+    private StatisticEleAnalyticsVo copyToAnalytics(StatisticEleSummaryVo summary) {
+        StatisticEleAnalyticsVo analytics = new StatisticEleAnalyticsVo();
+        analytics.setTotalConsumption(summary.getTotalConsumption());
+        analytics.setAvgConsumption(summary.getAvgConsumption());
+        analytics.setMaxConsumption(summary.getMaxConsumption());
+        analytics.setMinConsumption(summary.getMinConsumption());
+        analytics.setRecordCount(summary.getRecordCount());
+        analytics.setChartRecords(summary.getChartRecords());
+        analytics.setRecords(summary.getRecords());
+        return analytics;
+    }
+
+    /**
+     * 澶╃淮搴︽眹鎬伙細鍗曟棩鐢ㄥ皬鏃剁骇鏁版嵁璁$畻鍧囧��/鏋佸�煎苟灞曠ず 24 灏忔椂瓒嬪娍锛涘鏃ユ寜鏃ユ《瀵规瘮銆�
+     */
+    private StatisticEleSummaryVo getDayDimensionSummary(String startTime, String endTime) {
+        HourRange range = StatisticEleAggregateUtil.toHourQueryRange("day", startTime, endTime);
+        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
+        List<StatisticEleRecordVo> detailRecords = StatisticEleAggregateUtil.aggregateHourPerMeter(
+                hourRecords, StatisticEleAggregateUtil.HOUR_TO_DAY);
+
+        boolean singleDay = startTime.equals(endTime);
+        List<StatisticEleRecordVo> chartRecords = singleDay
+                ? StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR)
+                : StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, StatisticEleAggregateUtil.HOUR_TO_DAY);
+
+        return buildSummary(detailRecords, chartRecords, hourRecords);
+    }
+
+    private StatisticEleSummaryVo buildSummary(
+            List<StatisticEleRecordVo> detailRecords,
+            List<StatisticEleRecordVo> chartRecords,
+            List<StatisticEleRecordVo> hourRecords) {
+        StatisticEleAggregateUtil.StatisticEleSummaryMetrics metrics =
+                StatisticEleAnalyticsUtil.calcCombinedMetrics(hourRecords, chartRecords);
+        StatisticEleSummaryVo summary = new StatisticEleSummaryVo();
+        summary.setRecords(detailRecords);
+        summary.setChartRecords(chartRecords);
+        summary.setRecordCount(metrics.getRecordCount());
+        summary.setTotalConsumption(metrics.getTotalConsumption());
+        summary.setAvgConsumption(metrics.getAvgConsumption());
+        summary.setMaxConsumption(metrics.getMaxConsumption());
+        summary.setMinConsumption(metrics.getMinConsumption());
+        return summary;
+    }
+
+    @Override
+    public StatisticEleSummaryVo getYesterdaySummary() {
+        String day = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        return getDayDimensionSummary(day, day);
+    }
+
+    @Override
+    public void exportRecords(String dimension, String startTime, String endTime, HttpServletResponse response) {
+        List<StatisticEleRecordVo> records;
+        if ("hour".equals(dimension)) {
+            records = queryHourRecords(startTime, endTime);
+        } else {
+            records = aggregateFromHour(dimension, startTime, endTime, true);
+        }
+        ExcelUtil<StatisticEleRecordVo> util = new ExcelUtil<>(StatisticEleRecordVo.class);
+        util.exportExcel(response, records, "鑳借�楃粺璁℃暟鎹�");
+    }
+
+    @Override
+    public StatisticEleSyncStatusVo getSyncStatus() {
+        StatisticEleSyncStatusVo status = new StatisticEleSyncStatusVo();
+        status.setMeterCount(Math.toIntExact(meterMapper.selectCount(null)));
+        status.setCollectorCount(Math.toIntExact(collectorMapper.selectCount(null)));
+        status.setOnlineCollectorCount(Math.toIntExact(collectorMapper.selectCount(
+                Wrappers.<TqdianbiaoCollector>lambdaQuery().eq(TqdianbiaoCollector::getOnline, true))));
+
+        Map<String, Long> recordCountByDimension = new HashMap<>();
+        recordCountByDimension.put("hour", eleRecordMapper.selectCount(
+                Wrappers.<TqdianbiaoEleRecord>lambdaQuery().eq(TqdianbiaoEleRecord::getDimension, "hour")));
+        status.setRecordCountByDimension(recordCountByDimension);
+
+        Map<String, String> lastSyncTimeByType = new HashMap<>();
+        for (String syncType : List.of("collector", "meter", "hour")) {
+            TqdianbiaoSyncLog latest = syncLogMapper.selectOne(
+                    Wrappers.<TqdianbiaoSyncLog>lambdaQuery()
+                            .eq(TqdianbiaoSyncLog::getSyncType, syncType)
+                            .eq(TqdianbiaoSyncLog::getStatus, "success")
+                            .orderByDesc(TqdianbiaoSyncLog::getCreateTime)
+                            .last("LIMIT 1"));
+            if (latest != null && latest.getCreateTime() != null) {
+                lastSyncTimeByType.put(syncType, latest.getCreateTime().format(LOG_TIME_FMT));
+            }
+        }
+        status.setLastSyncTimeByType(lastSyncTimeByType);
+        return status;
+    }
+
+    private List<StatisticEleRecordVo> queryHourRecords(String startTime, String endTime) {
+        String normalizedStart = StatisticEleAggregateUtil.normalizeQueryStartTimeKey(startTime);
+        String normalizedEnd = StatisticEleAggregateUtil.normalizeQueryEndTimeKey(endTime);
+        List<StatisticEleRecordVo> records = eleRecordMapper.selectRecordList(DATA_DIMENSIONS, normalizedStart, normalizedEnd);
+        StatisticEleAggregateUtil.normalizeConsumptions(records);
+        return records;
+    }
+
+    private List<StatisticEleRecordVo> aggregateFromHour(
+            String dimension, String startTime, String endTime, boolean perMeter) {
+        HourRange range = StatisticEleAggregateUtil.toHourQueryRange(dimension, startTime, endTime);
+        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
+        Function<String, String> bucketFn = StatisticEleAggregateUtil.bucketFn(dimension);
+        if (perMeter) {
+            return StatisticEleAggregateUtil.aggregateHourPerMeter(hourRecords, bucketFn);
+        }
+        return StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, bucketFn);
+    }
+}

--
Gitblit v1.9.3