yuan
3 天以前 d2ab6f7153e604bac7bc4ad58f27f368b65d8a1e
feat: 添加能耗数据综合分析功能,支持按天和周维度的趋势分析
已添加4个文件
已修改4个文件
483 ■■■■■ 文件已修改
src/main/java/com/ruoyi/http/controller/StatisticEleController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/StatisticEleService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/util/StatisticEleAnalyticsUtil.java 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleAnalyticsVo.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleComparisonVo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleSplitItemVo.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/controller/StatisticEleController.java
@@ -44,6 +44,16 @@
        return AjaxResult.success(statisticEleService.getSummary(dimension, startTime, endTime));
    }
    @GetMapping("/analytics")
    @Operation(summary = "能耗数据-综合分析")
    public AjaxResult analytics(
            @RequestParam(defaultValue = "day") String dimension,
            @RequestParam String startTime,
            @RequestParam String endTime,
            @RequestParam(required = false) String trendGranularity) {
        return AjaxResult.success(statisticEleService.getAnalytics(dimension, startTime, endTime, trendGranularity));
    }
    @GetMapping("/yesterday")
    @Operation(summary = "昨日用电量汇总")
    public AjaxResult yesterday() {
src/main/java/com/ruoyi/http/service/StatisticEleService.java
@@ -1,5 +1,6 @@
package com.ruoyi.http.service;
import com.ruoyi.http.vo.StatisticEleAnalyticsVo;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import com.ruoyi.http.vo.StatisticEleSummaryVo;
import com.ruoyi.http.vo.StatisticEleSyncStatusVo;
@@ -18,6 +19,8 @@
    StatisticEleSummaryVo getSummary(String dimension, String startTime, String endTime);
    StatisticEleAnalyticsVo getAnalytics(String dimension, String startTime, String endTime, String trendGranularity);
    StatisticEleSummaryVo getYesterdaySummary();
    void exportRecords(String dimension, String startTime, String endTime, HttpServletResponse response);
src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java
@@ -15,6 +15,9 @@
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;
@@ -31,16 +34,17 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class StatisticEleServiceImpl implements StatisticEleService {
    private static final Set<String> STAT_DIMENSIONS = Set.of("day", "month", "quarter", "year");
    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;
@@ -76,22 +80,91 @@
            throw new ServiceException("开始时间和结束时间不能为空");
        }
        if ("hour".equals(dimension)) {
            List<StatisticEleRecordVo> detailRecords = queryHourRecords(startTime, endTime);
            List<StatisticEleRecordVo> hourRecords = queryHourRecords(startTime, endTime);
            List<StatisticEleRecordVo> chartRecords = StatisticEleAggregateUtil.aggregateHourToBuckets(
                    detailRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
            return buildSummary(detailRecords, chartRecords);
                    hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
            return buildSummary(hourRecords, chartRecords, hourRecords);
        }
        if (!STAT_DIMENSIONS.contains(dimension)) {
            throw new ServiceException("统计维度无效,支持 hour/day/month/quarter/year");
            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);
        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;
    }
    /**
@@ -108,13 +181,15 @@
                ? StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR)
                : StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, StatisticEleAggregateUtil.HOUR_TO_DAY);
        return buildSummary(detailRecords, chartRecords);
        return buildSummary(detailRecords, chartRecords, hourRecords);
    }
    private StatisticEleSummaryVo buildSummary(
            List<StatisticEleRecordVo> detailRecords, List<StatisticEleRecordVo> chartRecords) {
            List<StatisticEleRecordVo> detailRecords,
            List<StatisticEleRecordVo> chartRecords,
            List<StatisticEleRecordVo> hourRecords) {
        StatisticEleAggregateUtil.StatisticEleSummaryMetrics metrics =
                StatisticEleAggregateUtil.calcMetrics(chartRecords);
                StatisticEleAnalyticsUtil.calcCombinedMetrics(hourRecords, chartRecords);
        StatisticEleSummaryVo summary = new StatisticEleSummaryVo();
        summary.setRecords(detailRecords);
        summary.setChartRecords(chartRecords);
src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java
@@ -34,6 +34,7 @@
        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;
    /**
     * æŒ‰æ—¶é—´æ¡¶æ±‡æ€»ï¼ˆå¤šç”µè¡¨åˆå¹¶ï¼Œç”¨äºŽå›¾è¡¨ï¼‰
@@ -178,6 +179,7 @@
        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(
@@ -234,6 +236,7 @@
        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;
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();
    }
}
src/main/java/com/ruoyi/http/vo/StatisticEleAnalyticsVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.http.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
 * èƒ½è€—综合分析结果
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class StatisticEleAnalyticsVo extends StatisticEleSummaryVo {
    /** è´Ÿè·çއ = å°æ—¶å¹³å‡ Ã· å°æ—¶æœ€å¤§ */
    private Double loadRate;
    /** å³°å¹³è°·å°–拆分 */
    private List<StatisticEleSplitItemVo> periodSplits;
    /** ç™½ç­/夜班拆分 */
    private List<StatisticEleSplitItemVo> shiftSplits;
    /** å·¥ä½œæ—¥/休息日拆分 */
    private List<StatisticEleSplitItemVo> dayTypeSplits;
    /** çŽ¯æ¯” */
    private StatisticEleComparisonVo chainComparison;
    /** åŒæ¯” */
    private StatisticEleComparisonVo yoyComparison;
    /** è¶‹åŠ¿å›¾ç²’åº¦ */
    private String trendGranularity;
    /** è¶‹åŠ¿å›¾æ•°æ®ï¼ˆæŒ‰ trendGranularity èšåˆï¼‰ */
    private List<StatisticEleRecordVo> trendRecords;
}
src/main/java/com/ruoyi/http/vo/StatisticEleComparisonVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.http.vo;
import lombok.Data;
/**
 * åŒæ¯”/环比对比
 */
@Data
public class StatisticEleComparisonVo {
    /** å¯¹æ¯”类型:chain-环比, yoy-同比 */
    private String type;
    /** å¯¹æ¯”期标签,如「上期」「去年同期」 */
    private String label;
    /** å¯¹æ¯”期总用电量(kWh) */
    private Double compareTotal;
    /** æœ¬æœŸæ€»ç”¨ç”µé‡(kWh) */
    private Double currentTotal;
    /** å·®å€¼(kWh),本期-对比期 */
    private Double delta;
    /** å˜åŒ–率(%) */
    private Double changeRate;
}
src/main/java/com/ruoyi/http/vo/StatisticEleSplitItemVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.http.vo;
import lombok.Data;
/**
 * ç”¨ç”µé‡æ‹†åˆ†é¡¹ï¼ˆæ—¶æ®µ/班次/日类型等)
 */
@Data
public class StatisticEleSplitItemVo {
    private String name;
    /** ç”¨ç”µé‡(kWh) */
    private Double consumption;
    /** å æ¯”(%) */
    private Double ratio;
}