package com.ruoyi.energymeter.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.MonthlyUsageRow;
|
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
|
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
|
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
|
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
|
import com.ruoyi.energymeter.provider.MeterProviderClient;
|
import com.ruoyi.energymeter.service.EnergyMeterStatService;
|
import com.ruoyi.energymeter.util.EnergyMeterSupport;
|
import com.ruoyi.energymeter.vo.EnergyMeterOverviewVO;
|
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.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.stream.Collectors;
|
|
/**
|
* 概览统计实现。
|
*
|
* <p>只从 D 粒度的 *_USAGE 指标汇总,绝不累加 POINT 表码;缺失日期保持 null,由前端显示缺口。</p>
|
*/
|
@Slf4j
|
@Service
|
@RequiredArgsConstructor
|
public class EnergyMeterStatServiceImpl implements EnergyMeterStatService {
|
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
private final EnergyMeterDeviceMapper deviceMapper;
|
private final EnergyMeterMeasurementMapper measurementMapper;
|
private final MeterProviderClient provider;
|
|
@Override
|
public EnergyMeterOverviewVO overview(String meterType, Long meterId, String month, Integer year) {
|
String type = normalizeType(meterType);
|
YearMonth selectedMonth = parseMonth(month);
|
int selectedYear = year == null ? selectedMonth.getYear() : year;
|
if (selectedYear < 2000 || selectedYear > 2100) {
|
throw new ServiceException("年份不合法");
|
}
|
|
EnergyMeterOverviewVO vo = new EnergyMeterOverviewVO();
|
vo.setMeterType(type);
|
vo.setMetricCode(EnergyMeterConstants.usageMetric(type));
|
vo.setUnit(EnergyMeterConstants.unit(type));
|
vo.setMeterId(meterId);
|
vo.setMockMode(provider.mockMode());
|
vo.setSelectedMonth(selectedMonth.toString());
|
vo.setSelectedYear(String.valueOf(selectedYear));
|
vo.setMonthCost(null);
|
vo.setYearCost(null);
|
|
List<EnergyMeterDevice> devices = resolveDevices(type, meterId);
|
vo.setDeviceCount(devices.size());
|
vo.setScopeLabel(buildScopeLabel(devices, meterId));
|
List<Long> meterIds = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
|
|
LocalDate today = EnergyMeterSupport.today();
|
LocalDate yesterday = today.minusDays(1);
|
LocalDate monthStart = selectedMonth.atDay(1);
|
LocalDate monthEndExclusive = selectedMonth.plusMonths(1).atDay(1);
|
YearMonth previousMonth = selectedMonth.minusMonths(1);
|
|
Map<LocalDate, BigDecimal> currentDaily = toDayMap(monthIdsEmpty(meterIds) ? List.of() : measurementMapper.sumDailyUsage(
|
meterIds, vo.getMetricCode(), monthStart.atStartOfDay(), monthEndExclusive.atStartOfDay()));
|
Map<LocalDate, BigDecimal> previousDaily = toDayMap(monthIdsEmpty(meterIds) ? List.of() : measurementMapper.sumDailyUsage(
|
meterIds, vo.getMetricCode(), previousMonth.atDay(1).atStartOfDay(), monthStart.atStartOfDay()));
|
|
int daysInMonth = selectedMonth.lengthOfMonth();
|
int daysInPreviousMonth = previousMonth.lengthOfMonth();
|
int cutDay = resolveCutDay(selectedMonth, today);
|
|
List<EnergyMeterOverviewVO.MonthDayPoint> monthSeries = new ArrayList<>(daysInMonth);
|
BigDecimal monthTotal = BigDecimal.ZERO;
|
int collectedDays = 0;
|
for (int day = 1; day <= daysInMonth; day++) {
|
LocalDate date = monthStart.withDayOfMonth(day);
|
BigDecimal currentValue = currentDaily.get(date);
|
if (currentValue != null) {
|
monthTotal = monthTotal.add(currentValue);
|
if (day <= cutDay) {
|
collectedDays++;
|
}
|
}
|
EnergyMeterOverviewVO.MonthDayPoint point = new EnergyMeterOverviewVO.MonthDayPoint();
|
point.setDay(date.format(DATE_FORMAT));
|
point.setCurrent(currentValue);
|
point.setPrevious(day <= daysInPreviousMonth ? previousDaily.get(previousMonth.atDay(day)) : null);
|
monthSeries.add(point);
|
}
|
vo.setMonthSeries(monthSeries);
|
boolean monthHasData = !currentDaily.isEmpty();
|
vo.setMonthHasData(monthHasData);
|
vo.setMonthTotal(monthHasData ? monthTotal : null);
|
|
BigDecimal previousTotal = BigDecimal.ZERO;
|
int previousDayCount = 0;
|
int compareDays = Math.min(cutDay, daysInPreviousMonth);
|
for (int day = 1; day <= compareDays; day++) {
|
BigDecimal value = previousDaily.get(previousMonth.atDay(day));
|
if (value != null) {
|
previousTotal = previousTotal.add(value);
|
previousDayCount++;
|
}
|
}
|
vo.setMonthPreviousTotal(previousDayCount > 0 ? previousTotal : null);
|
|
int expectedDays = cutDay;
|
// 设备入网之前的日期不应计入缺口:从设备同步起点与月初的较大者开始算
|
if (!devices.isEmpty()) {
|
LocalDate earliestSync = devices.stream()
|
.map(EnergyMeterDevice::getSyncStartDate)
|
.filter(java.util.Objects::nonNull)
|
.min(LocalDate::compareTo)
|
.orElse(null);
|
if (earliestSync != null && earliestSync.isAfter(monthStart)) {
|
int days = (int) java.time.temporal.ChronoUnit.DAYS.between(earliestSync,
|
monthStart.plusDays(cutDay));
|
expectedDays = Math.max(0, Math.min(days, cutDay));
|
}
|
}
|
vo.setCollectedDays(collectedDays);
|
vo.setExpectedDays(expectedDays);
|
vo.setCoverageRate(expectedDays <= 0 ? BigDecimal.valueOf(100)
|
: BigDecimal.valueOf(collectedDays * 100.0 / expectedDays).setScale(1, RoundingMode.HALF_UP));
|
|
java.time.LocalDate yearStart = LocalDate.of(selectedYear, 1, 1);
|
java.time.LocalDate yearEndExclusive = yearStart.plusYears(1);
|
java.time.LocalDate previousYearStart = yearStart.minusYears(1);
|
Map<String, BigDecimal> currentMonthly = toMonthMap(monthIdsEmpty(meterIds) ? List.of()
|
: measurementMapper.sumMonthlyUsage(meterIds, vo.getMetricCode(),
|
yearStart.atStartOfDay(), yearEndExclusive.atStartOfDay()));
|
Map<String, BigDecimal> previousMonthly = toMonthMap(monthIdsEmpty(meterIds) ? List.of()
|
: measurementMapper.sumMonthlyUsage(meterIds, vo.getMetricCode(),
|
previousYearStart.atStartOfDay(), yearStart.atStartOfDay()));
|
boolean yearHasData = !currentMonthly.isEmpty();
|
vo.setYearHasData(yearHasData);
|
|
int currentYear = today.getYear();
|
int cutMonth = selectedYear < currentYear ? 12 : (selectedYear > currentYear ? 0 : today.getMonthValue());
|
List<EnergyMeterOverviewVO.YearMonthPoint> yearSeries = new ArrayList<>(12);
|
BigDecimal yearTotal = BigDecimal.ZERO;
|
for (int m = 1; m <= 12; m++) {
|
String key = selectedYear + "-" + String.format("%02d", m);
|
String previousKey = (selectedYear - 1) + "-" + String.format("%02d", m);
|
BigDecimal currentValue = currentMonthly.get(key);
|
if (currentValue != null) {
|
yearTotal = yearTotal.add(currentValue);
|
}
|
EnergyMeterOverviewVO.YearMonthPoint point = new EnergyMeterOverviewVO.YearMonthPoint();
|
point.setMonth(key);
|
point.setCurrent(currentValue);
|
point.setPrevious(previousMonthly.get(previousKey));
|
yearSeries.add(point);
|
}
|
vo.setYearSeries(yearSeries);
|
vo.setYearTotal(yearHasData ? yearTotal : null);
|
|
BigDecimal previousYearTotal = BigDecimal.ZERO;
|
int previousMonthCount = 0;
|
for (int m = 1; m <= cutMonth; m++) {
|
BigDecimal value = previousMonthly.get((selectedYear - 1) + "-" + String.format("%02d", m));
|
if (value != null) {
|
previousYearTotal = previousYearTotal.add(value);
|
previousMonthCount++;
|
}
|
}
|
vo.setYearPreviousTotal(previousMonthCount > 0 ? previousYearTotal : null);
|
|
DataRangeRow range = monthIdsEmpty(meterIds) ? null
|
: measurementMapper.selectDataRange(meterIds, vo.getMetricCode());
|
vo.setDataStart(range != null && range.getMinTime() != null ? range.getMinTime().toLocalDate().format(DATE_FORMAT) : null);
|
vo.setDataEnd(range != null && range.getMaxTime() != null ? range.getMaxTime().toLocalDate().format(DATE_FORMAT) : null);
|
vo.setDataThrough(yesterday.format(DATE_FORMAT));
|
|
LocalDateTime lastSuccess = devices.stream()
|
.map(EnergyMeterDevice::getLastSuccessAt)
|
.filter(java.util.Objects::nonNull)
|
.max(LocalDateTime::compareTo)
|
.orElse(null);
|
vo.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
|
boolean hasData = monthHasData || yearHasData || range != null && range.getMaxTime() != null;
|
vo.setHasData(hasData);
|
long manualCount = devices.stream()
|
.filter(d -> EnergyMeterConstants.isManual(d.getSourceType()))
|
.count();
|
boolean manualOnly = !devices.isEmpty() && manualCount == devices.size();
|
int expectedForStatus = expectedDays;
|
if (manualOnly) {
|
// 手工抄表按“抄表期数”计,不按自然日算缺口
|
vo.setExpectedDays(collectedDays);
|
vo.setCoverageRate(collectedDays > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO);
|
expectedForStatus = collectedDays;
|
}
|
applySyncStatus(vo, hasData, lastSuccess, collectedDays, expectedForStatus, monthHasData, yearHasData, manualOnly);
|
return vo;
|
}
|
|
@Override
|
public IPage<EnergyMeterMeasurement> pageMeasurements(Long meterId, String metricCode, String granularity,
|
LocalDate start, LocalDate end, long current, long size) {
|
EnergyMeterDevice device = requireDevice(meterId);
|
long safeSize = Math.min(Math.max(size, 1), 200);
|
Page<EnergyMeterMeasurement> page = new Page<>(Math.max(current, 1), safeSize);
|
LambdaQueryWrapper<EnergyMeterMeasurement> wrapper = new LambdaQueryWrapper<>();
|
wrapper.eq(EnergyMeterMeasurement::getMeterId, device.getId());
|
if (StringUtils.isNotBlank(metricCode)) {
|
wrapper.eq(EnergyMeterMeasurement::getMetricCode, metricCode);
|
}
|
if (StringUtils.isNotBlank(granularity)) {
|
wrapper.eq(EnergyMeterMeasurement::getGranularity, granularity);
|
}
|
if (start != null) {
|
wrapper.ge(EnergyMeterMeasurement::getMeasureTime, start.atStartOfDay());
|
}
|
if (end != null) {
|
wrapper.lt(EnergyMeterMeasurement::getMeasureTime, end.plusDays(1).atStartOfDay());
|
}
|
wrapper.orderByDesc(EnergyMeterMeasurement::getMeasureTime)
|
.orderByDesc(EnergyMeterMeasurement::getId);
|
IPage<EnergyMeterMeasurement> result = measurementMapper.selectPage(page, wrapper);
|
result.getRecords().forEach(item -> {
|
item.setMeterName(device.getDisplayName());
|
item.setMeterType(device.getMeterType());
|
item.setMeterAddress(device.getMeterAddress());
|
});
|
return result;
|
}
|
|
/* ------------------------------------------------------------------ */
|
|
private List<EnergyMeterDevice> resolveDevices(String meterType, Long meterId) {
|
if (meterId != null) {
|
EnergyMeterDevice device = requireDevice(meterId);
|
if (!meterType.equals(device.getMeterType())) {
|
throw new ServiceException("设备类型与当前能源类型不一致");
|
}
|
return List.of(device);
|
}
|
LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
|
Long tenantId = EnergyMeterSupport.currentTenantId();
|
if (tenantId != null) {
|
wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
|
}
|
wrapper.eq(EnergyMeterDevice::getMeterType, meterType)
|
.eq(EnergyMeterDevice::getEnabled, 1)
|
.eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
|
.orderByAsc(EnergyMeterDevice::getId);
|
return deviceMapper.selectList(wrapper);
|
}
|
|
private EnergyMeterDevice requireDevice(Long meterId) {
|
if (meterId == null) {
|
throw new ServiceException("设备ID不能为空");
|
}
|
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 buildScopeLabel(List<EnergyMeterDevice> devices, Long meterId) {
|
if (meterId != null && !devices.isEmpty()) {
|
EnergyMeterDevice device = devices.get(0);
|
return device.getDisplayName() + "(" + device.getMeterAddress() + ")";
|
}
|
return "全部已绑定设备(" + devices.size() + " 台)";
|
}
|
|
private void applySyncStatus(EnergyMeterOverviewVO vo, boolean hasData, LocalDateTime lastSuccess,
|
int collectedDays, int expectedDays, boolean monthHasData, boolean yearHasData,
|
boolean manualOnly) {
|
String status;
|
String text;
|
if (manualOnly) {
|
if (!monthHasData && !yearHasData) {
|
status = "EMPTY";
|
text = "暂无抄表记录";
|
} else if (expectedDays > 0 && collectedDays < expectedDays) {
|
status = "PARTIAL";
|
text = "抄表数据存在缺口(" + collectedDays + "/" + expectedDays + " 天)";
|
} else {
|
status = "OK";
|
text = "手工抄表数据";
|
}
|
vo.setSyncStatus(status);
|
vo.setSyncStatusText(text);
|
return;
|
}
|
if (!monthHasData && !yearHasData) {
|
status = "EMPTY";
|
text = "暂无采集数据";
|
} else if (!hasData) {
|
status = "EMPTY";
|
text = "暂无采集数据";
|
} else if (lastSuccess == null
|
|| lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) {
|
status = "STALE";
|
text = "超过 36 小时无成功采集";
|
} else if (expectedDays > 0 && collectedDays < expectedDays) {
|
status = "PARTIAL";
|
text = "数据存在缺口(" + collectedDays + "/" + expectedDays + " 天)";
|
} else {
|
status = "OK";
|
text = "数据完整";
|
}
|
vo.setSyncStatus(status);
|
vo.setSyncStatusText(text);
|
}
|
|
private int resolveCutDay(YearMonth month, LocalDate today) {
|
YearMonth currentMonth = YearMonth.from(today);
|
if (month.isAfter(currentMonth)) {
|
return 0;
|
}
|
if (month.equals(currentMonth)) {
|
return Math.max(today.minusDays(1).getDayOfMonth(), 0);
|
}
|
return month.lengthOfMonth();
|
}
|
|
private Map<LocalDate, BigDecimal> toDayMap(List<DailyUsageRow> rows) {
|
Map<LocalDate, BigDecimal> 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 Map<String, BigDecimal> toMonthMap(List<MonthlyUsageRow> rows) {
|
Map<String, BigDecimal> map = new HashMap<>();
|
for (MonthlyUsageRow row : rows) {
|
if (row.getMonth() != null && row.getTotal() != null) {
|
map.put(row.getMonth(), row.getTotal());
|
}
|
}
|
return map;
|
}
|
|
private boolean monthIdsEmpty(List<Long> ids) {
|
return ids == null || ids.isEmpty();
|
}
|
|
private YearMonth parseMonth(String month) {
|
if (StringUtils.isBlank(month)) {
|
return YearMonth.now(EnergyMeterConstants.ZONE);
|
}
|
try {
|
return YearMonth.parse(month.trim());
|
} catch (Exception e) {
|
throw new ServiceException("月份格式应为 yyyy-MM");
|
}
|
}
|
|
private String normalizeType(String meterType) {
|
String type = EnergyMeterConstants.canonicalType(meterType);
|
if (type == null) {
|
throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或气表(GAS)");
|
}
|
return type;
|
}
|
}
|