package com.ruoyi.energymeter.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.energymeter.constant.EnergyMeterConstants; import com.ruoyi.energymeter.dto.DailyUsageRow; import com.ruoyi.energymeter.dto.DataRangeRow; import com.ruoyi.energymeter.dto.MeterUsageRow; import com.ruoyi.energymeter.dto.MonthlyUsageRow; import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper; import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper; import com.ruoyi.energymeter.mapper.EnergyMeterSyncRunMapper; import com.ruoyi.energymeter.pojo.EnergyMeterDevice; import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement; import com.ruoyi.energymeter.pojo.EnergyMeterSyncRun; import com.ruoyi.energymeter.provider.MeterProviderClient; import com.ruoyi.energymeter.service.ElectricMeterDashboardService; import com.ruoyi.energymeter.service.EnergyMonitorService; import com.ruoyi.energymeter.util.EnergyMeterSupport; import com.ruoyi.energymeter.vo.ElectricMeterDashboardVO; import com.ruoyi.energymeter.vo.EnergyCollectionVO; import com.ruoyi.energymeter.vo.EnergyRealtimeVO; import com.ruoyi.energymeter.vo.EnergyReportVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; /** * 能耗数据采集、实时监控与统计分析实现。 * *

统计口径与用能概览保持一致:只汇总 D 粒度的用量指标,表码(POINT)不参与累加; * 缺失日期保持 null,由前端显示为缺口而不是 0。

*/ @Slf4j @Service @RequiredArgsConstructor public class EnergyMonitorServiceImpl implements EnergyMonitorService { private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final List ALL_TYPES = List.of(EnergyMeterConstants.TYPE_WATER, EnergyMeterConstants.TYPE_ELECTRIC, EnergyMeterConstants.TYPE_GAS); private static final int TREND_DAYS = 7; private static final int MAX_REPORT_DAYS = 366; private final EnergyMeterDeviceMapper deviceMapper; private final EnergyMeterMeasurementMapper measurementMapper; private final EnergyMeterSyncRunMapper runMapper; private final ElectricMeterDashboardService dashboardService; private final MeterProviderClient provider; /* ------------------------------------------------------------------ */ /* 数据采集 */ /* ------------------------------------------------------------------ */ @Override public EnergyCollectionVO collection() { LocalDate today = EnergyMeterSupport.today(); EnergyCollectionVO vo = new EnergyCollectionVO(); vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT)); vo.setToday(today.format(DAY_FORMAT)); vo.setMockMode(provider.mockMode()); List runs = runMapper.selectList(new LambdaQueryWrapper() .orderByDesc(EnergyMeterSyncRun::getId) .last("LIMIT 8")); int failed = 0; int pending = 0; for (EnergyMeterSyncRun run : runs) { EnergyCollectionVO.RunItem item = toRunItem(run); vo.getRuns().add(item); failed += nvl(item.getFailedTasks()); pending += Math.max(0, nvl(item.getTotalTasks()) - nvl(item.getSuccessTasks()) - nvl(item.getEmptyTasks()) - nvl(item.getFailedTasks())); } vo.setFailedTaskCount(failed); vo.setPendingTaskCount(pending); for (String type : ALL_TYPES) { List devices = loadDevices(type); EnergyCollectionVO.Medium medium = new EnergyCollectionVO.Medium(); medium.setMeterType(type); medium.setMeterName(mediumName(type)); medium.setUnit(EnergyMeterConstants.unit(type)); medium.setDeviceCount(devices.size()); int providerCount = 0; int manualCount = 0; for (EnergyMeterDevice device : devices) { if (EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated())) { providerCount++; } else { manualCount++; } } medium.setProviderCount(providerCount); medium.setManualCount(manualCount); List ids = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList()); DataRangeRow range = ids.isEmpty() ? null : measurementMapper.selectDataRange(ids, EnergyMeterConstants.usageMetric(type)); LocalDate dataStart = range == null || range.getMinTime() == null ? null : range.getMinTime().toLocalDate(); LocalDate dataEnd = range == null || range.getMaxTime() == null ? null : range.getMaxTime().toLocalDate(); medium.setDataStart(dataStart == null ? null : dataStart.format(DAY_FORMAT)); medium.setDataEnd(dataEnd == null ? null : dataEnd.format(DAY_FORMAT)); LocalDateTime lastSuccess = devices.stream() .map(EnergyMeterDevice::getLastSuccessAt) .filter(Objects::nonNull) .max(LocalDateTime::compareTo) .orElse(null); medium.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DATETIME_FORMAT)); if (!ids.isEmpty() && dataEnd != null) { Integer latestDayMeters = measurementMapper.countMetersWithUsage(ids, EnergyMeterConstants.usageMetric(type), dataEnd.atStartOfDay(), dataEnd.plusDays(1).atStartOfDay()); medium.setLatestDayMeters(latestDayMeters == null ? 0 : latestDayMeters); } Map daily = ids.isEmpty() ? Map.of() : toDayMap(measurementMapper.sumDailyUsage(ids, EnergyMeterConstants.usageMetric(type), today.minusDays(TREND_DAYS - 1).atStartOfDay(), today.plusDays(1).atStartOfDay())); medium.setCoveredDays(daily.size()); medium.setExpectedDays(TREND_DAYS); medium.setCoverageRate(daily.isEmpty() ? BigDecimal.ZERO : BigDecimal.valueOf(daily.size() * 100.0 / TREND_DAYS).setScale(1, RoundingMode.HALF_UP)); // 在线口径:供应商标记在线,或最近 36 小时成功采集;模拟表再看最近数据日是否已生成 boolean simulatedFresh = dataEnd != null && !dataEnd.isBefore(today.minusDays(1)); int onlineCount = 0; for (EnergyMeterDevice device : devices) { if (isOnline(device) || (EnergyMeterConstants.isSimulated(device.getSimulated()) && simulatedFresh)) { onlineCount++; } } medium.setOnlineCount(onlineCount); boolean manualOnly = !devices.isEmpty() && providerCount == 0; boolean simulatedOnly = !devices.isEmpty() && devices.stream().allMatch(d -> EnergyMeterConstants.isSimulated(d.getSimulated())); applyCollectionStatus(medium, devices.isEmpty(), dataEnd, lastSuccess, manualOnly, simulatedOnly, today); vo.getMedia().add(medium); } return vo; } private EnergyCollectionVO.RunItem toRunItem(EnergyMeterSyncRun run) { EnergyCollectionVO.RunItem item = new EnergyCollectionVO.RunItem(); item.setId(run.getId()); item.setRunKey(run.getRunKey()); item.setTriggerType(run.getTriggerType()); item.setTriggerTypeName(switch (StringUtils.defaultString(run.getTriggerType())) { case EnergyMeterConstants.RUN_SCHEDULED -> "定时同步"; case EnergyMeterConstants.RUN_BACKFILL -> "历史补采"; default -> "手动采集"; }); item.setStatus(run.getStatus()); item.setStatusText(switch (StringUtils.defaultString(run.getStatus())) { case EnergyMeterConstants.RUN_SUCCESS -> "全部成功"; case EnergyMeterConstants.RUN_PARTIAL -> "部分失败"; case EnergyMeterConstants.RUN_FAILED -> "采集失败"; case EnergyMeterConstants.RUN_RUNNING -> "采集中"; default -> "待执行"; }); item.setRangeStart(run.getRangeStart() == null ? null : run.getRangeStart().format(DAY_FORMAT)); item.setRangeEnd(run.getRangeEnd() == null ? null : run.getRangeEnd().format(DAY_FORMAT)); item.setTotalTasks(nvl(run.getTotalTasks())); item.setSuccessTasks(nvl(run.getSuccessTasks())); item.setEmptyTasks(nvl(run.getEmptyTasks())); item.setFailedTasks(nvl(run.getFailedTasks())); item.setStartedAt(run.getStartedAt() == null ? null : run.getStartedAt().format(DATETIME_FORMAT)); item.setFinishedAt(run.getFinishedAt() == null ? null : run.getFinishedAt().format(DATETIME_FORMAT)); return item; } private void applyCollectionStatus(EnergyCollectionVO.Medium medium, boolean empty, LocalDate dataEnd, LocalDateTime lastSuccess, boolean manualOnly, boolean simulatedOnly, LocalDate today) { if (empty) { medium.setStatus("NO_DEVICE"); medium.setStatusText("未接入设备"); return; } if (dataEnd == null) { medium.setStatus("EMPTY"); medium.setStatusText("暂无采集数据"); return; } if (simulatedOnly) { if (dataEnd.isBefore(today.minusDays(1))) { medium.setStatus("PARTIAL"); medium.setStatusText("自动采集数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " 天"); } else { medium.setStatus("OK"); medium.setStatusText("自动采集数据"); } return; } if (manualOnly) { if (dataEnd.isBefore(today.minusDays(1))) { medium.setStatus("PARTIAL"); medium.setStatusText("数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " 天"); } else { medium.setStatus("OK"); medium.setStatusText("手工抄表数据"); } return; } if (lastSuccess == null || lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) { medium.setStatus("STALE"); medium.setStatusText("超过 36 小时无成功采集"); return; } if (dataEnd.isBefore(today.minusDays(1))) { medium.setStatus("PARTIAL"); medium.setStatusText("数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " 天"); return; } medium.setStatus("OK"); medium.setStatusText("采集正常"); } /** * 在线口径:供应商标记在线,或最近 36 小时内有成功采集。 * *

第三方档案的在线标记只在绑定/验证时落库,长期不刷新,不能单独作为“在线”依据。

*/ private boolean isOnline(EnergyMeterDevice device) { if (Integer.valueOf(1).equals(device.getOnlineStatus())) { return true; } return device.getLastSuccessAt() != null && device.getLastSuccessAt().isAfter(EnergyMeterSupport.now().minusHours(36)); } /* ------------------------------------------------------------------ */ /* 实时监控 */ /* ------------------------------------------------------------------ */ @Override public EnergyRealtimeVO realtime(Long meterId) { LocalDate today = EnergyMeterSupport.today(); LocalDate windowStart = today.minusDays(TREND_DAYS); LocalDate monthStart = today.withDayOfMonth(1); EnergyRealtimeVO vo = new EnergyRealtimeVO(); vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT)); vo.setToday(today.format(DAY_FORMAT)); vo.setMockMode(provider.mockMode()); vo.setMeterId(meterId); List devices; List types; if (meterId != null) { EnergyMeterDevice device = requireDevice(meterId); devices = List.of(device); String type = EnergyMeterConstants.canonicalType(device.getMeterType()); if (type == null) { throw new ServiceException("设备介质类型不合法"); } types = List.of(type); vo.setScopeLabel(device.getDisplayName() + "(" + device.getMeterAddress() + ")"); } else { devices = loadDevices(null); types = ALL_TYPES; vo.setScopeLabel("全部已接入表具(" + devices.size() + " 台)"); } Map> dailyByType = new HashMap<>(); for (String type : types) { List typeDevices = devices.stream() .filter(d -> type.equals(d.getMeterType())) .collect(Collectors.toList()); List ids = typeDevices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList()); Map windowDaily = ids.isEmpty() ? Map.of() : toDayMap(measurementMapper.sumDailyUsage(ids, EnergyMeterConstants.usageMetric(type), windowStart.atStartOfDay(), today.plusDays(1).atStartOfDay())); dailyByType.put(type, windowDaily); EnergyRealtimeVO.Medium medium = new EnergyRealtimeVO.Medium(); medium.setMeterType(type); medium.setMeterName(mediumName(type)); medium.setUnit(EnergyMeterConstants.unit(type)); medium.setDeviceCount(typeDevices.size()); LocalDate latestDate = latestDate(windowDaily, today, windowStart); BigDecimal latest = latestDate == null ? null : windowDaily.get(latestDate); LocalDate previousDate = latestDate == null ? null : latestDate(windowDaily, latestDate.minusDays(1), windowStart); BigDecimal previous = previousDate == null ? null : windowDaily.get(previousDate); boolean simulatedFresh = latestDate != null && !latestDate.isBefore(today.minusDays(1)); medium.setOnlineCount((int) typeDevices.stream() .filter(d -> isOnline(d) || (EnergyMeterConstants.isSimulated(d.getSimulated()) && simulatedFresh)) .count()); medium.setLatestUsage(latest); medium.setLatestUsageDate(latestDate == null ? null : latestDate.format(DAY_FORMAT)); medium.setPreviousUsage(previous); medium.setPreviousUsageDate(previousDate == null ? null : previousDate.format(DAY_FORMAT)); medium.setCompareRate(compareRate(latest, previous)); BigDecimal monthTotal = null; if (!ids.isEmpty()) { Map monthDaily = toDayMap(measurementMapper.sumDailyUsage(ids, EnergyMeterConstants.usageMetric(type), monthStart.atStartOfDay(), today.plusDays(1).atStartOfDay())); monthTotal = sumValues(monthDaily.values()); if (monthTotal != null) { monthTotal = monthTotal.setScale(3, RoundingMode.HALF_UP); } } medium.setMonthTotal(monthTotal); List trendValues = new ArrayList<>(); for (int i = TREND_DAYS - 1; i >= 0; i--) { BigDecimal value = windowDaily.get(today.minusDays(i)); if (value != null) { trendValues.add(value); } } medium.setAvgDaily(average(trendValues)); medium.setPeakDaily(trendValues.stream().max(BigDecimal::compareTo).orElse(null)); EnergyMeterMeasurement reading = null; for (EnergyMeterDevice device : typeDevices) { EnergyMeterMeasurement point = measurementMapper.selectLatestPoint(device.getId(), EnergyMeterConstants.readingMetric(type)); if (point != null && (reading == null || point.getMeasureTime().isAfter(reading.getMeasureTime()))) { reading = point; } } if (reading != null) { medium.setLatestReading(reading.getMetricValue()); medium.setLatestReadingTime(reading.getMeasureTime().format(DATETIME_FORMAT)); } LocalDateTime lastSuccess = typeDevices.stream() .map(EnergyMeterDevice::getLastSuccessAt) .filter(Objects::nonNull) .max(LocalDateTime::compareTo) .orElse(null); medium.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DATETIME_FORMAT)); boolean manualOnly = !typeDevices.isEmpty() && typeDevices.stream().noneMatch(d -> EnergyMeterConstants.isAutoCollect(d.getSourceType(), d.getSimulated())); boolean simulatedOnly = !typeDevices.isEmpty() && typeDevices.stream().allMatch(d -> EnergyMeterConstants.isSimulated(d.getSimulated())); applyRealtimeStatus(medium, typeDevices.isEmpty(), latest, latestDate, lastSuccess, manualOnly, simulatedOnly, today); vo.getMedia().add(medium); } /* 近 7 日趋势 */ for (int i = TREND_DAYS - 1; i >= 0; i--) { LocalDate day = today.minusDays(i); EnergyRealtimeVO.TrendPoint point = new EnergyRealtimeVO.TrendPoint(); point.setDay(day.format(DAY_FORMAT)); point.setWater(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_WATER, Map.of()).get(day)); point.setElectric(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_ELECTRIC, Map.of()).get(day)); point.setGas(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_GAS, Map.of()).get(day)); vo.getTrend().add(point); } /* 电表实时曲线 */ if (types.contains(EnergyMeterConstants.TYPE_ELECTRIC)) { EnergyRealtimeVO.Electric electric = buildElectric(meterId); vo.setElectric(electric); if (electric != null) { for (EnergyRealtimeVO.Medium medium : vo.getMedia()) { if (EnergyMeterConstants.TYPE_ELECTRIC.equals(medium.getMeterType())) { medium.setCurrentPower(electric.getCurrentPower()); medium.setPowerFactor(electric.getPowerFactor()); } } } } buildAlerts(vo, devices, today); return vo; } private EnergyRealtimeVO.Electric buildElectric(Long meterId) { ElectricMeterDashboardVO dashboard; try { dashboard = dashboardService.dashboard(meterId, 0); } catch (Exception e) { log.warn("实时监控查询电表 15 分钟数据失败:{}", e.getMessage()); return null; } EnergyRealtimeVO.Electric electric = new EnergyRealtimeVO.Electric(); electric.setRangeStart(dashboard.getToday() == null ? null : dashboard.getToday().getRangeStart()); electric.setRangeEnd(dashboard.getToday() == null ? null : dashboard.getToday().getRangeEnd()); electric.setTodayTotal(dashboard.getToday() == null ? null : dashboard.getToday().getTotal()); electric.setYesterdaySamePeriodTotal(dashboard.getToday() == null ? null : dashboard.getToday().getYesterdaySamePeriodTotal()); List points = dashboard.getToday() == null ? List.of() : dashboard.getToday().getSeries(); BigDecimal lastPower = null; BigDecimal lastFactor = null; for (ElectricMeterDashboardVO.QuarterPoint point : points) { EnergyRealtimeVO.CurvePoint curvePoint = new EnergyRealtimeVO.CurvePoint(); curvePoint.setTime(point.getTime()); curvePoint.setActive(point.getActive()); if (point.getActive() == null) { electric.getSeries().add(curvePoint); continue; } // 15 分钟电量折算功率:kWh / 0.25h = kW BigDecimal power = point.getActive().multiply(BigDecimal.valueOf(4)).setScale(3, RoundingMode.HALF_UP); curvePoint.setPower(power); electric.getSeries().add(curvePoint); lastPower = power; lastFactor = powerFactor(power, point.getReactive()); } electric.setCurrentPower(lastPower); electric.setPowerFactor(lastFactor); return electric; } private BigDecimal powerFactor(BigDecimal activePower, BigDecimal reactiveEnergy) { if (activePower == null) { return null; } BigDecimal reactivePower = reactiveEnergy == null ? BigDecimal.ZERO : reactiveEnergy.multiply(BigDecimal.valueOf(4)); double p = activePower.doubleValue(); double q = reactivePower.doubleValue(); if (p <= 0 && q <= 0) { return null; } double factor = p / Math.sqrt(p * p + q * q); return BigDecimal.valueOf(Math.min(factor, 1.0)).setScale(3, RoundingMode.HALF_UP); } private void applyRealtimeStatus(EnergyRealtimeVO.Medium medium, boolean noDevice, BigDecimal latest, LocalDate latestDate, LocalDateTime lastSuccess, boolean manualOnly, boolean simulatedOnly, LocalDate today) { if (noDevice) { medium.setStatus("NO_DEVICE"); medium.setStatusText("未接入设备"); return; } if (latest == null && medium.getLatestReading() == null) { medium.setStatus("EMPTY"); medium.setStatusText("暂无数据"); return; } if (simulatedOnly) { if (latestDate != null && latestDate.isBefore(today.minusDays(1))) { medium.setStatus("STALE"); medium.setStatusText("自动采集数据滞后"); } else { medium.setStatus("OK"); medium.setStatusText("自动采集数据"); } return; } if (manualOnly) { if (latestDate != null && latestDate.isBefore(today.minusDays(1))) { medium.setStatus("STALE"); medium.setStatusText("抄表数据滞后"); } else { medium.setStatus("OK"); medium.setStatusText("手工抄表数据"); } return; } if (lastSuccess == null || lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) { medium.setStatus("STALE"); medium.setStatusText("采集滞后"); return; } medium.setStatus("OK"); medium.setStatusText("运行正常"); } private void buildAlerts(EnergyRealtimeVO vo, List devices, LocalDate today) { List alerts = vo.getAlerts(); for (EnergyRealtimeVO.Medium medium : vo.getMedia()) { if ("STALE".equals(medium.getStatus()) || "EMPTY".equals(medium.getStatus())) { alerts.add(alert("warning", medium.getMeterName() + ":" + medium.getStatusText() + ",请检查设备或手动采集")); } } int deviceAlerts = 0; for (EnergyMeterDevice device : devices) { if (StringUtils.isNotBlank(device.getLastErrorCode()) && deviceAlerts < 3) { String message = StringUtils.isBlank(device.getLastErrorMessage()) ? device.getLastErrorCode() : device.getLastErrorMessage(); alerts.add(alert("warning", "「" + device.getDisplayName() + "」最近一次采集失败:" + message)); deviceAlerts++; } } if (Boolean.TRUE.equals(vo.getMockMode())) { alerts.add(alert("info", "当前为演示数据模式,采集结果由本地模拟生成,仅用于联调与页面演示。")); } if (alerts.size() > 6) { vo.setAlerts(new ArrayList<>(alerts.subList(0, 6))); } } private EnergyRealtimeVO.Alert alert(String level, String text) { EnergyRealtimeVO.Alert alert = new EnergyRealtimeVO.Alert(); alert.setLevel(level); alert.setText(text); return alert; } /* ------------------------------------------------------------------ */ /* 统计分析 */ /* ------------------------------------------------------------------ */ @Override public EnergyReportVO report(String startDate, String endDate, String granularity, String meterType, String dimension) { LocalDate today = EnergyMeterSupport.today(); LocalDate start = StringUtils.isBlank(startDate) ? today.withDayOfMonth(1) : parseDate(startDate, "开始日期"); LocalDate end = StringUtils.isBlank(endDate) ? today : parseDate(endDate, "结束日期"); if (end.isBefore(start)) { throw new ServiceException("结束日期不能早于开始日期"); } if (ChronoUnit.DAYS.between(start, end) > MAX_REPORT_DAYS) { throw new ServiceException("统计跨度不能超过 " + MAX_REPORT_DAYS + " 天,请缩小时间范围"); } String gran = "MONTH".equalsIgnoreCase(StringUtils.trim(granularity)) ? "MONTH" : "DAY"; String dim = "DEVICE".equalsIgnoreCase(StringUtils.trim(dimension)) ? "DEVICE" : "MEDIUM"; String filterType = StringUtils.isBlank(meterType) || "ALL".equalsIgnoreCase(meterType.trim()) ? "ALL" : EnergyMeterConstants.canonicalType(meterType); if (filterType == null) { throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或蒸汽表(GAS)"); } List types = "ALL".equals(filterType) ? ALL_TYPES : List.of(filterType); EnergyReportVO vo = new EnergyReportVO(); vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT)); vo.setMockMode(provider.mockMode()); vo.setStart(start.format(DAY_FORMAT)); vo.setEnd(end.format(DAY_FORMAT)); vo.setGranularity(gran); vo.setDimension(dim); vo.setMeterType(filterType); List periods = buildPeriods(start, end, gran); vo.setPeriods(periods); vo.setTotalPeriods(periods.size()); vo.setNote("统计口径:仅汇总日粒度用量,表码不参与累加;缺失周期按空值展示,不按 0 计算。"); List devices = loadDevices(null).stream() .filter(d -> types.contains(d.getMeterType())) .collect(Collectors.toList()); Map> valuesByType = new HashMap<>(); for (String type : types) { List ids = devices.stream() .filter(d -> type.equals(d.getMeterType())) .map(EnergyMeterDevice::getId) .collect(Collectors.toList()); Map values = new HashMap<>(); if (!ids.isEmpty()) { if ("MONTH".equals(gran)) { for (MonthlyUsageRow row : measurementMapper.sumMonthlyUsage(ids, EnergyMeterConstants.usageMetric(type), start.atStartOfDay(), end.plusDays(1).atStartOfDay())) { if (row.getMonth() != null && row.getTotal() != null) { values.put(row.getMonth(), row.getTotal()); } } } else { for (DailyUsageRow row : measurementMapper.sumDailyUsage(ids, EnergyMeterConstants.usageMetric(type), start.atStartOfDay(), end.plusDays(1).atStartOfDay())) { if (row.getDay() != null && row.getTotal() != null) { values.put(row.getDay(), row.getTotal()); } } } } valuesByType.put(type, values); } int coveredPeriods = 0; for (String period : periods) { EnergyReportVO.PeriodRow row = new EnergyReportVO.PeriodRow(); row.setPeriod(period); boolean hasValue = false; for (String type : types) { BigDecimal value = valuesByType.get(type).get(period); if (value != null) { hasValue = true; } if (EnergyMeterConstants.isWater(type)) { row.setWater(value); } else if (EnergyMeterConstants.isElectric(type)) { row.setElectric(value); } else { row.setGas(value); } } if (hasValue) { coveredPeriods++; } vo.getRows().add(row); } vo.setCoveredPeriods(coveredPeriods); Map mediumTotals = new HashMap<>(); for (String type : types) { BigDecimal total = sumValues(valuesByType.get(type).values()); mediumTotals.put(type, total); EnergyReportVO.MediumTotal mediumTotal = new EnergyReportVO.MediumTotal(); mediumTotal.setMeterType(type); mediumTotal.setMeterName(mediumName(type)); mediumTotal.setUnit(EnergyMeterConstants.unit(type)); mediumTotal.setTotal(total == null ? null : total.setScale(3, RoundingMode.HALF_UP)); mediumTotal.setDeviceCount((int) devices.stream() .filter(d -> type.equals(d.getMeterType())).count()); vo.getTotals().add(mediumTotal); } for (String type : types) { List typeDevices = devices.stream() .filter(d -> type.equals(d.getMeterType())) .collect(Collectors.toList()); if (typeDevices.isEmpty()) { continue; } List ids = typeDevices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList()); Map usageMap = new HashMap<>(); for (MeterUsageRow row : measurementMapper.sumUsageByMeter(ids, EnergyMeterConstants.usageMetric(type), start.atStartOfDay(), end.plusDays(1).atStartOfDay())) { if (row.getMeterId() != null && row.getTotal() != null) { usageMap.put(row.getMeterId(), row.getTotal()); } } BigDecimal mediumTotal = mediumTotals.get(type); for (EnergyMeterDevice device : typeDevices) { EnergyReportVO.DeviceRow row = new EnergyReportVO.DeviceRow(); row.setMeterId(device.getId()); row.setMeterName(device.getDisplayName()); row.setMeterAddress(device.getMeterAddress()); row.setMeterType(type); row.setMeterTypeName(mediumName(type)); row.setSourceTypeName(EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated()) ? "自动采集" : "手工抄表"); row.setUnit(EnergyMeterConstants.unit(type)); BigDecimal total = usageMap.get(device.getId()); row.setTotal(total == null ? null : total.setScale(3, RoundingMode.HALF_UP)); if (total != null && mediumTotal != null && mediumTotal.compareTo(BigDecimal.ZERO) > 0) { row.setShare(total.multiply(BigDecimal.valueOf(100)) .divide(mediumTotal, 1, RoundingMode.HALF_UP)); } vo.getDevices().add(row); } } vo.getDevices().sort(Comparator .comparingInt((EnergyReportVO.DeviceRow row) -> typeOrder(row.getMeterType())) .thenComparing(EnergyReportVO.DeviceRow::getTotal, Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing(EnergyReportVO.DeviceRow::getMeterId)); return vo; } private int typeOrder(String type) { if (EnergyMeterConstants.isWater(type)) { return 0; } if (EnergyMeterConstants.isElectric(type)) { return 1; } return 2; } private List buildPeriods(LocalDate start, LocalDate end, String gran) { List periods = new ArrayList<>(); if ("MONTH".equals(gran)) { YearMonth month = YearMonth.from(start); YearMonth last = YearMonth.from(end); while (!month.isAfter(last)) { periods.add(month.toString()); month = month.plusMonths(1); } } else { for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) { periods.add(day.format(DAY_FORMAT)); } } return periods; } /* ------------------------------------------------------------------ */ /* 工具 */ /* ------------------------------------------------------------------ */ private List loadDevices(String meterType) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); Long tenantId = EnergyMeterSupport.currentTenantId(); if (tenantId != null) { wrapper.eq(EnergyMeterDevice::getTenantId, tenantId); } if (StringUtils.isNotBlank(meterType)) { wrapper.eq(EnergyMeterDevice::getMeterType, meterType); } wrapper.eq(EnergyMeterDevice::getEnabled, 1) .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED) .orderByAsc(EnergyMeterDevice::getId); return deviceMapper.selectList(wrapper); } private EnergyMeterDevice requireDevice(Long meterId) { EnergyMeterDevice device = deviceMapper.selectById(meterId); if (device == null) { throw new ServiceException("设备不存在或已删除"); } Long tenantId = EnergyMeterSupport.currentTenantId(); if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) { throw new ServiceException("无权访问该设备"); } return device; } private String mediumName(String meterType) { if (EnergyMeterConstants.isWater(meterType)) { return "水表"; } if (EnergyMeterConstants.isElectric(meterType)) { return "电表"; } if (EnergyMeterConstants.isGas(meterType)) { return "蒸汽表"; } return meterType; } private Map toDayMap(List rows) { Map map = new HashMap<>(); for (DailyUsageRow row : rows) { if (row.getDay() != null && row.getTotal() != null) { map.put(LocalDate.parse(row.getDay()), row.getTotal()); } } return map; } private LocalDate latestDate(Map daily, LocalDate from, LocalDate floor) { LocalDate day = from; while (!day.isBefore(floor)) { BigDecimal value = daily.get(day); if (value != null) { return day; } day = day.minusDays(1); } return null; } private BigDecimal compareRate(BigDecimal latest, BigDecimal previous) { if (latest == null || previous == null || previous.compareTo(BigDecimal.ZERO) <= 0) { return null; } return latest.subtract(previous) .multiply(BigDecimal.valueOf(100)) .divide(previous, 1, RoundingMode.HALF_UP); } private BigDecimal sumValues(java.util.Collection values) { BigDecimal sum = BigDecimal.ZERO; boolean hasValue = false; for (BigDecimal value : values) { if (value != null) { sum = sum.add(value); hasValue = true; } } return hasValue ? sum : null; } private BigDecimal average(List values) { if (values == null || values.isEmpty()) { return null; } BigDecimal sum = BigDecimal.ZERO; for (BigDecimal value : values) { sum = sum.add(value); } return sum.divide(BigDecimal.valueOf(values.size()), 3, RoundingMode.HALF_UP); } private int nvl(Integer value) { return value == null ? 0 : value; } private LocalDate parseDate(String value, String label) { try { return LocalDate.parse(value.trim()); } catch (Exception e) { throw new ServiceException(label + "格式应为 yyyy-MM-dd"); } } }