소스 검색

能耗报表代码提交口

fuyuchuan 1 주 전
부모
커밋
260358b651

+ 12 - 4
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceItemCode.java

@@ -22,12 +22,20 @@ public class EmsDeviceItemCode implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    @TableField("device_id")
-    private String deviceId;
+    // @TableField("device_id")
+    // private String deviceId;
 
     @TableField("item_code")
     private String itemCode;
 
-    @TableField("project_id")
-    private Long projectId;
+    // @TableField("project_id")
+    // private Long projectId;
+
+    // 能源类型
+    @TableField("energy_type")
+    private Integer energyType;
+
+    // 设备 UUID
+    @TableField("device_uuid")
+    private String deviceUuid;
 }

+ 687 - 56
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java

@@ -1,16 +1,18 @@
 package com.usky.ems.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
-import com.usky.ems.domain.DmpDevice;
-import com.usky.ems.domain.EmsDevice;
-import com.usky.ems.domain.EmsDeviceFunction;
-import com.usky.ems.mapper.DmpDeviceMapper;
-import com.usky.ems.mapper.EmsDeviceFunctionMapper;
-import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.demo.RemoteTsdbProxyService;
+import com.usky.demo.domain.HistorysInnerRequestVO;
+import com.usky.demo.domain.HistorysInnerResultVO;
+import com.usky.demo.domain.MetricVO;
+import com.usky.ems.domain.*;
+import com.usky.ems.mapper.*;
 import com.usky.ems.service.EmsReportService;
 import com.usky.ems.service.vo.*;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
@@ -22,6 +24,7 @@ import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -30,6 +33,7 @@ import java.util.stream.Collectors;
  * 统计报表服务实现(设备列表基于 leo 设备与属性点位,统计与导出为占位/预留)
  */
 @Service
+@Slf4j
 public class EmsReportServiceImpl implements EmsReportService {
 
     private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
@@ -40,6 +44,16 @@ public class EmsReportServiceImpl implements EmsReportService {
     private DmpDeviceMapper dmpDeviceMapper;
     @Autowired
     private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
+    @Autowired
+    private RemoteTsdbProxyService remoteTsdbProxyService;
+    @Autowired
+    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
+    @Autowired
+    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
+    @Autowired
+    private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
+    @Autowired
+    private BaseSpaceMapper baseSpaceMapper;
 
     private String energyTypeName(Long energyTypeId) {
         if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
@@ -177,8 +191,8 @@ public class EmsReportServiceImpl implements EmsReportService {
         // 生成列定义
         List<EnergyReportResponse.ColumnItem> columnList = generateEnergyReportColumns(request.getTimeType(), startTime, endTime);
 
-        // 生成数据列表
-        List<EnergyReportResponse.ValueItem> valueList = generateEnergyReportData(request, startTime, endTime);
+        // 生成数据列表(按设备分组的二维数组)
+        List<List<EnergyReportResponse.ValueItem>> valueList = generateEnergyReportData(request, startTime, endTime);
 
         EnergyReportResponse response = new EnergyReportResponse();
         response.setColumnList(columnList);
@@ -201,10 +215,10 @@ public class EmsReportServiceImpl implements EmsReportService {
         }
 
         // 校验时间参数
-        validateTimeParams(startTime, endTime, request.getDateType());
+        validateTimeParams(startTime, endTime, request.getTimeType());
 
         // 生成标题列
-        List<ItemReportResponse.TitleItem> titleList = generateItemReportTitles(request.getDateType(), startTime, endTime);
+        List<ItemReportResponse.TitleItem> titleList = generateItemReportTitles(request.getTimeType(), startTime, endTime);
 
         // 生成数据列表
         List<Map<String, Object>> dataList = generateItemReportData(request, startTime, endTime);
@@ -230,13 +244,13 @@ public class EmsReportServiceImpl implements EmsReportService {
         }
 
         // 校验时间参数
-        validateTimeParams(startTime, endTime, request.getDateType());
+        validateTimeParams(startTime, endTime, request.getTimeType());
 
         // 生成列定义
-        List<EnergyReportResponse.ColumnItem> columnList = generateSpaceReportColumns(request.getDateType(), startTime, endTime);
+        List<EnergyReportResponse.ColumnItem> columnList = generateSpaceReportColumns(request.getTimeType(), startTime, endTime);
 
-        // 生成数据列表
-        List<Map<String, Object>> valueList = generateSpaceReportData(request, startTime, endTime);
+        // 生成数据列表(按区域/空间分组的二维数组)
+        List<List<Map<String, Object>>> valueList = generateSpaceReportData(request, startTime, endTime);
 
         // 计算总计
         Number total = calculateSpaceReportTotal(valueList);
@@ -273,35 +287,125 @@ public class EmsReportServiceImpl implements EmsReportService {
     }
 
     /**
-     * 生成能源报表数据
+     * 生成能源报表数据(按设备分组的二维数组)
+     * 外层 List 代表不同的设备
+     * 内层 List 包含该设备下所有请求的功能点(属性)数据
      */
-    private List<EnergyReportResponse.ValueItem> generateEnergyReportData(EnergyReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
-        List<EnergyReportResponse.ValueItem> valueList = new ArrayList<>();
+    private List<List<EnergyReportResponse.ValueItem>> generateEnergyReportData(EnergyReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
+        List<List<EnergyReportResponse.ValueItem>> groupedValueList = new ArrayList<>();
+
+        // 获取设备uuid列表
+        List<Integer> deviceIds = request.getDeviceList().stream()
+                .filter(device -> device != null && device.getId() != null)
+                .map(EnergyReportRequest.DeviceItem::getId)
+                .collect(Collectors.toList());
+        List<String> deviceUuids = getUuidList(deviceIds);
+
+        // 查询tsdb历史数据
+        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
+        historyRequestVO.setDeviceuuid(deviceUuids);
+        historyRequestVO.setStartTime(request.getStartTime());
+        historyRequestVO.setEndTime(request.getEndTime());
+        historyRequestVO.setMetrics(request.getFuncList() != null && !request.getFuncList().isEmpty()
+                ? request.getFuncList().stream().map(EnergyReportRequest.FuncItem::getIdentifier).collect(Collectors.toList())
+                : new ArrayList<>());
+        log.info("历史数据接口请求:{}", historyRequestVO);
+        ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
+
+        // 判断历史数据是否为空
+        boolean hasData = !isHistoryDataEmpty(listHistoryData);
+        log.warn("历史数据为空,设备uuid:{}", deviceUuids);
+        log.info("历史数据接口返回:{}", listHistoryData);
+
+        // 如果有数据,解析TSDB返回的结果
+        Map<String, Map<String, List<Map<String, Object>>>> tsdbDataMap = new HashMap<>();
+        Map<String, String> deviceIdMap = new HashMap<>(); // deviceuuid -> device_id
+        if (hasData) {
+            tsdbDataMap = parseTsdbData(listHistoryData.getData());
+            deviceIdMap = extractDeviceIdMap(listHistoryData.getData());
+        }
+        log.info("设备id映射:{}", deviceIdMap);
+        log.info("tsdb数据:{}", tsdbDataMap);
 
         for (EnergyReportRequest.DeviceItem device : request.getDeviceList()) {
             if (device == null || StringUtils.isEmpty(device.getId())) {
                 continue;
             }
 
-            // 查询该设备的能耗数据
-            Map<String, String> timeData = queryDeviceEnergyData(device.getId(), startTime, endTime, request.getTimeType());
+            // 为当前设备创建一个子列表
+            List<EnergyReportResponse.ValueItem> deviceValueList = new ArrayList<>();
+
+            // 获取设备UUID用于匹配TSDB数据
+            DmpDevice dmpDevice = dmpDeviceMapper.selectById(device.getId());
+            String deviceUuid = dmpDevice != null ? dmpDevice.getDeviceUuid() : null;
+
+            // 如果没有功能点列表,为设备创建一个默认记录
+            if (request.getFuncList() == null || request.getFuncList().isEmpty()) {
+                Map<String, String> timeData;
+                String total;
+
+                if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                    // 从TSDB数据中获取
+                    timeData = buildTimeDataFromTsdb(tsdbDataMap.get(deviceUuid), request.getTimeType(), startTime, endTime);
+                    total = calculateTotal(timeData);
+                } else {
+                    // 生成空数据结构
+                    timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
+                    total = "-";
+                }
 
-            // 计算合计
-            String total = calculateTotal(timeData);
+                EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
+                item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
+                item.setDeviceName(device.getName());
+                item.setCommAddress(device.getCommAddress());
+                item.setIdentifier("-");
+                item.setTotal(total);
+                item.setTimeData(timeData);
 
-            EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
-            item.setDeviceId(device.getId());
-            item.setDeviceName(device.getName());
-            item.setCommAddress(device.getCommAddress());
-            item.setIdentifier(request.getFuncList() != null && !request.getFuncList().isEmpty()
-                    ? request.getFuncList().get(0).getIdentifierName() : "-");
-            item.setTotal(total);
-            item.setTimeData(timeData);
+                deviceValueList.add(item);
+            } else {
+                // 为每个功能点创建一条记录,添加到当前设备的子列表中
+                for (EnergyReportRequest.FuncItem func : request.getFuncList()) {
+                    if (func == null || StringUtils.isEmpty(func.getIdentifier())) {
+                        continue;
+                    }
+
+                    Map<String, String> timeData;
+                    String total;
+
+                    // 如果有历史数据,从真实数据中查询;否则使用空值
+                    if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                        // 从TSDB数据中获取指定功能点的数据
+                        timeData = buildTimeDataFromTsdbForMetric(
+                                tsdbDataMap.get(deviceUuid),
+                                func.getIdentifier(),
+                                request.getTimeType(),
+                                startTime,
+                                endTime);
+                        total = calculateTotal(timeData);
+                    } else {
+                        // 生成空数据结构
+                        timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
+                        total = "-";
+                    }
+
+                    EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
+                    item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
+                    item.setDeviceName(device.getName());
+                    item.setCommAddress(device.getCommAddress());
+                    item.setIdentifier(func.getIdentifierName() != null ? func.getIdentifierName() : func.getIdentifier());
+                    item.setTotal(total);
+                    item.setTimeData(timeData);
+
+                    deviceValueList.add(item);
+                }
+            }
 
-            valueList.add(item);
+            // 将当前设备的所有功能点数据添加到外层列表
+            groupedValueList.add(deviceValueList);
         }
 
-        return valueList;
+        return groupedValueList;
     }
 
     /**
@@ -321,6 +425,39 @@ public class EmsReportServiceImpl implements EmsReportService {
         return data;
     }
 
+    /**
+     * 生成空的时间数据结构(用于无数据时填充)
+     */
+    private Map<String, String> generateEmptyTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        Map<String, String> data = new LinkedHashMap<>();
+
+        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, "-"); // 模拟无数据
+        }
+
+        return data;
+    }
+
     /**
      * 计算合计值
      */
@@ -345,6 +482,187 @@ public class EmsReportServiceImpl implements EmsReportService {
         return hasData ? sum.toString() : "-";
     }
 
+    // ==================== TSDB数据解析方法 ====================
+
+    /**
+     * 解析TSDB返回的数据
+     * 返回结构:Map<deviceuuid, Map<metric, List<metricItems>>>
+     */
+    private Map<String, Map<String, List<Map<String, Object>>>> parseTsdbData(List<HistorysInnerResultVO> tsdbResultList) {
+        Map<String, Map<String, List<Map<String, Object>>>> dataMap = new HashMap<>();
+
+        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
+            return dataMap;
+        }
+
+        for (HistorysInnerResultVO result : tsdbResultList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+
+            String deviceUuid = result.getDeviceuuid();
+            Map<String, List<Map<String, Object>>> metricMap = new HashMap<>();
+
+            if (result.getMetrics() != null && !result.getMetrics().isEmpty()) {
+                for (MetricVO metricVO : result.getMetrics()) {
+                    if (metricVO != null && StringUtils.hasText(metricVO.getMetric())) {
+                        metricMap.put(metricVO.getMetric(), metricVO.getMetricItems());
+                    }
+                }
+            }
+
+            dataMap.put(deviceUuid, metricMap);
+        }
+
+        return dataMap;
+    }
+
+    /**
+     * 从TSDB返回结果中提取device_id映射
+     * 返回结构:Map<deviceuuid, device_id>
+     */
+    private Map<String, String> extractDeviceIdMap(List<HistorysInnerResultVO> tsdbResultList) {
+        Map<String, String> deviceIdMap = new HashMap<>();
+
+        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
+            return deviceIdMap;
+        }
+
+        for (HistorysInnerResultVO result : tsdbResultList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+
+            String deviceUuid = result.getDeviceuuid();
+            // 从tags中获取device_id
+            if (result.getTags() != null && result.getTags().containsKey("device_id")) {
+                String deviceId = (String) result.getTags().get("device_id");
+                deviceIdMap.put(deviceUuid, deviceId);
+            }
+        }
+
+        return deviceIdMap;
+    }
+
+    /**
+     * 从TSDB数据中构建时间数据(所有功能点)
+     */
+    private Map<String, String> buildTimeDataFromTsdb(Map<String, List<Map<String, Object>>> metricDataMap,
+                                                      String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        Map<String, String> timeData = new LinkedHashMap<>();
+
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
+        for (String label : timeLabels) {
+            String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
+
+            // 如果有数据,取第一个功能点的数据作为示例
+            if (!metricDataMap.isEmpty()) {
+                // 获取第一个功能点的数据
+                List<Map<String, Object>> firstMetricItems = metricDataMap.values().iterator().next();
+                String value = findValueByTimestamp(firstMetricItems, label, timeType, startTime, endTime);
+                timeData.put(key, value);
+            } else {
+                timeData.put(key, "-");
+            }
+        }
+
+        return timeData;
+    }
+
+    /**
+     * 从TSDB数据中构建指定功能点的时间数据
+     */
+    private Map<String, String> buildTimeDataFromTsdbForMetric(Map<String, List<Map<String, Object>>> metricDataMap,
+                                                               String metric,
+                                                               String timeType,
+                                                               LocalDateTime startTime,
+                                                               LocalDateTime endTime) {
+        Map<String, String> timeData = new LinkedHashMap<>();
+
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
+
+        // 获取指定功能点的数据
+        List<Map<String, Object>> metricItems = metricDataMap.get(metric);
+
+        for (String label : timeLabels) {
+            String key = "_" + label.replace("时", "").replace("日", "").replace("月", "");
+
+            if (metricItems != null && !metricItems.isEmpty()) {
+                String value = findValueByTimestamp(metricItems, label, timeType, startTime, endTime);
+                timeData.put(key, value);
+            } else {
+                timeData.put(key, "-");
+            }
+        }
+
+        return timeData;
+    }
+
+    /**
+     * 根据时间标签查找对应的值
+     * 按小时查询时,将小时范围划分为时间段,匹配对应时间点的数据
+     */
+    private String findValueByTimestamp(List<Map<String, Object>> metricItems,
+                                        String timeLabel,
+                                        String timeType,
+                                        LocalDateTime startTime,
+                                        LocalDateTime endTime) {
+        if (metricItems == null || metricItems.isEmpty()) {
+            return "-";
+        }
+
+        // 解析时间标签,找到对应的时间点
+        // 例如:"1时" -> 需要从数据中找到该小时范围内的第一个数据点
+        try {
+            int hour = Integer.parseInt(timeLabel.replace("时", ""));
+            LocalDateTime targetTime = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
+            LocalDateTime nextTime = targetTime.plusHours(1);
+
+            // 查找该时间范围内的数据
+            for (Map<String, Object> item : metricItems) {
+                if (item.containsKey("timestamp") && item.containsKey("value")) {
+                    String timestampStr = (String) item.get("timestamp");
+                    if (StringUtils.hasText(timestampStr)) {
+                        LocalDateTime itemTime = parseDateTime(timestampStr);
+                        if (itemTime != null && !itemTime.isBefore(targetTime) && itemTime.isBefore(nextTime)) {
+                            // 找到匹配的数据,进行单位转换和精度处理
+                            String valueStr = (String) item.get("value");
+                            if (StringUtils.hasText(valueStr)) {
+                                try {
+                                    BigDecimal value = new BigDecimal(valueStr);
+                                    // 除以10000后保留两位小数
+                                    value = value.divide(new BigDecimal("10000")).setScale(2, java.math.RoundingMode.HALF_UP);
+                                    return value.toString();
+                                } catch (NumberFormatException e) {
+                                    return "-";
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            // 如果解析失败,返回第一个数据点的值
+            if (!metricItems.isEmpty()) {
+                Map<String, Object> firstItem = metricItems.get(0);
+                if (firstItem.containsKey("value")) {
+                    String valueStr = (String) firstItem.get("value");
+                    if (StringUtils.hasText(valueStr)) {
+                        try {
+                            BigDecimal value = new BigDecimal(valueStr);
+                            value = value.divide(new BigDecimal("10000")).setScale(2, java.math.RoundingMode.HALF_UP);
+                            return value.toString();
+                        } catch (NumberFormatException ex) {
+                            return "-";
+                        }
+                    }
+                }
+            }
+        }
+
+        return "-";
+    }
+
     // ==================== 分项报表私有方法 ====================
 
     /**
@@ -384,7 +702,7 @@ public class EmsReportServiceImpl implements EmsReportService {
 
             // 查询分项能耗数据(模拟)
             Map<String, Number> timeData = queryItemEnergyData(itemCode.getCode(), request.getSpaceId(),
-                    startTime, endTime, request.getDateType());
+                    startTime, endTime, request.getTimeType());
 
             // 计算合计
             Number total = calculateItemTotal(timeData);
@@ -457,16 +775,24 @@ public class EmsReportServiceImpl implements EmsReportService {
     }
 
     /**
-     * 生成区域报表数据
+     * 生成区域报表数据(按区域/空间分组的二维数组)
+     * 外层 List 代表不同的区域
+     * 内层 List 包含该区域下所有设备的详细数据记录
      */
-    private List<Map<String, Object>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
-        List<Map<String, Object>> valueList = new ArrayList<>();
+    private List<List<Map<String, Object>>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
+        List<List<Map<String, Object>>> groupedValueList = new ArrayList<>();
 
         for (SpaceReportRequest.SpaceItem space : request.getSpaces()) {
             if (space == null || space.getId() == null) {
                 continue;
             }
 
+            // 查询区域分项能耗数据
+            Map<String, Number> timeData = querySpaceItemEnergyData(request, startTime, endTime, request.getTimeType());
+
+            // 为当前区域创建一个子列表
+            List<Map<String, Object>> spaceValueList = new ArrayList<>();
+
             for (SpaceReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
                 if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
                     continue;
@@ -476,9 +802,6 @@ public class EmsReportServiceImpl implements EmsReportService {
                 row.put("spaceName", space.getName());
                 row.put("itemName", itemCode.getName());
 
-                // 查询区域分项能耗数据(模拟)
-                Map<String, Number> timeData = querySpaceItemEnergyData(space.getId(), itemCode.getCode(),
-                        startTime, endTime, request.getDateType());
 
                 // 计算合计
                 Number total = calculateItemTotal(timeData);
@@ -491,42 +814,329 @@ public class EmsReportServiceImpl implements EmsReportService {
                     index++;
                 }
 
-                valueList.add(row);
+                spaceValueList.add(row);
             }
+
+            // 将当前区域的所有数据添加到外层列表
+            groupedValueList.add(spaceValueList);
         }
 
-        return valueList;
+        return groupedValueList;
     }
 
     /**
-     * 查询区域分项能耗数据(模拟实现)
+     * 查询区域分项能耗数据
      */
-    private Map<String, Number> querySpaceItemEnergyData(Long spaceId, String itemCode,
-                                                         LocalDateTime startTime, LocalDateTime endTime, String dateType) {
+    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));
+        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");
+        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
+        historyRequestVO.setDeviceuuid(getDeviceUuids);
+        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<>();
+
+            for (HistorysInnerResultVO result : listHistoryData.getData()) {
+                String deviceUuid = result.getDeviceuuid();
+                Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());
+
+                for (MetricVO metric : result.getMetrics()) {
+                    String metricName = metric.getMetric();
+                    TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());
+
+                    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);
+                        }
+                    }
+                }
+            }
 
-        // TODO: 实际应从数据库查询真实数据
-        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
-        for (String label : timeLabels) {
-            data.put(label, 0); // 模拟数据为0
+            // 2. 按itemCode分组计算用量
+            Map<String, Map<String, Double>> itemCodeTimeUsageMap = new HashMap<>(); // itemCode -> 时间段 -> 用量
+            Map<String, Double> itemCodeTotalUsageMap = new HashMap<>(); // itemCode -> 总用量
+
+            // 初始化所有itemCode的总用量
+            for (String itemCode : itemCodeByDeviceUuid.keySet()) {
+                itemCodeTotalUsageMap.put(itemCode, 0.0);
+                itemCodeTimeUsageMap.put(itemCode, new HashMap<>());
+            }
+
+            // 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);
+                }
+            }
+
+            // 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);
+                }
+
+                // 遍历该itemCode下的所有设备
+                for (String deviceUuid : deviceUuids) {
+                    if (!deviceMetricTimeValueMap.containsKey(deviceUuid)) {
+                        continue;
+                    }
+
+                    Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
+                    if (!metricTimeValueMap.containsKey("totalactiveenergyf")) {
+                        continue;
+                    }
+
+                    TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
+                    if (timeValueMap.isEmpty()) {
+                        continue;
+                    }
+
+                    // 按时间段计算用量差值
+                    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());
+                            }
+                        }
+
+                        if (segmentData.size() < 2) {
+                            continue; // 数据点不足,无法计算差值
+                        }
+
+                        // 找到时间段内的第一个和最后一个有效数据点
+                        LocalDateTime firstTimestampInSegment = null;
+                        LocalDateTime lastTimestampInSegment = 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;
+                                }
+                            }
+                        }
+
+                        // 如果找不到有效的时间点,使用整个时间段的首尾数据点
+                        if (firstTimestampInSegment == null) {
+                            firstTimestampInSegment = segmentData.ceilingKey(segmentStart.minusHours(1));
+                        }
+                        if (lastTimestampInSegment == null) {
+                            lastTimestampInSegment = segmentData.floorKey(segmentEnd.plusHours(1));
+                        }
+
+                        if (firstTimestampInSegment == null || lastTimestampInSegment == null) {
+                            continue;
+                        }
+
+                        Double firstValue = segmentData.get(firstTimestampInSegment);
+                        Double lastValue = segmentData.get(lastTimestampInSegment);
+                        Double usage = lastValue - firstValue;
+
+                        // 处理计数器重置的情况(用量为负数)
+                        if (usage < 0) {
+                            // 假设设备重置,取最后的值作为用量
+                            usage = lastValue;
+                        }
+
+                        // 累加到当前itemCode的用量
+                        timeUsageMap.put(timeLabel, timeUsageMap.get(timeLabel) + usage);
+                    }
+                }
+            }
+
+            // 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);
+            }
+
+            // 7. 将结果放入返回的data中
+            // 这里假设我们返回第一个itemCode的数据,或者根据业务需求调整
+            if (!itemCodeTimeUsageMap.isEmpty()) {
+                String firstItemCode = itemCodeTimeUsageMap.keySet().iterator().next();
+                Map<String, Double> timeUsageMap = itemCodeTimeUsageMap.get(firstItemCode);
+
+                // 按时间标签顺序放入结果
+                for (String timeLabel : timeLabels) {
+                    if (timeUsageMap.containsKey(timeLabel)) {
+                        data.put(timeLabel, timeUsageMap.get(timeLabel));
+                    } else {
+                        data.put(timeLabel, 0);
+                    }
+                }
+
+                // 添加总量
+                data.put("total", itemCodeTotalUsageMap.get(firstItemCode));
+            }
+        } else {
+            log.warn("历史数据为空,设备uuid:{}", getDeviceUuids);
+            // 无数据时初始化所有时间标签为0
+            List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
+            for (String label : timeLabels) {
+                data.put(label, 0);
+            }
+            data.put("total", 0);
         }
 
         return data;
     }
 
     /**
-     * 计算区域报表总计
+     * 查询所有区域id(包含子区域id)
+     */
+    private List<Long> spaceIds(List<Long> spaceId) {
+        return baseSpaceMapper.selectList(
+                new LambdaQueryWrapper<BaseSpace>()
+                        .in(BaseSpace::getId, spaceId)
+                        .or().in(BaseSpace::getParentId, spaceId)
+        ).stream().map(BaseSpace::getId).collect(Collectors.toList());
+    }
+
+    /**
+     * 通过energy_type获取product_id
+     */
+    private Long getProductIdByEnergyType(Integer energyType) {
+        return emsProductEnergyTypeMapper.selectOne(
+                new LambdaQueryWrapper<EmsProductEnergyType>()
+                        .eq(EmsProductEnergyType::getEnergyType, energyType)
+        ).getProductId();
+    }
+
+    /**
+     * 通过device_uuid按item_code分组
+     */
+    private Map<String, List<String>> getItemCodeByDeviceUuid(List<String> deviceUuids) {
+        return emsDeviceItemCodeMapper.selectList(
+                new LambdaQueryWrapper<EmsDeviceItemCode>()
+                        .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
+        ).stream().collect(Collectors.groupingBy(
+                EmsDeviceItemCode::getItemCode,
+                Collectors.mapping(
+                        EmsDeviceItemCode::getDeviceUuid,
+                        Collectors.toList()
+                )
+        ));
+    }
+
+    /**
+     * 通过device_uuid获取energy_type
      */
-    private Number calculateSpaceReportTotal(List<Map<String, Object>> valueList) {
-        if (valueList == null || valueList.isEmpty()) {
+    private Integer getEnergyTypeByDeviceUuid(String deviceUuid) {
+        return emsDeviceItemCodeMapper.selectList(
+                new LambdaQueryWrapper<EmsDeviceItemCode>()
+                        .eq(EmsDeviceItemCode::getDeviceUuid, deviceUuid)
+        ).get(0).getEnergyType();
+    }
+
+    /**
+     * 根据网关id获取设备UUID列表
+     */
+    private List<DmpDevice> getDeviceUuidsByGatewayIds(List<String> gatewayIds) {
+        return new ArrayList<>(dmpDeviceMapper.selectList(
+                new LambdaQueryWrapper<DmpDevice>()
+                        .in(DmpDevice::getGatewayUuid, gatewayIds)));
+    }
+
+    /**
+     * 根据区域id获取网关UUID列表
+     */
+    private List<BaseSpaceGateway> getGatewayUuidsBySpaceIds(List<Long> spaceIds) {
+        return new ArrayList<>(baseSpaceGatewayMapper.selectList(
+                new LambdaQueryWrapper<BaseSpaceGateway>()
+                        .in(BaseSpaceGateway::getSpaceId, spaceIds)));
+    }
+
+    /**
+     * 计算区域报表总计(适配二维数组结构)
+     */
+    private Number calculateSpaceReportTotal(List<List<Map<String, Object>>> groupedValueList) {
+        if (groupedValueList == null || groupedValueList.isEmpty()) {
             return 0;
         }
 
         double sum = 0;
-        for (Map<String, Object> row : valueList) {
-            Object total = row.get("total");
-            if (total instanceof Number) {
-                sum += ((Number) total).doubleValue();
+        for (List<Map<String, Object>> group : groupedValueList) {
+            if (group == null || group.isEmpty()) {
+                continue;
+            }
+            for (Map<String, Object> row : group) {
+                Object total = row.get("total");
+                if (total instanceof Number) {
+                    sum += ((Number) total).doubleValue();
+                }
             }
         }
         return sum;
@@ -569,9 +1179,11 @@ public class EmsReportServiceImpl implements EmsReportService {
                     throw new BusinessException("按年查询时,起止时间必须是同一年");
                 }
                 break;
-            default:
-                // 默认不校验
+            case "customize":
+                // 自定义查询:不进行时间校验
                 break;
+            default:
+                throw new BusinessException("查询时间类型错误!请重试");
         }
     }
 
@@ -651,6 +1263,8 @@ public class EmsReportServiceImpl implements EmsReportService {
                     }
                 }
                 break;
+            case "custom":
+                break;
             default:
                 // 默认按日查询,返回小时数据
                 for (int i = 0; i < 24; i++) {
@@ -676,6 +1290,23 @@ public class EmsReportServiceImpl implements EmsReportService {
         }
     }
 
+    /**
+     * 获取设备UUID列表
+     */
+    private List<String> getUuidList(List<Integer> deviceIds) {
+        return deviceIds.stream().map(deviceId -> {
+            DmpDevice device = dmpDeviceMapper.selectById(deviceId);
+            return device != null ? device.getDeviceUuid() : null;
+        }).collect(Collectors.toList());
+    }
+
+    private boolean isHistoryDataEmpty(ApiResult<?> result) {
+        return result == null
+                || !result.isSuccess()
+                || result.getStatus() != ApiResult.ResultStatus.SUCCESS
+                || result.getData() == null;
+    }
+
     // ==================== 空响应创建方法 ====================
 
     private EnergyReportResponse createEmptyEnergyReport() {

+ 2 - 2
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportRequest.java

@@ -58,9 +58,9 @@ public class EnergyReportRequest {
         private String name;
 
         /**
-         * 设备ID
+         * 设备主键ID
          */
-        private String id;
+        private Integer id;
     }
 
     @Data

+ 4 - 2
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportResponse.java

@@ -16,9 +16,11 @@ public class EnergyReportResponse {
     private List<ColumnItem> columnList;
 
     /**
-     * 数据值列表
+     * 数据值列表(按设备分组的二维数组)
+     * 外层 List 代表不同的设备
+     * 内层 List 包含该设备下所有请求的功能点(属性)数据
      */
-    private List<ValueItem> valueList;
+    private List<List<ValueItem>> valueList;
 
     @Data
     public static class ColumnItem {

+ 1 - 1
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ItemReportRequest.java

@@ -33,7 +33,7 @@ public class ItemReportRequest {
     /**
      * 日期类型:day-按日,month-按月,year-按年
      */
-    private String dateType;
+    private String timeType;
 
     @Data
     public static class ItemCodeItem {

+ 1 - 1
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportRequest.java

@@ -33,7 +33,7 @@ public class SpaceReportRequest {
     /**
      * 日期类型:month-按月,year-按年
      */
-    private String dateType;
+    private String timeType;
 
     @Data
     public static class SpaceItem {

+ 4 - 2
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportResponse.java

@@ -17,9 +17,11 @@ public class SpaceReportResponse {
     private List<EnergyReportResponse.ColumnItem> columnList;
 
     /**
-     * 数据值列表
+     * 数据值列表(按区域/空间分组的二维数组)
+     * 外层 List 代表不同的区域
+     * 内层 List 包含该区域下所有设备的详细数据记录
      */
-    private List<Map<String, Object>> valueList;
+    private List<List<Map<String, Object>>> valueList;
 
     /**
      * 合计值