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");
}
}
}