package com.ruoyi.energymeter.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.energymeter.constant.EnergyMeterConstants; import com.ruoyi.energymeter.dto.EnergyMeterDeviceQuery; import com.ruoyi.energymeter.dto.MeterArchive; import com.ruoyi.energymeter.dto.MeterBindRequest; import com.ruoyi.energymeter.dto.MeterManualBindRequest; import com.ruoyi.energymeter.dto.MeterReadingRequest; import com.ruoyi.energymeter.dto.MeterUpdateRequest; import com.ruoyi.energymeter.dto.MeterUsageRow; import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper; import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper; import com.ruoyi.energymeter.mapper.EnergyMeterSyncTaskMapper; import com.ruoyi.energymeter.pojo.EnergyMeterDevice; import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement; import com.ruoyi.energymeter.pojo.EnergyMeterSyncTask; import com.ruoyi.energymeter.provider.MeterProviderClient; import com.ruoyi.energymeter.service.EnergyMeterDeviceService; import com.ruoyi.energymeter.service.EnergyMeterSyncService; import com.ruoyi.energymeter.util.EnergyMeterSupport; import com.ruoyi.energymeter.vo.MeterArchiveVO; import com.ruoyi.energymeter.vo.MeterReadingResultVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 能耗表设备服务实现:第三方接入设备 + 手工抄表设备。 */ @Slf4j @Service @RequiredArgsConstructor public class EnergyMeterDeviceServiceImpl extends ServiceImpl implements EnergyMeterDeviceService { private final EnergyMeterDeviceMapper deviceMapper; private final EnergyMeterMeasurementMapper measurementMapper; private final EnergyMeterSyncTaskMapper taskMapper; private final MeterProviderClient provider; private final EnergyMeterSyncService syncService; @Override public IPage pageDevices(Page page, EnergyMeterDeviceQuery query) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); Long tenantId = EnergyMeterSupport.currentTenantId(); if (tenantId != null) { wrapper.eq(EnergyMeterDevice::getTenantId, tenantId); } if (query != null) { if (StringUtils.isNotBlank(query.getMeterType())) { wrapper.eq(EnergyMeterDevice::getMeterType, normalizeType(query.getMeterType())); } if (StringUtils.isNotBlank(query.getSourceType())) { wrapper.eq(EnergyMeterDevice::getSourceType, query.getSourceType().trim().toUpperCase()); } if (StringUtils.isNotBlank(query.getMeterAddress())) { wrapper.eq(EnergyMeterDevice::getMeterAddress, query.getMeterAddress().trim()); } if (query.getEnabled() != null) { wrapper.eq(EnergyMeterDevice::getEnabled, query.getEnabled()); } if (query.getSimulated() != null) { wrapper.eq(EnergyMeterDevice::getSimulated, query.getSimulated()); } if (StringUtils.isNotBlank(query.getCollectionMode())) { String mode = query.getCollectionMode().trim().toUpperCase(); if ("AUTO".equals(mode)) { // 第三方接入 + 系统每日自动生成,都属于自动采集 wrapper.and(w -> w.eq(EnergyMeterDevice::getSourceType, EnergyMeterConstants.SOURCE_TYPE_PROVIDER) .or().eq(EnergyMeterDevice::getSimulated, 1)); } else if ("MANUAL".equals(mode)) { wrapper.eq(EnergyMeterDevice::getSourceType, EnergyMeterConstants.SOURCE_TYPE_MANUAL) .eq(EnergyMeterDevice::getSimulated, 0); } } if (StringUtils.isNotBlank(query.getBindStatus())) { wrapper.eq(EnergyMeterDevice::getBindStatus, query.getBindStatus()); } if (StringUtils.isNotBlank(query.getKeyword())) { String keyword = query.getKeyword().trim(); wrapper.and(w -> w.like(EnergyMeterDevice::getDisplayName, keyword) .or().like(EnergyMeterDevice::getProviderName, keyword) .or().like(EnergyMeterDevice::getMeterAddress, keyword)); } } wrapper.orderByAsc(EnergyMeterDevice::getMeterType) .orderByDesc(EnergyMeterDevice::getIsSeed) .orderByAsc(EnergyMeterDevice::getId); IPage result = deviceMapper.selectPage(page, wrapper); enrichDevices(result.getRecords()); return result; } @Override public List listDeviceOptions(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, normalizeType(meterType)); } wrapper.eq(EnergyMeterDevice::getEnabled, 1) .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED) .orderByAsc(EnergyMeterDevice::getId); return deviceMapper.selectList(wrapper); } @Override public MeterArchiveVO verifyArchive(String meterType, String meterAddress) { String normalizedType = normalizeType(meterType); String address = StringUtils.trim(meterAddress); if (StringUtils.isBlank(address)) { throw new ServiceException("通信地址不能为空"); } MeterArchive archive = provider.queryArchive(normalizedType, address); MeterArchiveVO vo = new MeterArchiveVO(); vo.setMeterType(normalizedType); vo.setMeterAddress(address); vo.setProviderMeterId(archive.getProviderMeterId()); vo.setProviderName(archive.getProviderName()); vo.setAreaId(archive.getAreaId()); vo.setAreaName(archive.getAreaName()); vo.setOnlineStatus(archive.getOnlineStatus()); vo.setParentProviderId(archive.getParentProviderId()); vo.setMockMode(provider.mockMode()); EnergyMeterDevice existing = findBinding(normalizedType, address); if (existing == null) { vo.setBindable(true); vo.setMessage("验证通过,可新增绑定"); } else { vo.setExistingDeviceId(existing.getId()); vo.setExistingEnabled(Integer.valueOf(1).equals(existing.getEnabled())); boolean enabled = Integer.valueOf(1).equals(existing.getEnabled()); vo.setBindable(!enabled); vo.setMessage(enabled ? "该通信地址已在本地绑定,请勿重复新增" : "该地址已存在停用设备,确认后将重新启用并补采停用期间数据"); } return vo; } @Override @Transactional(rollbackFor = Exception.class) public EnergyMeterDevice bind(MeterBindRequest request) { String normalizedType = normalizeType(request.getMeterType()); String address = StringUtils.trim(request.getMeterAddress()); if (StringUtils.isBlank(address)) { throw new ServiceException("通信地址不能为空"); } MeterArchive archive = provider.queryArchive(normalizedType, address); EnergyMeterDevice existing = findBinding(normalizedType, address); if (existing != null && Integer.valueOf(1).equals(existing.getEnabled())) { throw new ServiceException("该通信地址已在本地绑定,请勿重复新增"); } if (existing != null && EnergyMeterConstants.isManual(existing.getSourceType())) { throw new ServiceException("该表号已用于手工抄表设备,请先删除原设备或改用其他表号"); } LocalDate yesterday = EnergyMeterSupport.today().minusDays(1); // 采集起点由系统自动确定:已停用设备沿用原起点,新设备默认上一自然年 1 月 1 日 LocalDate startDate = resolveStartDate(null, existing != null ? existing.getSyncStartDate() : null, yesterday); EnergyMeterDevice device = existing == null ? new EnergyMeterDevice() : existing; device.setMeterType(normalizedType); device.setSourceType(EnergyMeterConstants.SOURCE_TYPE_PROVIDER); device.setMeterAddress(address); device.setProviderMeterId(archive.getProviderMeterId()); device.setProviderName(archive.getProviderName()); device.setProviderAreaId(archive.getAreaId()); device.setProviderAreaName(archive.getAreaName()); device.setParentProviderId(archive.getParentProviderId()); device.setOnlineStatus(archive.getOnlineStatus()); device.setBindStatus(EnergyMeterConstants.BIND_VERIFIED); device.setEnabled(1); device.setSyncStartDate(startDate); device.setDisplayName(StringUtils.isNotBlank(request.getDisplayName()) ? request.getDisplayName().trim() : StringUtils.defaultIfBlank(archive.getProviderName(), address)); device.setLocation(StringUtils.defaultIfBlank(request.getLocation(), device.getLocation())); if (device.getIsSeed() == null) { device.setIsSeed(0); } if (existing == null) { deviceMapper.insert(device); } else { deviceMapper.updateById(device); } log.info("能耗表绑定成功 meterId={} type={} address={}", device.getId(), normalizedType, address); try { syncService.triggerBackfill(device, startDate, yesterday); } catch (Exception e) { log.warn("绑定成功但历史补采批次创建失败 meterId={}: {}", device.getId(), e.getMessage()); } return device; } @Override @Transactional(rollbackFor = Exception.class) public EnergyMeterDevice addManual(MeterManualBindRequest request) { String type = normalizeType(request.getMeterType()); String address = StringUtils.trim(request.getMeterAddress()); if (StringUtils.isBlank(address)) { throw new ServiceException("表号不能为空"); } if (request.getInitialReading() == null) { throw new ServiceException("起始表码不能为空"); } if (request.getInitialReading().signum() < 0) { throw new ServiceException("起始表码不能为负数"); } LocalDate today = EnergyMeterSupport.today(); LocalDate initialDate = request.getInitialReadingDate() == null ? today : request.getInitialReadingDate(); if (initialDate.isAfter(today)) { throw new ServiceException("起始表码日期不能晚于今天"); } EnergyMeterDevice existing = findBinding(type, address); if (existing != null) { if (Integer.valueOf(1).equals(existing.getEnabled())) { throw new ServiceException("该表号已存在有效的" + EnergyMeterConstants.typeName(type) + "设备,请勿重复新增"); } if (!EnergyMeterConstants.isManual(existing.getSourceType())) { throw new ServiceException("该表号已被第三方接入设备占用,请先停用原设备或改用其他表号"); } } EnergyMeterDevice device = existing == null ? new EnergyMeterDevice() : existing; device.setMeterType(type); device.setSourceType(EnergyMeterConstants.SOURCE_TYPE_MANUAL); device.setMeterAddress(address); device.setDisplayName(StringUtils.isNotBlank(request.getDisplayName()) ? request.getDisplayName().trim() : address); device.setLocation(StringUtils.defaultIfBlank(request.getLocation(), device.getLocation())); device.setBindStatus(EnergyMeterConstants.BIND_VERIFIED); device.setEnabled(1); device.setIsSeed(0); device.setInitialReading(request.getInitialReading()); device.setInitialReadingDate(initialDate); device.setSyncStartDate(initialDate); device.setOnlineStatus(null); device.setLastErrorCode(null); device.setLastErrorMessage(null); if (existing == null) { deviceMapper.insert(device); } else { deviceMapper.updateById(device); } // 起始表码作为基准记录写入,不计入用量 EnergyMeterMeasurement basePoint = newManualPoint(device, initialDate.atStartOfDay(), request.getInitialReading(), "起始表码"); measurementMapper.upsertBatch(List.of(basePoint)); deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now()); log.info("新增手工抄表设备 meterId={} type={} address={} initial={}", device.getId(), type, address, request.getInitialReading()); return deviceMapper.selectById(device.getId()); } @Override @Transactional(rollbackFor = Exception.class) public void removeDevice(Long id) { EnergyMeterDevice device = getOwnedDevice(id); if (Integer.valueOf(1).equals(device.getIsSeed())) { throw new ServiceException("初始化设备不允许删除,如需停用请编辑设备状态"); } if (!EnergyMeterConstants.isManual(device.getSourceType())) { throw new ServiceException("第三方接入设备请使用“停用”,不允许删除"); } measurementMapper.delete(new LambdaQueryWrapper() .eq(EnergyMeterMeasurement::getMeterId, id)); removeById(id); log.info("删除手工抄表设备 meterId={} address={}", id, device.getMeterAddress()); } @Override public IPage pageReadings(Long meterId, long current, long size) { EnergyMeterDevice device = getOwnedDevice(meterId); String readingMetric = EnergyMeterConstants.readingMetric(device.getMeterType()); String usageMetric = EnergyMeterConstants.usageMetric(device.getMeterType()); long safeSize = Math.min(Math.max(size, 1), 200); Page page = new Page<>(Math.max(current, 1), safeSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(EnergyMeterMeasurement::getMeterId, device.getId()) .eq(EnergyMeterMeasurement::getMetricCode, readingMetric) .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_POINT) .orderByDesc(EnergyMeterMeasurement::getMeasureTime) .orderByDesc(EnergyMeterMeasurement::getId); IPage result = measurementMapper.selectPage(page, wrapper); List records = result.getRecords(); if (!records.isEmpty()) { LocalDateTime maxTime = records.get(0).getMeasureTime(); LocalDateTime minTime = records.get(records.size() - 1).getMeasureTime(); List usages = measurementMapper.selectList( new LambdaQueryWrapper() .eq(EnergyMeterMeasurement::getMeterId, device.getId()) .eq(EnergyMeterMeasurement::getMetricCode, usageMetric) .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_DAY) .ge(EnergyMeterMeasurement::getMeasureTime, minTime) .le(EnergyMeterMeasurement::getMeasureTime, maxTime)); Map usageMap = new HashMap<>(); for (EnergyMeterMeasurement usage : usages) { usageMap.put(usage.getMeasureTime().toLocalDate(), usage.getMetricValue()); } for (EnergyMeterMeasurement record : records) { record.setUsageValue(usageMap.get(record.getMeasureTime().toLocalDate())); record.setMeterName(device.getDisplayName()); record.setMeterType(device.getMeterType()); record.setMeterAddress(device.getMeterAddress()); } } return result; } @Override @Transactional(rollbackFor = Exception.class) public MeterReadingResultVO addReading(Long meterId, MeterReadingRequest request) { EnergyMeterDevice device = requireManualDevice(meterId); if (request.getReadingDate() == null) { throw new ServiceException("抄表日期不能为空"); } if (request.getReadingValue() == null) { throw new ServiceException("本次表码不能为空"); } LocalDate date = request.getReadingDate(); LocalDate today = EnergyMeterSupport.today(); if (date.isAfter(today)) { throw new ServiceException("抄表日期不能晚于今天"); } if (device.getInitialReadingDate() != null && date.isBefore(device.getInitialReadingDate())) { throw new ServiceException("抄表日期不能早于起始表码日期(" + device.getInitialReadingDate() + ")"); } BigDecimal value = request.getReadingValue(); if (value.signum() < 0) { throw new ServiceException("本次表码不能为负数"); } BigDecimal previous = resolvePreviousReading(device, date.atStartOfDay()); if (previous != null && value.compareTo(previous) < 0) { throw new ServiceException("本次表码不能小于上期表码(上期 " + previous.stripTrailingZeros().toPlainString() + ")"); } EnergyMeterMeasurement point = newManualPoint(device, date.atStartOfDay(), value, StringUtils.defaultIfBlank(request.getRemark(), null)); measurementMapper.upsertBatch(List.of(point)); BigDecimal usage = previous == null ? null : value.subtract(previous); upsertUsage(device, date, usage); // 若该日期之后已有抄表,重新计算下一期的用量 EnergyMeterMeasurement next = measurementMapper.selectNextPoint(device.getId(), EnergyMeterConstants.readingMetric(device.getMeterType()), date.atStartOfDay()); if (next != null) { recalcUsage(device, next); } deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now()); MeterReadingResultVO result = new MeterReadingResultVO(); result.setMeterId(device.getId()); result.setMeasureTime(date.atStartOfDay()); result.setReadingValue(value); result.setPreviousReading(previous); result.setUsage(usage); result.setUnit(EnergyMeterConstants.unit(device.getMeterType())); return result; } @Override @Transactional(rollbackFor = Exception.class) public void removeReading(Long meterId, Long measurementId) { EnergyMeterDevice device = requireManualDevice(meterId); EnergyMeterMeasurement record = measurementMapper.selectById(measurementId); if (record == null || !meterId.equals(record.getMeterId())) { throw new ServiceException("抄表记录不存在"); } if (!EnergyMeterConstants.GRANULARITY_POINT.equals(record.getGranularity())) { throw new ServiceException("只能删除抄表记录"); } if (device.getInitialReadingDate() != null && device.getInitialReadingDate().equals(record.getMeasureTime().toLocalDate())) { throw new ServiceException("起始表码不允许删除,如需修改请编辑设备"); } EnergyMeterMeasurement next = measurementMapper.selectNextPoint(device.getId(), EnergyMeterConstants.readingMetric(device.getMeterType()), record.getMeasureTime()); measurementMapper.deleteById(measurementId); deleteUsage(device, record.getMeasureTime().toLocalDate()); if (next != null) { recalcUsage(device, next); } log.info("删除抄表记录 meterId={} measurementId={} date={}", meterId, measurementId, record.getMeasureTime().toLocalDate()); } @Override @Transactional(rollbackFor = Exception.class) public EnergyMeterDevice updateLocal(Long id, MeterUpdateRequest request) { EnergyMeterDevice device = getOwnedDevice(id); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(EnergyMeterDevice::getId, id); if (request.getDisplayName() != null) { if (StringUtils.isBlank(request.getDisplayName())) { throw new ServiceException("显示名称不能为空"); } wrapper.set(EnergyMeterDevice::getDisplayName, request.getDisplayName().trim()); } if (request.getLocation() != null) { wrapper.set(EnergyMeterDevice::getLocation, request.getLocation()); } boolean reEnabled = false; if (request.getEnabled() != null) { if (request.getEnabled() != 0 && request.getEnabled() != 1) { throw new ServiceException("启停用参数不合法"); } reEnabled = request.getEnabled() == 1 && !Integer.valueOf(1).equals(device.getEnabled()); wrapper.set(EnergyMeterDevice::getEnabled, request.getEnabled()); } deviceMapper.update(null, wrapper); EnergyMeterDevice updated = getOwnedDevice(id); if (reEnabled && !EnergyMeterConstants.isManual(updated.getSourceType())) { try { syncService.triggerBackfillGap(updated); } catch (Exception e) { log.warn("设备启用成功但补采批次创建失败 meterId={}: {}", id, e.getMessage()); } } enrichDevices(List.of(updated)); return updated; } @Override public EnergyMeterDevice getOwnedDevice(Long id) { EnergyMeterDevice device = deviceMapper.selectById(id); 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; } @Override public List listEnabledDevices(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, normalizeType(meterType)); } wrapper.eq(EnergyMeterDevice::getEnabled, 1) .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED) .orderByAsc(EnergyMeterDevice::getId); return deviceMapper.selectList(wrapper); } @Override public boolean mockMode() { return provider.mockMode(); } /* ------------------------------------------------------------------ */ /** * 计算指定日期之前的上一期表码:优先取抄表记录,其次取设备起始表码。 */ private BigDecimal resolvePreviousReading(EnergyMeterDevice device, LocalDateTime time) { EnergyMeterMeasurement previous = measurementMapper.selectPreviousPoint(device.getId(), EnergyMeterConstants.readingMetric(device.getMeterType()), time); if (previous != null) { return previous.getMetricValue(); } return device.getInitialReading(); } /** * 重算某条抄表记录对应的日用量(在插入/删除其它抄表记录后调用)。 */ private void recalcUsage(EnergyMeterDevice device, EnergyMeterMeasurement point) { BigDecimal previous = resolvePreviousReading(device, point.getMeasureTime()); if (previous == null) { deleteUsage(device, point.getMeasureTime().toLocalDate()); return; } upsertUsage(device, point.getMeasureTime().toLocalDate(), point.getMetricValue().subtract(previous)); } private void upsertUsage(EnergyMeterDevice device, LocalDate date, BigDecimal usage) { if (usage == null) { return; } EnergyMeterMeasurement record = new EnergyMeterMeasurement(); record.setTenantId(device.getTenantId()); record.setMeterId(device.getId()); record.setMetricCode(EnergyMeterConstants.usageMetric(device.getMeterType())); record.setGranularity(EnergyMeterConstants.GRANULARITY_DAY); record.setMeasureTime(date.atStartOfDay()); record.setMetricValue(usage); record.setUnit(EnergyMeterConstants.unit(device.getMeterType())); record.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL); record.setSourceRecordTime(date.atStartOfDay()); record.setCollectedAt(EnergyMeterSupport.now()); measurementMapper.upsertBatch(List.of(record)); } private void deleteUsage(EnergyMeterDevice device, LocalDate date) { measurementMapper.delete(new LambdaQueryWrapper() .eq(EnergyMeterMeasurement::getMeterId, device.getId()) .eq(EnergyMeterMeasurement::getMetricCode, EnergyMeterConstants.usageMetric(device.getMeterType())) .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_DAY) .eq(EnergyMeterMeasurement::getMeasureTime, date.atStartOfDay())); } private EnergyMeterMeasurement newManualPoint(EnergyMeterDevice device, LocalDateTime time, BigDecimal value, String remark) { EnergyMeterMeasurement point = new EnergyMeterMeasurement(); point.setTenantId(device.getTenantId()); point.setMeterId(device.getId()); point.setMetricCode(EnergyMeterConstants.readingMetric(device.getMeterType())); point.setGranularity(EnergyMeterConstants.GRANULARITY_POINT); point.setMeasureTime(time); point.setMetricValue(value); point.setUnit(EnergyMeterConstants.unit(device.getMeterType())); point.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL); point.setSourceRecordTime(time); point.setCollectedAt(EnergyMeterSupport.now()); point.setRemark(remark); return point; } private EnergyMeterDevice requireManualDevice(Long meterId) { EnergyMeterDevice device = getOwnedDevice(meterId); if (!EnergyMeterConstants.isManual(device.getSourceType())) { throw new ServiceException("该设备为第三方接入,请使用“查表”获取数据"); } return device; } private void enrichDevices(List devices) { if (devices == null || devices.isEmpty()) { return; } List ids = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList()); Map latestTask = new HashMap<>(); for (EnergyMeterSyncTask task : taskMapper.selectLatestByMeterIds(ids)) { latestTask.put(task.getMeterId(), task); } LocalDate yesterday = EnergyMeterSupport.today().minusDays(1); LocalDateTime start = yesterday.atStartOfDay(); LocalDateTime end = yesterday.plusDays(1).atStartOfDay(); Map usageMap = new HashMap<>(); Set types = devices.stream().map(EnergyMeterDevice::getMeterType) .filter(StringUtils::isNotBlank).collect(Collectors.toSet()); for (String type : types) { List typeIds = devices.stream() .filter(d -> type.equals(d.getMeterType())) .map(EnergyMeterDevice::getId).collect(Collectors.toList()); if (typeIds.isEmpty()) { continue; } for (MeterUsageRow row : measurementMapper.sumUsageByMeter(typeIds, EnergyMeterConstants.usageMetric(type), start, end)) { usageMap.put(row.getMeterId(), row.getTotal()); } } for (EnergyMeterDevice device : devices) { boolean manual = EnergyMeterConstants.isManual(device.getSourceType()); EnergyMeterSyncTask task = latestTask.get(device.getId()); if (task != null) { device.setLastTaskStatus(task.getStatus()); device.setLastTaskTime(task.getFinishedAt() != null ? task.getFinishedAt() : task.getStartedAt()); } BigDecimal dayUsage = usageMap.get(device.getId()); if (dayUsage != null) { device.setLastDayUsage(dayUsage); device.setLastDayUsageDate(yesterday); } else { // 昨天没有用量时(手工抄表按抄表期入账、第三方设备可能只有当天数据), // 退回最近一条用量及其业务日期,避免列表长期显示 "--" EnergyMeterMeasurement latestUsage = measurementMapper.selectLatestPoint(device.getId(), EnergyMeterConstants.usageMetric(device.getMeterType())); if (latestUsage != null) { device.setLastDayUsage(latestUsage.getMetricValue()); device.setLastDayUsageDate(latestUsage.getMeasureTime().toLocalDate()); } } EnergyMeterMeasurement latest = measurementMapper.selectLatestPoint(device.getId(), EnergyMeterConstants.readingMetric(device.getMeterType())); if (latest != null) { device.setLatestReading(latest.getMetricValue()); EnergyMeterMeasurement previous = measurementMapper.selectPreviousPoint(device.getId(), EnergyMeterConstants.readingMetric(device.getMeterType()), latest.getMeasureTime()); if (previous != null) { device.setPreviousReading(previous.getMetricValue()); } else if (device.getLastDayUsage() != null) { // 只有一条读数时(新装表第一天),用“最新表码 - 当期用量”反推上期表码 device.setPreviousReading(latest.getMetricValue().subtract(device.getLastDayUsage())); } } device.setCollectStateText(buildCollectState(device, task)); device.setMeterTypeName(EnergyMeterConstants.typeName(device.getMeterType())); device.setSourceTypeName(EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated()) ? "自动采集" : "手工抄表"); device.setUnit(EnergyMeterConstants.unit(device.getMeterType())); device.setDeletable(!Integer.valueOf(1).equals(device.getIsSeed()) && manual); } } private String buildCollectState(EnergyMeterDevice device, EnergyMeterSyncTask task) { if (EnergyMeterConstants.isSimulated(device.getSimulated())) { return "自动采集"; } if (EnergyMeterConstants.isManual(device.getSourceType())) { return device.getLastSuccessAt() == null ? "尚未抄表" : "手工抄表"; } if (task == null) { return "尚未采集"; } return switch (StringUtils.defaultString(task.getStatus())) { case "SUCCESS" -> "采集成功"; case "EMPTY" -> "确认无数据"; case "FAILED" -> "采集失败"; case "RETRY" -> "等待重试"; case "RUNNING" -> "采集中"; default -> "待执行"; }; } private EnergyMeterDevice findBinding(String meterType, String address) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(EnergyMeterDevice::getMeterType, meterType) .eq(EnergyMeterDevice::getMeterAddress, address); Long tenantId = EnergyMeterSupport.currentTenantId(); if (tenantId != null) { wrapper.eq(EnergyMeterDevice::getTenantId, tenantId); } List list = deviceMapper.selectList(wrapper); return list.isEmpty() ? null : list.get(0); } private LocalDate resolveStartDate(LocalDate requested, LocalDate existingStart, LocalDate yesterday) { LocalDate latestAllowed = yesterday; LocalDate defaultStart = LocalDate.of(latestAllowed.getYear() - 1, 1, 1); LocalDate start = requested != null ? requested : (existingStart != null ? existingStart : defaultStart); if (start.isAfter(latestAllowed)) { throw new ServiceException("补采起始日期不能晚于昨天"); } if (start.isBefore(LocalDate.of(2000, 1, 1))) { throw new ServiceException("补采起始日期过早,请确认供应商允许的历史范围"); } return start; } private String normalizeType(String meterType) { String type = EnergyMeterConstants.canonicalType(meterType); if (type == null) { throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或气表(GAS)"); } return type; } }