瀏覽代碼

5、开发建筑能耗分析(按建筑排名)接口;
6、开发分项能耗占比接口;
7、优化service-tsdb服务对外能耗分项汇总API接口,增加设备uuid集合请求参数的刷选逻辑;

james 3 天之前
父節點
當前提交
680f147a28

+ 11 - 17
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsOverviewController.java

@@ -65,17 +65,15 @@ public class EmsOverviewController {
      * 分类能耗统计(按能源类型关联产品,调用 TSDB 分项汇总)
      * 参数说明:
      * - dateType:时间类型(1-日,2-月,3-年)
-     * - identifier:分项字段编码
-     * - energyType:能源类型(1电 2水 3冷 4热)
+     * - energyType:能源类型(1电 2水 3冷 4热;分项 identifier 从 ems_energy_item_code 根节点解析)
      * - projectId:项目 ID(可选,不传则取当前租户第一个项目;用于查询折算系数)
      */
     @GetMapping("/classification-energy")
     public ApiResult<Map<String, Object>> queryClassificationEnergy(
             @RequestParam Integer dateType,
-            @RequestParam String identifier,
             @RequestParam Integer energyType,
             @RequestParam(required = false) Long projectId) {
-        return ApiResult.success(emsOverviewService.queryClassificationEnergy(dateType, identifier, energyType, projectId));
+        return ApiResult.success(emsOverviewService.queryClassificationEnergy(dateType, energyType, projectId));
     }
 
     /**
@@ -88,24 +86,22 @@ public class EmsOverviewController {
     @GetMapping("/energy-trend")
     public ApiResult<List<Map<String, Object>>> queryEnergyTrend(
             @RequestParam Integer dateType,
-            @RequestParam String identifier,
             @RequestParam Integer energyType) {
-        return ApiResult.success(emsOverviewService.queryEnergyTrend(dateType, identifier, energyType));
+        return ApiResult.success(emsOverviewService.queryEnergyTrend(dateType, energyType));
     }
 
     /**
-     * 建筑能耗分析(模拟数据
+     * 建筑能耗分析(按建筑排名
      * 参数说明:
      * - dateType:时间类型(1-日,2-月,3-年)
-     * - itemCode:能耗条目编码
-     * - spaceId:空间ID(可选;下钻时与 base_space 子节点一致
+     * - itemCode:能耗分项字段编码
+     * - energyType:能源类型(1电 2水 3冷 4热
      */
     @GetMapping("/building-ranking")
     public ApiResult<List<Map<String, Object>>> queryBuildingRanking(
             @RequestParam Integer dateType,
-            @RequestParam String itemCode,
-            @RequestParam(required = false) Long spaceId) {
-        return ApiResult.success(emsOverviewService.queryBuildingRanking(dateType, itemCode, spaceId));
+            @RequestParam Integer energyType) {
+        return ApiResult.success(emsOverviewService.queryBuildingRanking(dateType, energyType));
     }
 
     /**
@@ -125,14 +121,12 @@ public class EmsOverviewController {
      * 分项能耗占比
      * 参数说明:
      * - dateType:时间类型(1-日,2-月,3-年)
-     * - itemCode:分项编码(作为父项,展开下级分项)
-     * - projectId:项目ID(可选,不传则取第一个项目)
+     * - energyType:能源类型(1电 2水 3冷 4热;根分项从 ems_energy_item_code 解析)
      */
     @GetMapping("/item-ratio")
     public ApiResult<List<Map<String, Object>>> queryItemRatio(
             @RequestParam Integer dateType,
-            @RequestParam String itemCode,
-            @RequestParam(required = false) Long projectId) {
-        return ApiResult.success(emsOverviewService.queryItemRatio(dateType, itemCode, projectId));
+            @RequestParam Integer energyType) {
+        return ApiResult.success(emsOverviewService.queryItemRatio(dateType, energyType));
     }
 }

+ 33 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceItemCode.java

@@ -0,0 +1,33 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 设备与能耗分项关联(ems_device_item_code)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_device_item_code")
+public class EmsDeviceItemCode implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("device_id")
+    private String deviceId;
+
+    @TableField("item_code")
+    private String itemCode;
+
+    @TableField("project_id")
+    private Long projectId;
+}

+ 19 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceItemCodeMapper.java

@@ -0,0 +1,19 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsDeviceItemCode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 设备能耗分项关联 Mapper(ems_device_item_code)
+ */
+public interface EmsDeviceItemCodeMapper extends CrudMapper<EmsDeviceItemCode> {
+
+    /**
+     * 按分项编码解析 TSDB 设备 UUID(ems_device_item_code → ems_device → dmp_device)
+     */
+    List<String> selectDeviceUuidsByItemCode(@Param("tenantId") Integer tenantId,
+                                             @Param("itemCode") String itemCode);
+}

+ 5 - 6
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java

@@ -36,19 +36,19 @@ public interface EmsOverviewService {
      * 分类能耗统计(按时间维度、能耗类型等)
      * 先按 Map 结构返回,后续可以再封装 VO。
      */
-    Map<String, Object> queryClassificationEnergy(Integer dateType, String identifier, Integer energyType, Long projectId);
+    Map<String, Object> queryClassificationEnergy(Integer dateType, Integer energyType, Long projectId);
 
     /**
      * 能耗用能趋势(按时间维度:日/月/年)
      * 按能源类型关联产品调用 TSDB 汇总,返回当前期与去年同期各时间粒度的用能数据。
      */
-    java.util.List<java.util.Map<String, Object>> queryEnergyTrend(Integer dateType, String identifier, Integer energyType);
+    java.util.List<java.util.Map<String, Object>> queryEnergyTrend(Integer dateType, Integer energyType);
 
     /**
      * 建筑能耗分析(建筑/楼层能耗排名)
      * 返回按能耗从高到低排序的建筑(或楼层)名称及其能耗值列表(当前为模拟数据实现)。
      */
-    java.util.List<java.util.Map<String, Object>> queryBuildingRanking(Integer dateType, String itemCode, Long spaceId);
+    java.util.List<java.util.Map<String, Object>> queryBuildingRanking(Integer dateType, Integer energyType);
 
     /**
      * 单位综合能耗、综合累计能耗、分类能耗占比(总览页顶部)
@@ -57,8 +57,7 @@ public interface EmsOverviewService {
     Map<String, Object> queryOverviewTop(Integer dateType, Long projectId);
 
     /**
-     * 分项能耗占比(按分项编码 itemCode 展开下级条目)
-     * 部分数据(分项定义)来自 leo.ems_energy_item_code,能耗数值按规则模拟。
+     * 分项能耗占比(按能源类型展开下级分项,设备范围来自 ems_device_item_code)
      */
-    java.util.List<java.util.Map<String, Object>> queryItemRatio(Integer dateType, String itemCode, Long projectId);
+    java.util.List<java.util.Map<String, Object>> queryItemRatio(Integer dateType, Integer energyType);
 }

+ 0 - 1
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java

@@ -13,7 +13,6 @@ import com.usky.ems.service.vo.EmsAttributePointVO;
 import com.usky.ems.service.vo.EmsEnergyTypeVO;
 import com.usky.ems.service.vo.EmsGatewayDeviceTreeNode;
 import com.usky.ems.service.vo.EmsModelSaveRequest;
-import com.usky.ems.service.vo.EmsStructureTreeNode;
 import com.usky.ems.service.vo.SimpleProductVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;

+ 238 - 111
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java

@@ -11,7 +11,9 @@ import com.usky.demo.domain.EnergyItemTrendPointVO;
 import com.usky.demo.domain.EnergyItemTrendQueryVO;
 import com.usky.demo.domain.EnergyItemTrendResultVO;
 import com.usky.ems.domain.*;
+import com.usky.ems.mapper.BaseSpaceGatewayMapper;
 import com.usky.ems.mapper.DmpProductMapper;
+import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
 import com.usky.ems.mapper.EmsDeviceMapper;
 import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
 import com.usky.ems.mapper.EmsProjectConversionFactorMapper;
@@ -20,6 +22,7 @@ import com.usky.ems.mapper.EmsProjectMapper;
 import com.usky.ems.service.EmsModelService;
 import com.usky.ems.service.EmsOverviewService;
 import com.usky.ems.service.BaseSpaceService;
+import com.usky.ems.service.DmpDeviceService;
 import com.usky.ems.service.vo.EmsEnergyTypeVO;
 import com.usky.ems.service.vo.EmsOverviewDeviceSystemStatVO;
 import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
@@ -43,7 +46,9 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.LinkedHashMap;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -78,6 +83,9 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     @Autowired
     private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
+    @Autowired
+    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
+
     @Autowired
     private EmsProjectMapper emsProjectMapper;
 
@@ -90,6 +98,12 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     @Autowired
     private DmpProductMapper dmpProductMapper;
 
+    @Autowired
+    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
+
+    @Autowired
+    private DmpDeviceService dmpDeviceService;
+
     @Autowired
     private RemoteTsdbProxyService remoteTsdbProxyService;
 
@@ -232,8 +246,8 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
      * 分类能耗统计:按能源类型关联产品汇总 TSDB 用量,并计算环比/同比及折标煤、碳排放。
      */
     @Override
-    public Map<String, Object> queryClassificationEnergy(Integer dateType, String identifier, Integer energyType, Long projectId) {
-        if (dateType == null || StringUtils.isBlank(identifier) || energyType == null) {
+    public Map<String, Object> queryClassificationEnergy(Integer dateType, Integer energyType, Long projectId) {
+        if (dateType == null || energyType == null) {
             return null;
         }
 
@@ -249,17 +263,23 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
                     coalFactor, co2Factor);
         }
 
+        String identifier = resolveRootIdentifierByEnergyType(energyType);
+        if (identifier == null) {
+            return buildClassificationResult(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
+                    coalFactor, co2Factor);
+        }
+
         LocalDateTime endTime = LocalDateTime.now();
         LocalDateTime startTime = getStartTime(dateType, endTime);
         if (startTime == null) {
             return null;
         }
 
-        BigDecimal data = sumEnergyByProductCodes(productCodeList, identifier.toLowerCase(), startTime, endTime);
+        BigDecimal data = sumEnergyByProductCodes(productCodeList, identifier, startTime, endTime);
 
         LocalDateTime pariPassuEndTime = endTime.minusYears(1);
         LocalDateTime pariPassuStartTime = getStartTime(dateType, pariPassuEndTime);
-        BigDecimal pariPassuData = sumEnergyByProductCodes(productCodeList, identifier.toLowerCase(), pariPassuStartTime, pariPassuEndTime);
+        BigDecimal pariPassuData = sumEnergyByProductCodes(productCodeList, identifier, pariPassuStartTime, pariPassuEndTime);
 
         LocalDateTime sequentialEndTime;
         if (dateType == 1) {
@@ -270,7 +290,7 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
             sequentialEndTime = endTime.minusYears(1);
         }
         LocalDateTime sequentialStartTime = getStartTime(dateType, sequentialEndTime);
-        BigDecimal sequentialData = sumEnergyByProductCodes(productCodeList, identifier.toLowerCase(), sequentialStartTime, sequentialEndTime);
+        BigDecimal sequentialData = sumEnergyByProductCodes(productCodeList, identifier, sequentialStartTime, sequentialEndTime);
 
         return buildClassificationResult(data, sequentialData, pariPassuData, coalFactor, co2Factor);
     }
@@ -322,9 +342,20 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
 
     private BigDecimal sumEnergyByProductCodes(List<String> productCodeList, String fieldIdentifier,
                                                LocalDateTime startTime, LocalDateTime endTime) {
+        return sumEnergyByProductCodes(productCodeList, fieldIdentifier, startTime, endTime, null);
+    }
+
+    private BigDecimal sumEnergyByProductCodes(List<String> productCodeList, String fieldIdentifier,
+                                               LocalDateTime startTime, LocalDateTime endTime,
+                                               List<String> deviceUuidList) {
         BigDecimal total = BigDecimal.ZERO;
         String start = formatTsdbTime(startTime);
         String end = formatTsdbTime(endTime);
+        List<String> effectiveDeviceUuids = deviceUuidList == null ? null : deviceUuidList.stream()
+                .filter(StringUtils::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
         for (String productCode : productCodeList) {
             if (StringUtils.isBlank(productCode)) {
                 continue;
@@ -334,6 +365,9 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
             queryVO.setSuperTable("super_" + productCode.trim());
             queryVO.setStartTime(start);
             queryVO.setEndTime(end);
+            if (effectiveDeviceUuids != null && !effectiveDeviceUuids.isEmpty()) {
+                queryVO.setDeviceUuid(effectiveDeviceUuids);
+            }
             EnergyItemSumResultVO apiResult = remoteTsdbProxyService.sumEnergyItem(queryVO);
             if (apiResult == null || apiResult.getSumDiff() == null) {
                 continue;
@@ -420,8 +454,8 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
      * 能耗用能趋势:按产品+时间范围查询 TSDB 得 time/value,再按原系统 getListResult 逻辑封装。
      */
     @Override
-    public List<Map<String, Object>> queryEnergyTrend(Integer dateType, String identifier, Integer energyType) {
-        if (dateType == null || StringUtils.isBlank(identifier) || energyType == null) {
+    public List<Map<String, Object>> queryEnergyTrend(Integer dateType, Integer energyType) {
+        if (dateType == null|| energyType == null) {
             return new ArrayList<>();
         }
 
@@ -431,6 +465,11 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
             return new ArrayList<>();
         }
 
+        String identifier = resolveRootIdentifierByEnergyType(energyType);
+        if (identifier == null) {
+            return new ArrayList<>();
+        }
+
         if (dateType == 1 || dateType == 2 || dateType == 3) {
             return buildEnergyTrendResult(productCodeList, identifier, dateType);
         }
@@ -590,6 +629,24 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
         return list;
     }
 
+    /**
+     * 按能源类型取根分项 identifier(parent_code=0)。
+     */
+    private String resolveRootIdentifierByEnergyType(Integer energyType) {
+        if (energyType == null) {
+            return null;
+        }
+        EmsEnergyItemCode rootItem = emsEnergyItemCodeMapper.selectOne(
+                Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                        .eq(EmsEnergyItemCode::getParentCode, "0")
+                        .eq(EmsEnergyItemCode::getEnergyType, energyType)
+                        .last("LIMIT 1"));
+        if (rootItem == null || StringUtils.isBlank(rootItem.getIdentifier())) {
+            return null;
+        }
+        return rootItem.getIdentifier().trim().toLowerCase();
+    }
+
     private EmsEnergyItemCode resolveEnergyItemCode(String identifier) {
         EmsEnergyItemCode itemCode = emsEnergyItemCodeMapper.selectOne(
                 Wrappers.lambdaQuery(EmsEnergyItemCode.class).eq(EmsEnergyItemCode::getCode, identifier));
@@ -701,84 +758,136 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     }
 
     /**
-     * 建筑能耗分析(模拟数据版本)
-     * 参考原系统 queryBuildingRanking:按指定时间维度与能耗条目,对建筑/楼层进行能耗排名。
-     * 当前实现:空间信息从 base_space 真实查询(建筑 type=3;指定 spaceId 时取子节点),仅能耗值使用规则模拟。
+     * 建筑能耗分析:从 base_space 取 type=3 建筑,经 base_space_gateway → dmp_device 定位设备,
+     * 再按能源类型关联产品调用 TSDB 分项汇总。
      */
     @Override
-    public List<Map<String, Object>> queryBuildingRanking(Integer dateType, String itemCode, Long spaceId) {
-        List<Map<String, Object>> resultList = new ArrayList<>();
-
-        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
-            return resultList;
+    public List<Map<String, Object>> queryBuildingRanking(Integer dateType, Integer energyType) {
+        if (dateType == null || energyType == null) {
+            return new ArrayList<>();
         }
 
         BaseSpace project = baseSpaceService.getOne(
-                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BaseSpace>()
-                        .eq(BaseSpace::getRootId, 0)
+                new LambdaQueryWrapper<BaseSpace>()
+                        .eq(BaseSpace::getParentId, 0)
                         .eq(BaseSpace::getTenantId, SecurityUtils.getTenantId()));
         if (project == null || project.getId() == null) {
-            return resultList;
-        }
-        Long rootSpaceId = project.getId();
-
-        // 2. 查询空间列表:
-        // - 未指定 spaceId:取根空间下 type=3 的建筑(base_space)
-        // - 指定 spaceId:取该空间下直接子节点(新表无独立楼层类型时,用子空间展示排名)
-        List<BaseSpace> spaceList;
-        if (spaceId == null) {
-            spaceList = baseSpaceService.list(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BaseSpace>()
-                            .eq(BaseSpace::getRootId, rootSpaceId)
-                            .eq(BaseSpace::getType, 3)
-            );
-        } else {
-            spaceList = baseSpaceService.list(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BaseSpace>()
-                            .eq(BaseSpace::getParentId, spaceId)
-            );
+            return new ArrayList<>();
         }
+
+        List<BaseSpace> spaceList = baseSpaceService.list(
+                new LambdaQueryWrapper<BaseSpace>()
+                        .eq(BaseSpace::getParentId, project.getId())
+                        .eq(BaseSpace::getType, 3));
         if (spaceList == null || spaceList.isEmpty()) {
-            return resultList;
+            return new ArrayList<>();
         }
 
-        // 3. 为每个空间模拟能耗值(真实系统中此处应从 TSDB 或能耗统计表获取)
-        BigDecimal base = new BigDecimal("1000");
-        BigDecimal step = new BigDecimal("150");
-        BigDecimal factor;
-        if (dateType == 1) {
-            factor = BigDecimal.ONE;
-        } else if (dateType == 2) {
-            factor = new BigDecimal("1.5");
-        } else if (dateType == 3) {
-            factor = new BigDecimal("2.0");
-        } else {
-            factor = BigDecimal.ONE;
+        List<String> productCodeList = dmpProductMapper.selectProductCodesByEnergyType(
+                SecurityUtils.getTenantId(), energyType);
+        if (productCodeList == null || productCodeList.isEmpty()) {
+            return new ArrayList<>();
         }
+        Set<String> productCodeSet = productCodeList.stream()
+                .filter(StringUtils::isNotBlank)
+                .map(String::trim)
+                .collect(Collectors.toSet());
 
-        for (int i = 0; i < spaceList.size(); i++) {
-            BaseSpace space = spaceList.get(i);
-            if (space == null) {
+        Map<Long, List<String>> spaceDeviceUuidMap = loadSpaceDeviceUuidMap(spaceList, productCodeSet);
+
+        LocalDateTime endTime = LocalDateTime.now();
+        LocalDateTime startTime = getStartTime(dateType, endTime);
+        if (startTime == null) {
+            return new ArrayList<>();
+        }
+        String identifier = resolveRootIdentifierByEnergyType(energyType);
+        if (identifier == null) {
+            return new ArrayList<>();
+        }
+
+        Map<Long, BigDecimal> dataMap = new LinkedHashMap<>();
+        for (BaseSpace space : spaceList) {
+            if (space == null || space.getId() == null) {
                 continue;
             }
-            Map<String, Object> map = new HashMap<>();
-            map.put("name", space.getName());
-            BigDecimal value = base.subtract(step.multiply(BigDecimal.valueOf(i))).multiply(factor);
-            map.put("value", value);
-            resultList.add(map);
+            List<String> deviceUuids = spaceDeviceUuidMap.get(space.getId());
+            if (deviceUuids == null || deviceUuids.isEmpty()) {
+                continue;
+            }
+            BigDecimal value = sumEnergyByProductCodes(productCodeList, identifier,
+                    startTime, endTime, deviceUuids);
+            if (value != null) {
+                dataMap.put(space.getId(), value);
+            }
         }
 
-        // 4. 按 value 从大到小排序,形成能耗排名
-        resultList.sort((m1, m2) -> {
-            BigDecimal v1 = (BigDecimal) m1.get("value");
-            BigDecimal v2 = (BigDecimal) m2.get("value");
-            if (v1 == null && v2 == null) return 0;
-            if (v1 == null) return 1;
-            if (v2 == null) return -1;
-            return v2.compareTo(v1);
-        });
+        Map<Long, BaseSpace> spaceMap = spaceList.stream()
+                .filter(space -> space.getId() != null)
+                .collect(Collectors.toMap(BaseSpace::getId, space -> space, (oldVal, newVal) -> oldVal));
 
-        return resultList;
+        return dataMap.entrySet().stream()
+                .sorted(Map.Entry.comparingByValue())
+                .map(entry -> {
+                    Map<String, Object> map = new HashMap<>();
+                    BaseSpace space = spaceMap.get(entry.getKey());
+                    map.put("name", space != null ? space.getName() : null);
+                    map.put("value", entry.getValue());
+                    return map;
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 按空间批量解析网关及子设备 UUID(base_space_gateway → dmp_device)。
+     */
+    private Map<Long, List<String>> loadSpaceDeviceUuidMap(List<BaseSpace> spaceList, Set<String> productCodeSet) {
+        List<Long> spaceIds = spaceList.stream()
+                .map(BaseSpace::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (spaceIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        List<BaseSpaceGateway> links = baseSpaceGatewayMapper.selectList(
+                Wrappers.lambdaQuery(BaseSpaceGateway.class).in(BaseSpaceGateway::getSpaceId, spaceIds));
+        if (links == null || links.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Long> gatewayToSpace = new HashMap<>();
+        for (BaseSpaceGateway link : links) {
+            if (link.getSpaceId() == null || StringUtils.isBlank(link.getGatewayUuid())) {
+                continue;
+            }
+            gatewayToSpace.putIfAbsent(link.getGatewayUuid().trim(), link.getSpaceId());
+        }
+        if (gatewayToSpace.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        Map<Long, List<String>> spaceDeviceUuidMap = new HashMap<>();
+        for (String gatewayUuid : gatewayToSpace.keySet()) {
+            Long spaceId = gatewayToSpace.get(gatewayUuid);
+            List<DmpDevice> devices = dmpDeviceService.getDevicesByGatewayUuid(gatewayUuid);
+            if (devices == null || devices.isEmpty()) {
+                continue;
+            }
+            for (DmpDevice device : devices) {
+                if (device == null || StringUtils.isBlank(device.getDeviceUuid())
+                        || StringUtils.isBlank(device.getProductCode())) {
+                    continue;
+                }
+                if (!productCodeSet.contains(device.getProductCode().trim())) {
+                    continue;
+                }
+                spaceDeviceUuidMap.computeIfAbsent(spaceId, key -> new ArrayList<>())
+                        .add(device.getDeviceUuid().trim());
+            }
+        }
+
+        spaceDeviceUuidMap.replaceAll((spaceId, uuids) -> uuids.stream().distinct().collect(Collectors.toList()));
+        return spaceDeviceUuidMap;
     }
 
     /**
@@ -906,44 +1015,58 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
 
     /**
      * 分项能耗占比
-     * - 分项定义来自 leo.ems_energy_item_code:给定 itemCode 作为父项,按 parent_code 查询下级分项;
-     * - 若无下级分项,则仅返回父项自身;
-     * - 各分项及汇总能耗数值暂按规则模拟。
+     * - 根分项按 energyType 从 ems_energy_item_code 解析;
+     * - 冷/热(3/4)不展开下级,仅返回根分项;
+     * - 其余类型按 parent_code 展开子分项;
+     * - 各分项设备范围来自 ems_device_item_code,再调用 TSDB 汇总。
      */
     @Override
-    public List<Map<String, Object>> queryItemRatio(Integer dateType, String itemCode, Long projectId) {
+    public List<Map<String, Object>> queryItemRatio(Integer dateType, Integer energyType) {
         List<Map<String, Object>> resultList = new ArrayList<>();
-        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
+        if (dateType == null || energyType == null) {
+            return resultList;
+        }
+
+        EmsEnergyItemCode parent = emsEnergyItemCodeMapper.selectOne(
+                Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                        .eq(EmsEnergyItemCode::getParentCode, "0")
+                        .eq(EmsEnergyItemCode::getEnergyType, energyType)
+                        .last("LIMIT 1"));
+        if (parent == null || StringUtils.isBlank(parent.getIdentifier())) {
+            return resultList;
+        }
+
+        List<String> productCodeList = dmpProductMapper.selectProductCodesByEnergyType(
+                SecurityUtils.getTenantId(), energyType);
+        if (productCodeList == null || productCodeList.isEmpty()) {
             return resultList;
         }
 
-        // 当前实现中 startTime/spaceId 仅为预留,不参与模拟计算
         LocalDateTime endTime = LocalDateTime.now();
         LocalDateTime startTime = getStartTime(dateType, endTime);
         if (startTime == null) {
             return resultList;
         }
 
-        // 查询父分项编码
-        EmsEnergyItemCode parent = emsEnergyItemCodeMapper.selectOne(
-                new LambdaQueryWrapper<EmsEnergyItemCode>()
-                        .eq(EmsEnergyItemCode::getCode, itemCode)
-        );
-        if (parent == null) {
+        String parentIdentifier = parent.getIdentifier().trim().toLowerCase();
+
+        if (energyType == 3 || energyType == 4) {
+
+            BigDecimal total = sumEnergyForItemCode(productCodeList, parentIdentifier, parent.getCode(), startTime, endTime);
+            Map<String, Object> map = new HashMap<>();
+            map.put("name", parent.getName());
+            map.put("consume", total);
+            map.put("total", total);
+            resultList.add(map);
             return resultList;
         }
 
-        // 模拟父分项总能耗
-        BigDecimal total = simulateItemConsume(itemCode, dateType, 0);
 
-        // 查询子分项列表(按 parent_code)
+        BigDecimal total = sumEnergyForItemCode(productCodeList, parentIdentifier, null, startTime, endTime);
         List<EmsEnergyItemCode> children = emsEnergyItemCodeMapper.selectList(
-                new LambdaQueryWrapper<EmsEnergyItemCode>()
-                        .eq(EmsEnergyItemCode::getParentCode, parent.getCode())
-        );
-
+                Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                        .eq(EmsEnergyItemCode::getParentCode, parent.getCode()));
         if (children == null || children.isEmpty()) {
-            // 无下级分项:仅返回父项自身
             Map<String, Object> map = new HashMap<>();
             map.put("name", parent.getName());
             map.put("consume", total);
@@ -952,43 +1075,47 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
             return resultList;
         }
 
-        int index = 0;
         for (EmsEnergyItemCode child : children) {
-            if (child == null) continue;
-            BigDecimal consume = simulateItemConsume(child.getCode(), dateType, index);
+            if (child == null || StringUtils.isBlank(child.getIdentifier())) {
+                continue;
+            }
+            String childIdentifier = child.getIdentifier().trim().toLowerCase();
+            BigDecimal consume = sumEnergyForItemCode(productCodeList, childIdentifier, child.getCode(), startTime, endTime);
             Map<String, Object> map = new HashMap<>();
             map.put("name", child.getName());
             map.put("consume", consume);
             map.put("total", total);
             resultList.add(map);
-            index++;
         }
 
         return resultList;
     }
 
-    /**
-     * 分项能耗模拟函数
-     * 根据 dateType 与 index 返回不同量级的示例值。
-     */
-    private BigDecimal simulateItemConsume(String code, Integer dateType, int index) {
-        BigDecimal base = new BigDecimal("500");
-        BigDecimal step = new BigDecimal("80");
-        BigDecimal factor;
-        if (dateType != null && dateType == 1) {
-            factor = BigDecimal.ONE;
-        } else if (dateType != null && dateType == 2) {
-            factor = new BigDecimal("1.5");
-        } else if (dateType != null && dateType == 3) {
-            factor = new BigDecimal("2.0");
-        } else {
-            factor = BigDecimal.ONE;
+    private BigDecimal sumEnergyForItemCode(List<String> productCodeList, String fieldIdentifier, String itemCode, LocalDateTime startTime, LocalDateTime endTime) {
+        if(itemCode == null || itemCode.isEmpty()) {
+            return sumEnergyByProductCodes(productCodeList, fieldIdentifier, startTime, endTime, null);
         }
-        BigDecimal value = base.subtract(step.multiply(BigDecimal.valueOf(index))).multiply(factor);
-        if (value.compareTo(BigDecimal.ZERO) < 0) {
-            value = BigDecimal.ZERO;
+        List<String> deviceUuids = resolveDeviceUuidsByItemCode(itemCode);
+        if (deviceUuids.isEmpty()) {
+            return BigDecimal.ZERO;
         }
-        return value.setScale(2, RoundingMode.UP);
+        return sumEnergyByProductCodes(productCodeList, fieldIdentifier, startTime, endTime, deviceUuids);
+    }
+
+    private List<String> resolveDeviceUuidsByItemCode(String itemCode) {
+        if (StringUtils.isBlank(itemCode)) {
+            return Collections.emptyList();
+        }
+        List<String> deviceUuids = emsDeviceItemCodeMapper.selectDeviceUuidsByItemCode(
+                SecurityUtils.getTenantId(), itemCode.trim());
+        if (deviceUuids == null || deviceUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return deviceUuids.stream()
+                .filter(StringUtils::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
     }
 
     /** 根据设备系统编码返回名称(与 dmp_product.deviceType 含义保持一致) */

+ 16 - 0
service-ems/service-ems-biz/src/main/resources/mapper/ems/EmsDeviceItemCodeMapper.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.ems.mapper.EmsDeviceItemCodeMapper">
+
+    <select id="selectDeviceUuidsByItemCode" resultType="java.lang.String">
+        SELECT DISTINCT ic.device_uuid AS device_uuid
+        FROM ems_device_item_code ic
+        INNER JOIN dmp_device d ON d.device_uuid = ic.device_uuid
+        WHERE ic.item_code = #{itemCode}
+          AND d.delete_flag = 0
+          AND d.tenant_id = #{tenantId}
+          AND d.device_uuid IS NOT NULL
+          AND d.device_uuid != ''
+    </select>
+
+</mapper>

+ 4 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemSumQueryVO.java

@@ -5,6 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.io.Serializable;
+import java.util.List;
 
 /**
  * 能耗分项汇总查询参数(TDengine 超级表)
@@ -27,4 +28,7 @@ public class EnergyItemSumQueryVO implements Serializable {
 
     /** 结束时间(不含),如 2024-01-02 00:00:00.000 */
     private String endTime;
+
+    /** 设备 UUID 列表(可选;传入时仅汇总对应子表数据) */
+    private List<String> deviceUuid;
 }

+ 16 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/impl/QueryTdengineDataServiceImpl.java

@@ -329,10 +329,12 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
         assertSqlIdentifier(superTable, "super_table");
         assertSqlIdentifier(identifier, "identifier");
 
+        String deviceFilter = buildDeviceUuidFilter(requestVO.getDeviceUuid());
         String sql = "SELECT SUM(diff) AS sum_diff FROM ("
                 + " SELECT device_id, LAST(" + identifier + ") - FIRST(" + identifier + ") AS diff"
                 + " FROM " + superTable
                 + " WHERE ts >= '" + startTime + "' AND ts < '" + endTime + "'"
+                + deviceFilter
                 + " GROUP BY device_id"
                 + ") AS sub_table_diff";
 
@@ -423,6 +425,20 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
         throw new BusinessException("dateType 仅支持 1-小时、2-天、3-月");
     }
 
+    private static String buildDeviceUuidFilter(List<String> deviceUuids) {
+        if (deviceUuids == null || deviceUuids.isEmpty()) {
+            return "";
+        }
+        String tbnames = deviceUuids.stream()
+                .filter(StringUtils::isNotBlank)
+                .map(uuid -> "'_" + uuid.trim().replace("'", "''") + "'")
+                .collect(Collectors.joining(","));
+        if (tbnames.isEmpty()) {
+            return "";
+        }
+        return " AND tbname IN (" + tbnames + ")";
+    }
+
     private static void assertSqlIdentifier(String name, String paramName) {
         if (!SQL_IDENTIFIER_PATTERN.matcher(name).matches()) {
             throw new BusinessException(paramName + " 格式非法,仅允许字母、数字与下划线,且不能以数字开头");