Quellcode durchsuchen

能耗接口代码提交

fuyuchuan vor 1 Woche
Ursprung
Commit
441e718cda

+ 430 - 299
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java

@@ -1,6 +1,7 @@
 package com.usky.ems.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
@@ -21,6 +22,7 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -54,6 +56,8 @@ public class EmsReportServiceImpl implements EmsReportService {
     private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
     @Autowired
     private BaseSpaceMapper baseSpaceMapper;
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     private String energyTypeName(Long energyTypeId) {
         if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
@@ -386,7 +390,7 @@ public class EmsReportServiceImpl implements EmsReportService {
                     } else {
                         // 生成空数据结构
                         timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
-                        total = "-";
+                        total = "0";
                     }
 
                     EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
@@ -408,23 +412,6 @@ public class EmsReportServiceImpl implements EmsReportService {
         return groupedValueList;
     }
 
-    /**
-     * 查询设备能耗数据(模拟实现,实际应从TDengine或能耗表查询)
-     */
-    private Map<String, String> queryDeviceEnergyData(String deviceId, LocalDateTime startTime, LocalDateTime endTime, String timeType) {
-        Map<String, String> data = new LinkedHashMap<>();
-
-        // TODO: 实际应从数据库查询真实数据
-        // 这里使用模拟数据
-        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
-        for (String label : timeLabels) {
-            String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
-            data.put(key, "-"); // 模拟无数据
-        }
-
-        return data;
-    }
-
     /**
      * 生成空的时间数据结构(用于无数据时填充)
      */
@@ -434,25 +421,7 @@ public class EmsReportServiceImpl implements EmsReportService {
         List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
         for (String label : timeLabels) {
             String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
-            data.put(key, "-"); // 空值填充
-        }
-
-        return data;
-    }
-
-    /**
-     * 查询设备指定功能点的能耗数据(模拟实现,实际应从TDengine或能耗表查询)
-     */
-    private Map<String, String> queryDeviceEnergyDataWithFunc(String deviceId, String identifier,
-                                                              LocalDateTime startTime, LocalDateTime endTime, String timeType) {
-        Map<String, String> data = new LinkedHashMap<>();
-
-        // TODO: 实际应从数据库查询真实数据,并根据identifier区分不同功能点
-        // 这里使用模拟数据
-        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
-        for (String label : timeLabels) {
-            String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
-            data.put(key, "-"); // 模拟无数据
+            data.put(key, "0"); // 空值填充
         }
 
         return data;
@@ -463,7 +432,7 @@ public class EmsReportServiceImpl implements EmsReportService {
      */
     private String calculateTotal(Map<String, String> timeData) {
         if (timeData == null || timeData.isEmpty()) {
-            return "-";
+            return "0";
         }
 
         BigDecimal sum = BigDecimal.ZERO;
@@ -744,12 +713,7 @@ public class EmsReportServiceImpl implements EmsReportService {
         if (timeData == null || timeData.isEmpty()) {
             return 0;
         }
-
-        double sum = 0;
-        for (Number value : timeData.values()) {
-            sum += value.doubleValue();
-        }
-        return sum;
+        return sumUsageValues(timeData.values());
     }
 
     // ==================== 区域报表私有方法 ====================
@@ -765,10 +729,10 @@ public class EmsReportServiceImpl implements EmsReportService {
         columns.add(createColumn("分项名称", "itemName", true));
         columns.add(createColumn("合计", "total", true));
 
-        // 动态时间列
+        // 动态时间列(prop 与前端约定:date/month 保留「时/日」后缀,year 去掉「月」)
         List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
         for (String label : timeLabels) {
-            columns.add(createColumn(label, "_" + label.replace("月", ""), false));
+            columns.add(createColumn(label, toSpaceReportColumnProp(label, dateType), false));
         }
 
         return columns;
@@ -781,16 +745,15 @@ public class EmsReportServiceImpl implements EmsReportService {
      */
     private List<List<Map<String, Object>>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
         List<List<Map<String, Object>>> groupedValueList = new ArrayList<>();
+        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
 
         for (SpaceReportRequest.SpaceItem space : request.getSpaces()) {
             if (space == null || space.getId() == null) {
                 continue;
             }
 
-            // 查询区域分项能耗数据
-            Map<String, Number> timeData = querySpaceItemEnergyData(request, startTime, endTime, request.getTimeType());
-
-            // 为当前区域创建一个子列表
+            Map<String, Map<String, Number>> itemCodeTimeData = querySpaceItemEnergyData(space, request, startTime, endTime);
             List<Map<String, Object>> spaceValueList = new ArrayList<>();
 
             for (SpaceReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
@@ -798,26 +761,25 @@ public class EmsReportServiceImpl implements EmsReportService {
                     continue;
                 }
 
+                Map<String, Number> timeData = itemCodeTimeData.getOrDefault(
+                        buildSpaceItemKey(itemCode),
+                        createEmptySpaceTimeData(timeType, startTime, endTime));
+
                 Map<String, Object> row = new LinkedHashMap<>();
                 row.put("spaceName", space.getName());
                 row.put("itemName", itemCode.getName());
 
-
-                // 计算合计
-                Number total = calculateItemTotal(timeData);
-                row.put("total", total);
-
-                // 添加时间列数据
-                int index = 1;
-                for (Number value : timeData.values()) {
-                    row.put("_" + index, value);
-                    index++;
+                List<Number> periodValues = new ArrayList<>();
+                for (String label : timeLabels) {
+                    Number value = timeData.getOrDefault(label, 0);
+                    Number roundedValue = toUsageDecimal(value);
+                    row.put(toSpaceReportValueKey(label), roundedValue.doubleValue());
+                    periodValues.add(roundedValue);
                 }
-
+                row.put("total", sumUsageValues(periodValues));
                 spaceValueList.add(row);
             }
 
-            // 将当前区域的所有数据添加到外层列表
             groupedValueList.add(spaceValueList);
         }
 
@@ -825,233 +787,451 @@ public class EmsReportServiceImpl implements EmsReportService {
     }
 
     /**
-     * 查询区域分项能耗数据
+     * 查询区域分项能耗数据,按分项(code+name)-> 时间标签 -> 用量 返回
      */
-    private Map<String, Number> querySpaceItemEnergyData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime, String dateType) {
-        Map<String, Number> data = new LinkedHashMap<>();
-        List<Long> spaceIds = request.getSpaces().stream().map(SpaceReportRequest.SpaceItem::getId).collect(Collectors.toList());
-        spaceIds = spaceIds(spaceIds);
-        List<String> getGatewayUuids = getGatewayUuidsBySpaceIds(spaceIds).stream().map(BaseSpaceGateway::getGatewayUuid).collect(Collectors.toList());
-        List<String> getDeviceUuids = getDeviceUuidsByGatewayIds(getGatewayUuids).stream().map(DmpDevice::getDeviceUuid).collect(Collectors.toList());
-        Integer energyType = getEnergyTypeByDeviceUuid(getDeviceUuids.get(0));
+    private Map<String, Map<String, Number>> querySpaceItemEnergyData(SpaceReportRequest.SpaceItem space,
+                                                                      SpaceReportRequest request,
+                                                                      LocalDateTime startTime,
+                                                                      LocalDateTime endTime) {
+        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
+        List<TimeSegmentBound> timeSegments = buildReportTimeSegments(timeType, startTime, endTime);
+
+        Map<String, Map<String, Number>> result = new LinkedHashMap<>();
+        List<SpaceReportRequest.ItemCodeItem> requestedItems = request.getItemCodes() == null
+                ? Collections.emptyList()
+                : request.getItemCodes().stream()
+                .filter(item -> item != null && StringUtils.hasText(item.getCode()))
+                .collect(Collectors.toList());
+
+        for (SpaceReportRequest.ItemCodeItem item : requestedItems) {
+            result.put(buildSpaceItemKey(item), createEmptySpaceTimeData(timeType, startTime, endTime));
+        }
+
+        if (space == null || space.getId() == null || requestedItems.isEmpty()) {
+            return result;
+        }
+
+        List<Long> spaceIds = spaceIds(Collections.singletonList(space.getId()));
+        List<String> gatewayUuids = getGatewayUuidsBySpaceIds(spaceIds).stream()
+                .map(BaseSpaceGateway::getGatewayUuid)
+                .collect(Collectors.toList());
+        List<String> spaceDeviceUuids = getDeviceUuidsByGatewayIds(gatewayUuids).stream()
+                .map(DmpDevice::getDeviceUuid)
+                .collect(Collectors.toList());
+
+        if (spaceDeviceUuids.isEmpty()) {
+            log.warn("区域下无设备,spaceId:{}", space.getId());
+            return result;
+        }
+
+        Integer energyType = getEnergyTypeByDeviceUuid(spaceDeviceUuids.get(0));
         Long productId = getProductIdByEnergyType(energyType);
         if (productId != 360L) {
             throw new BusinessException("暂不支持该设备类型");
         }
-        // 按code将uuid分组
-        Map<String, List<String>> itemCodeByDeviceUuid = getItemCodeByDeviceUuid(getDeviceUuids);
-        // TODO:当前模拟数据都是电表设备,获取总有功功率
-        List<String> metrics = new ArrayList<>();
-        metrics.add("totalactiveenergyf");
+
+        Map<String, List<String>> itemCodeToDeviceUuids = getItemCodeByDeviceUuid(spaceDeviceUuids);
+
+        List<String> metrics = Collections.singletonList("totalactiveenergyf");
         HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
-        historyRequestVO.setDeviceuuid(getDeviceUuids);
+        historyRequestVO.setDeviceuuid(spaceDeviceUuids);
         historyRequestVO.setStartTime(request.getStartTime());
         historyRequestVO.setEndTime(request.getEndTime());
         historyRequestVO.setMetrics(metrics);
         log.info("历史数据接口请求:{}", historyRequestVO);
         ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
-        // 判断历史数据是否为空
-        boolean hasData = !isHistoryDataEmpty(listHistoryData);
         log.info("历史数据接口返回:{}", listHistoryData);
 
-        // 如果有数据,解析TSDB返回的结果
-        if (hasData) {
-            // 1. 解析TSDB数据,构建设备UUID -> 指标 -> 时间点 -> 值的映射
-            Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap = new HashMap<>();
+        if (isHistoryDataEmpty(listHistoryData)) {
+            log.warn("历史数据为空,设备uuid:{}", spaceDeviceUuids);
+            return result;
+        }
 
-            for (HistorysInnerResultVO result : listHistoryData.getData()) {
-                String deviceUuid = result.getDeviceuuid();
-                Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());
+        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap =
+                parseDeviceMetricTimeValueMap(listHistoryData.getData());
 
-                for (MetricVO metric : result.getMetrics()) {
-                    String metricName = metric.getMetric();
-                    TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());
+        for (SpaceReportRequest.ItemCodeItem itemCodeItem : requestedItems) {
+            List<String> itemDeviceUuids = resolveSpaceItemDeviceUuids(spaceDeviceUuids, itemCodeToDeviceUuids, itemCodeItem);
+            if (itemDeviceUuids.isEmpty()) {
+                log.warn("分项未匹配到区域设备,spaceId:{},itemCode:{},itemName:{}",
+                        space.getId(), itemCodeItem.getCode(), itemCodeItem.getName());
+                continue;
+            }
 
-                    for (Map<String, Object> point : metric.getMetricItems()) {
-                        try {
-                            // 解析时间戳字符串为LocalDateTime
-                            String timestampStr = point.get("timestamp").toString();
-                            // 处理可能的时间格式,移除末尾的.0
-                            if (timestampStr.endsWith(".0")) {
-                                timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
-                            }
-                            LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
-                            Double value = Double.parseDouble(point.get("value").toString());
-                            timeValueMap.put(timestamp, value);
-                        } catch (Exception e) {
-                            log.error("解析数据失败,设备:{},指标:{},值:{}", deviceUuid, metricName, point.get("value"), e);
-                        }
-                    }
+            Map<String, Number> timeUsageMap = result.get(buildSpaceItemKey(itemCodeItem));
+            for (String deviceUuid : itemDeviceUuids) {
+                Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
+                if (metricTimeValueMap == null) {
+                    continue;
+                }
+                TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
+                if (timeValueMap == null || timeValueMap.isEmpty()) {
+                    continue;
+                }
+
+                for (TimeSegmentBound segment : timeSegments) {
+                    double usage = calculateSegmentUsage(timeValueMap, segment.getStartInclusive(), segment.getEndExclusive());
+                    timeUsageMap.put(segment.getLabel(), addUsage(timeUsageMap.get(segment.getLabel()), usage).doubleValue());
                 }
             }
+        }
 
-            // 2. 按itemCode分组计算用量
-            Map<String, Map<String, Double>> itemCodeTimeUsageMap = new HashMap<>(); // itemCode -> 时间段 -> 用量
-            Map<String, Double> itemCodeTotalUsageMap = new HashMap<>(); // itemCode -> 总用量
+        return result;
+    }
 
-            // 初始化所有itemCode的总用量
-            for (String itemCode : itemCodeByDeviceUuid.keySet()) {
-                itemCodeTotalUsageMap.put(itemCode, 0.0);
-                itemCodeTimeUsageMap.put(itemCode, new HashMap<>());
-            }
+    private String buildSpaceItemKey(SpaceReportRequest.ItemCodeItem item) {
+        return item.getCode() + "::" + (item.getName() != null ? item.getName() : "");
+    }
 
-            // 3. 生成时间标签分段
-            List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
-
-            // 4. 按dateType生成时间分段
-            Map<String, List<LocalDateTime>> timeSegments = new HashMap<>();
-
-            if ("hour".equals(dateType)) {
-                // 按小时分段
-                LocalDateTime current = startTime.withMinute(0).withSecond(0).withNano(0);
-                while (!current.isAfter(endTime)) {
-                    String hourLabel = current.getHour() + "时";
-                    LocalDateTime segmentStart = current;
-                    LocalDateTime segmentEnd = current.plusHours(1);
-                    timeSegments.put(hourLabel, Arrays.asList(segmentStart, segmentEnd));
-                    current = current.plusHours(1);
-                }
-            } else if ("day".equals(dateType)) {
-                // 按天分段
-                LocalDateTime current = startTime.with(LocalTime.MIN);
-                while (!current.isAfter(endTime)) {
-                    String dateLabel = current.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-                    LocalDateTime segmentStart = current.with(LocalTime.MIN);
-                    LocalDateTime segmentEnd = current.with(LocalTime.MAX);
-                    timeSegments.put(dateLabel, Arrays.asList(segmentStart, segmentEnd));
-                    current = current.plusDays(1);
-                }
+    /**
+     * 解析分项对应设备:优先按分项名称解析编码,再按 code 匹配,返回区域内全部关联设备
+     */
+    private List<String> resolveSpaceItemDeviceUuids(List<String> spaceDeviceUuids,
+                                                     Map<String, List<String>> itemCodeToDeviceUuids,
+                                                     SpaceReportRequest.ItemCodeItem itemCodeItem) {
+        Set<String> spaceDeviceSet = new HashSet<>(spaceDeviceUuids);
+        String requestCode = itemCodeItem.getCode().trim();
+
+        String resolvedCode = resolveEnergyItemDbCode(requestCode, itemCodeItem.getName());
+        if (StringUtils.hasText(resolvedCode)) {
+            List<String> matched = findDevicesByItemCode(resolvedCode, itemCodeToDeviceUuids, spaceDeviceSet);
+            if (!matched.isEmpty()) {
+                return matched;
             }
+        }
 
-            // 5. 计算每个itemCode在每个时间段的用量
-            for (Map.Entry<String, List<String>> entry : itemCodeByDeviceUuid.entrySet()) {
-                String itemCode = entry.getKey();
-                List<String> deviceUuids = entry.getValue();
-                Map<String, Double> timeUsageMap = itemCodeTimeUsageMap.get(itemCode);
-
-                // 初始化时间段用量
-                for (String timeLabel : timeSegments.keySet()) {
-                    timeUsageMap.putIfAbsent(timeLabel, 0.0);
-                }
+        List<String> matched = findDevicesByItemCode(requestCode, itemCodeToDeviceUuids, spaceDeviceSet);
+        if (!matched.isEmpty()) {
+            return matched;
+        }
 
-                // 遍历该itemCode下的所有设备
-                for (String deviceUuid : deviceUuids) {
-                    if (!deviceMetricTimeValueMap.containsKey(deviceUuid)) {
-                        continue;
-                    }
+        return Collections.emptyList();
+    }
 
-                    Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
-                    if (!metricTimeValueMap.containsKey("totalactiveenergyf")) {
-                        continue;
-                    }
+    private List<String> findDevicesByItemCode(String itemCode,
+                                               Map<String, List<String>> itemCodeToDeviceUuids,
+                                               Set<String> spaceDeviceSet) {
+        List<String> matched = filterDevicesInSpace(itemCodeToDeviceUuids.get(itemCode), spaceDeviceSet);
+        if (!matched.isEmpty()) {
+            return matched;
+        }
+        return filterDevicesInSpace(
+                emsDeviceItemCodeMapper.selectDeviceUuidsByItemCode(SecurityUtils.getTenantId(), itemCode),
+                spaceDeviceSet);
+    }
 
-                    TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
-                    if (timeValueMap.isEmpty()) {
-                        continue;
-                    }
+    private List<String> filterDevicesInSpace(List<String> deviceUuids, Set<String> spaceDeviceSet) {
+        if (deviceUuids == null || deviceUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return deviceUuids.stream()
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .filter(spaceDeviceSet::contains)
+                .distinct()
+                .collect(Collectors.toList());
+    }
 
-                    // 按时间段计算用量差值
-                    for (Map.Entry<String, List<LocalDateTime>> segmentEntry : timeSegments.entrySet()) {
-                        String timeLabel = segmentEntry.getKey();
-                        List<LocalDateTime> segment = segmentEntry.getValue();
-                        LocalDateTime segmentStart = segment.get(0);
-                        LocalDateTime segmentEnd = segment.get(1);
-
-                        // 获取时间段内的数据点(包含前后缓冲时间,确保能取到首尾数据)
-                        TreeMap<LocalDateTime, Double> segmentData = new TreeMap<>();
-                        for (Map.Entry<LocalDateTime, Double> entryData : timeValueMap.entrySet()) {
-                            LocalDateTime timestamp = entryData.getKey();
-                            // 考虑时间段前1小时和后1小时的数据,确保能取到边界数据
-                            if (!timestamp.isBefore(segmentStart.minusHours(1)) && !timestamp.isAfter(segmentEnd.plusHours(1))) {
-                                segmentData.put(timestamp, entryData.getValue());
-                            }
-                        }
+    /**
+     * 将请求中的 code/identifier/name 解析为 ems_energy_item_code 表中的 code
+     */
+    private String resolveEnergyItemDbCode(String requestCode, String itemName) {
+        if (StringUtils.hasText(itemName)) {
+            EmsEnergyItemCode byName = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getName, itemName)
+                            .last("LIMIT 1"));
+            if (byName != null) {
+                return byName.getCode();
+            }
+        }
 
-                        if (segmentData.size() < 2) {
-                            continue; // 数据点不足,无法计算差值
-                        }
+        if (StringUtils.hasText(requestCode)) {
+            EmsEnergyItemCode byCode = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getCode, requestCode)
+                            .last("LIMIT 1"));
+            if (byCode != null) {
+                return byCode.getCode();
+            }
 
-                        // 找到时间段内的第一个和最后一个有效数据点
-                        LocalDateTime firstTimestampInSegment = null;
-                        LocalDateTime lastTimestampInSegment = null;
+            EmsEnergyItemCode byIdentifier = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getIdentifier, requestCode)
+                            .last("LIMIT 1"));
+            if (byIdentifier != null) {
+                return byIdentifier.getCode();
+            }
+        }
+        return null;
+    }
 
-                        for (LocalDateTime timestamp : segmentData.keySet()) {
-                            if (!timestamp.isBefore(segmentStart)) {
-                                if (firstTimestampInSegment == null || timestamp.isBefore(firstTimestampInSegment)) {
-                                    firstTimestampInSegment = timestamp;
-                                }
-                            }
-                            if (!timestamp.isAfter(segmentEnd)) {
-                                if (lastTimestampInSegment == null || timestamp.isAfter(lastTimestampInSegment)) {
-                                    lastTimestampInSegment = timestamp;
-                                }
-                            }
-                        }
+    private TreeMap<LocalDateTime, Double> extractSegmentReadings(TreeMap<LocalDateTime, Double> source,
+                                                                LocalDateTime startInclusive,
+                                                                LocalDateTime endExclusive) {
+        TreeMap<LocalDateTime, Double> result = new TreeMap<>();
+        if (source == null || source.isEmpty() || !startInclusive.isBefore(endExclusive)) {
+            return result;
+        }
+        for (Map.Entry<LocalDateTime, Double> entry : source.entrySet()) {
+            LocalDateTime ts = entry.getKey();
+            if (!ts.isBefore(startInclusive) && ts.isBefore(endExclusive)) {
+                result.put(ts, entry.getValue());
+            }
+        }
+        return result;
+    }
 
-                        // 如果找不到有效的时间点,使用整个时间段的首尾数据点
-                        if (firstTimestampInSegment == null) {
-                            firstTimestampInSegment = segmentData.ceilingKey(segmentStart.minusHours(1));
-                        }
-                        if (lastTimestampInSegment == null) {
-                            lastTimestampInSegment = segmentData.floorKey(segmentEnd.plusHours(1));
-                        }
+    /**
+     * 解析 TSDB 历史数据为 设备 -> 指标 -> 时间 -> 值 结构
+     */
+    private Map<String, Map<String, TreeMap<LocalDateTime, Double>>> parseDeviceMetricTimeValueMap(
+            List<HistorysInnerResultVO> historyDataList) {
+        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap = new HashMap<>();
 
-                        if (firstTimestampInSegment == null || lastTimestampInSegment == null) {
-                            continue;
-                        }
+        if (historyDataList == null || historyDataList.isEmpty()) {
+            return deviceMetricTimeValueMap;
+        }
 
-                        Double firstValue = segmentData.get(firstTimestampInSegment);
-                        Double lastValue = segmentData.get(lastTimestampInSegment);
-                        Double usage = lastValue - firstValue;
+        for (HistorysInnerResultVO result : historyDataList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+            String deviceUuid = result.getDeviceuuid();
+            Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap =
+                    deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());
 
-                        // 处理计数器重置的情况(用量为负数)
-                        if (usage < 0) {
-                            // 假设设备重置,取最后的值作为用量
-                            usage = lastValue;
+            if (result.getMetrics() == null) {
+                continue;
+            }
+            for (MetricVO metric : result.getMetrics()) {
+                if (metric == null || !StringUtils.hasText(metric.getMetric()) || metric.getMetricItems() == null) {
+                    continue;
+                }
+                String metricName = metric.getMetric();
+                TreeMap<LocalDateTime, Double> timeValueMap =
+                        metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());
+
+                for (Map<String, Object> point : metric.getMetricItems()) {
+                    try {
+                        String timestampStr = point.get("timestamp").toString();
+                        if (timestampStr.endsWith(".0")) {
+                            timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
                         }
-
-                        // 累加到当前itemCode的用量
-                        timeUsageMap.put(timeLabel, timeUsageMap.get(timeLabel) + usage);
+                        LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DATE_TIME_FORMATTER);
+                        Double value = Double.parseDouble(point.get("value").toString());
+                        timeValueMap.put(timestamp, value);
+                    } catch (Exception e) {
+                        log.error("解析历史数据失败,设备:{},指标:{},值:{}",
+                                deviceUuid, metricName, point.get("value"), e);
                     }
                 }
             }
+        }
+        return deviceMetricTimeValueMap;
+    }
+
+    /**
+     * 时间段用量 = 该时间段内最后一个读数 - 第一个读数(至少 2 条读数)
+     */
+    private double calculateSegmentUsage(TreeMap<LocalDateTime, Double> timeValueMap,
+                                         LocalDateTime startInclusive,
+                                         LocalDateTime endExclusive) {
+        if (timeValueMap == null || timeValueMap.isEmpty()) {
+            return 0;
+        }
+
+        TreeMap<LocalDateTime, Double> segmentData = extractSegmentReadings(timeValueMap, startInclusive, endExclusive);
+        if (segmentData.size() < 2) {
+            return 0;
+        }
+
+        double firstValue = segmentData.firstEntry().getValue();
+        double lastValue = segmentData.lastEntry().getValue();
+        return formatUsageValue(calculateReadingDelta(firstValue, lastValue));
+    }
+
+    private double calculateReadingDelta(double firstValue, double lastValue) {
+        double usage = lastValue - firstValue;
+        if (usage < 0) {
+            return lastValue;
+        }
+        return usage;
+    }
+
+    private double formatUsageValue(double usage) {
+        return toUsageDecimal(usage).doubleValue();
+    }
+
+    private static final int USAGE_SCALE = 2;
+
+    private BigDecimal toUsageDecimal(Number value) {
+        if (value == null) {
+            return BigDecimal.ZERO.setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+        }
+        return BigDecimal.valueOf(value.doubleValue()).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal addUsage(Number a, Number b) {
+        return toUsageDecimal(a).add(toUsageDecimal(b)).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+    }
 
-            // 6. 计算总量(所有时间段的用量之和)
-            for (String itemCode : itemCodeByDeviceUuid.keySet()) {
-                Map<String, Double> timeUsageMap = itemCodeTimeUsageMap.get(itemCode);
-                Double totalUsage = timeUsageMap.values().stream().mapToDouble(Double::doubleValue).sum();
-                itemCodeTotalUsageMap.put(itemCode, totalUsage);
+    private double sumUsageValues(Iterable<? extends Number> values) {
+        BigDecimal sum = BigDecimal.ZERO;
+        if (values != null) {
+            for (Number value : values) {
+                sum = sum.add(toUsageDecimal(value));
             }
+        }
+        return sum.setScale(USAGE_SCALE, RoundingMode.HALF_UP).doubleValue();
+    }
 
-            // 7. 将结果放入返回的data中
-            // 这里假设我们返回第一个itemCode的数据,或者根据业务需求调整
-            if (!itemCodeTimeUsageMap.isEmpty()) {
-                String firstItemCode = itemCodeTimeUsageMap.keySet().iterator().next();
-                Map<String, Double> timeUsageMap = itemCodeTimeUsageMap.get(firstItemCode);
+    private Map<String, Number> createEmptySpaceTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        Map<String, Number> data = new LinkedHashMap<>();
+        for (String label : generateTimeLabels(timeType, startTime, endTime)) {
+            data.put(label, 0);
+        }
+        return data;
+    }
 
-                // 按时间标签顺序放入结果
-                for (String timeLabel : timeLabels) {
-                    if (timeUsageMap.containsKey(timeLabel)) {
-                        data.put(timeLabel, timeUsageMap.get(timeLabel));
-                    } else {
-                        data.put(timeLabel, 0);
-                    }
-                }
+    /**
+     * 报表时间分段(左闭右开),与 columnList 标签一一对应
+     */
+    private static class TimeSegmentBound {
+        private final String label;
+        private final LocalDateTime startInclusive;
+        private final LocalDateTime endExclusive;
+
+        TimeSegmentBound(String label, LocalDateTime startInclusive, LocalDateTime endExclusive) {
+            this.label = label;
+            this.startInclusive = startInclusive;
+            this.endExclusive = endExclusive;
+        }
+
+        String getLabel() {
+            return label;
+        }
+
+        LocalDateTime getStartInclusive() {
+            return startInclusive;
+        }
+
+        LocalDateTime getEndExclusive() {
+            return endExclusive;
+        }
+    }
+
+    private List<TimeSegmentBound> buildReportTimeSegments(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<TimeSegmentBound> segments = new ArrayList<>();
+        if (startTime == null || endTime == null || !startTime.isBefore(endTime)) {
+            return segments;
+        }
+
+        String type = timeType != null ? timeType.toLowerCase() : "date";
+        switch (type) {
+            case "date":
+                buildHourSegments(segments, startTime, endTime);
+                break;
+            case "month":
+                buildDaySegments(segments, startTime, endTime);
+                break;
+            case "year":
+                buildMonthSegments(segments, startTime, endTime);
+                break;
+            case "custom":
+                break;
+            default:
+                buildHourSegments(segments, startTime, endTime);
+                break;
+        }
+        return segments;
+    }
 
-                // 添加总量
-                data.put("total", itemCodeTotalUsageMap.get(firstItemCode));
+    /** date:按小时分段 [HH:00, HH+1:00),与传参起止小时一致 */
+    private void buildHourSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        int startHour = startTime.getHour();
+        int endHour = endTime.getHour();
+        for (int hour = startHour; hour <= endHour; hour++) {
+            String label = hour + "时";
+            LocalDateTime hourStart = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
+            LocalDateTime hourEndExclusive = hourStart.plusHours(1);
+            LocalDateTime segmentStart = hourStart.isBefore(startTime) ? startTime : hourStart;
+            LocalDateTime segmentEndExclusive = hourEndExclusive.isAfter(endTime) ? endTime : hourEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
+            } else {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentStart.plusNanos(1)));
             }
-        } else {
-            log.warn("历史数据为空,设备uuid:{}", getDeviceUuids);
-            // 无数据时初始化所有时间标签为0
-            List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
-            for (String label : timeLabels) {
-                data.put(label, 0);
+        }
+    }
+
+    /** month:按自然日分段 [00:00, 次日00:00) */
+    private void buildDaySegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        LocalDate current = startTime.toLocalDate();
+        LocalDate lastDate = resolveLastDateInRange(startTime, endTime);
+        while (!current.isAfter(lastDate)) {
+            String label = String.format("%02d日", current.getDayOfMonth());
+            LocalDateTime dayStart = current.atStartOfDay();
+            LocalDateTime dayEndExclusive = current.plusDays(1).atStartOfDay();
+            LocalDateTime segmentStart = dayStart.isBefore(startTime) ? startTime : dayStart;
+            LocalDateTime segmentEndExclusive = dayEndExclusive.isAfter(endTime) ? endTime : dayEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
             }
-            data.put("total", 0);
+            current = current.plusDays(1);
         }
+    }
 
-        return data;
+    /** year:按自然月分段 [月初00:00, 下月初00:00) */
+    private void buildMonthSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        int year = startTime.getYear();
+        int month = startTime.getMonthValue();
+        int endYear = endTime.getYear();
+        int endMonth = endTime.getMonthValue();
+
+        while (year < endYear || (year == endYear && month <= endMonth)) {
+            String label = month + "月";
+            LocalDateTime monthStart = LocalDateTime.of(year, month, 1, 0, 0, 0);
+            LocalDateTime monthEndExclusive = monthStart.plusMonths(1);
+            LocalDateTime segmentStart = monthStart.isBefore(startTime) ? startTime : monthStart;
+            LocalDateTime segmentEndExclusive = monthEndExclusive.isAfter(endTime) ? endTime : monthEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
+            }
+            month++;
+            if (month > 12) {
+                month = 1;
+                year++;
+            }
+        }
+    }
+
+    /**
+     * 结束时间为某日 00:00:00 时,表示不含该日,最后一天取前一日
+     */
+    private LocalDate resolveLastDateInRange(LocalDateTime startTime, LocalDateTime endTime) {
+        LocalDate endDate = endTime.toLocalDate();
+        if (endTime.toLocalTime().equals(LocalTime.MIDNIGHT) && endDate.isAfter(startTime.toLocalDate())) {
+            return endDate.minusDays(1);
+        }
+        return endDate;
+    }
+
+    private String toSpaceReportColumnProp(String label, String timeType) {
+        String type = timeType != null ? timeType.toLowerCase() : "date";
+        if ("year".equals(type)) {
+            return "_" + label.replace("月", "");
+        }
+        return "_" + label;
+    }
+
+    private String toSpaceReportValueKey(String label) {
+        String numPart = label.replace("时", "").replace("日", "").replace("月", "");
+        try {
+            return "_" + Integer.parseInt(numPart);
+        } catch (NumberFormatException e) {
+            return "_" + numPart;
+        }
     }
 
     /**
@@ -1127,7 +1307,7 @@ public class EmsReportServiceImpl implements EmsReportService {
             return 0;
         }
 
-        double sum = 0;
+        List<Number> rowTotals = new ArrayList<>();
         for (List<Map<String, Object>> group : groupedValueList) {
             if (group == null || group.isEmpty()) {
                 continue;
@@ -1135,11 +1315,11 @@ public class EmsReportServiceImpl implements EmsReportService {
             for (Map<String, Object> row : group) {
                 Object total = row.get("total");
                 if (total instanceof Number) {
-                    sum += ((Number) total).doubleValue();
+                    rowTotals.add((Number) total);
                 }
             }
         }
-        return sum;
+        return sumUsageValues(rowTotals);
     }
 
     // ==================== 通用工具方法 ====================
@@ -1179,7 +1359,7 @@ public class EmsReportServiceImpl implements EmsReportService {
                     throw new BusinessException("按年查询时,起止时间必须是同一年");
                 }
                 break;
-            case "customize":
+            case "custom":
                 // 自定义查询:不进行时间校验
                 break;
             default:
@@ -1219,61 +1399,12 @@ public class EmsReportServiceImpl implements EmsReportService {
      * - 按年(year)查询 → 返回月数据(1月-12月)
      */
     private List<String> generateTimeLabels(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
-        List<String> labels = new ArrayList<>();
-
         if (startTime == null || endTime == null) {
-            return labels;
+            return new ArrayList<>();
         }
-
-        String type = timeType != null ? timeType.toLowerCase() : "date";
-
-        switch (type) {
-            case "date":
-                // 按日查询:根据起止时间的小时来分段
-                int startHour = startTime.getHour();
-                int endHour = endTime.getHour();
-                for (int i = startHour; i <= endHour; i++) {
-                    labels.add(i + "时");
-                }
-                break;
-            case "month":
-                // 按月查询:返回日数据 01日, 02日, ..., 31日
-                LocalDate start = startTime.toLocalDate();
-                LocalDate end = endTime.toLocalDate();
-                int day = 1;
-                while (!start.isAfter(end)) {
-                    labels.add(String.format("%02d日", day));
-                    start = start.plusDays(1);
-                    day++;
-                }
-                break;
-            case "year":
-                // 按年查询:返回月数据 1月, 2月, ..., 12月
-                int startMonth = startTime.getMonthValue();
-                int endMonth = endTime.getMonthValue();
-                int year = startTime.getYear();
-                int endYear = endTime.getYear();
-
-                while (year < endYear || (year == endYear && startMonth <= endMonth)) {
-                    labels.add(startMonth + "月");
-                    startMonth++;
-                    if (startMonth > 12) {
-                        startMonth = 1;
-                        year++;
-                    }
-                }
-                break;
-            case "custom":
-                break;
-            default:
-                // 默认按日查询,返回小时数据
-                for (int i = 0; i < 24; i++) {
-                    labels.add(i + "时");
-                }
-                break;
-        }
-
-        return labels;
+        return buildReportTimeSegments(timeType, startTime, endTime).stream()
+                .map(TimeSegmentBound::getLabel)
+                .collect(Collectors.toList());
     }
 
     /**