james hai 5 días
pai
achega
a547066ba2

+ 20 - 2
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java

@@ -1,6 +1,7 @@
 package com.usky.ems.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.domain.DmpDevice;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.EmsEnergyItemCodeService;
 import com.usky.ems.service.EmsProjectService;
@@ -30,8 +31,9 @@ public class EmsProjectController {
      * 能耗类型列表(SELECT * FROM ems_energy_item_code WHERE parent_code = 0)
      */
     @GetMapping("/energy-type/list")
-    public ApiResult<List<EmsEnergyItemCode>> listEnergyTypes() {
-        return ApiResult.success(emsEnergyItemCodeService.listEnergyTypes());
+    public ApiResult<List<EmsEnergyItemCode>> listEnergyTypes(
+            @RequestParam(required = false) Integer energyType) {
+        return ApiResult.success(emsEnergyItemCodeService.listEnergyTypes(energyType));
     }
 
     /**
@@ -42,6 +44,22 @@ public class EmsProjectController {
         return ApiResult.success(emsEnergyItemCodeService.listEnergySubItems());
     }
 
+    /**
+     * 区域设备信息:按空间及其子空间关联网关,查询 dmp_device 子设备列表
+     */
+    @GetMapping("/area/device/list")
+    public ApiResult<List<DmpDevice>> listAreaDevices(@RequestParam Integer spaceId) {
+        return ApiResult.success(emsProjectService.listAreaDevices(spaceId));
+    }
+
+    /**
+     * 区域能耗分项类型信息:按区域设备关联的分项编码查询 ems_energy_item_code
+     */
+    @GetMapping("/area/device-item-code/list")
+    public ApiResult<List<EmsEnergyItemCode>> listAreaDeviceItemCodes(@RequestParam Integer spaceId) {
+        return ApiResult.success(emsProjectService.listAreaDeviceItemCodes(spaceId));
+    }
+
     /**
      * 产品关联的能源类型(ems_product_energy_type)
      */

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

@@ -46,5 +46,4 @@ public class EmsDeviceItemCode implements Serializable {
 
     /** 租户号 */
     private Integer tenantId;
-
 }

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

@@ -34,16 +34,16 @@ public class EmsEnergyItemCode implements Serializable {
     private String identifier;
     @TableField("energy_type")
     private Integer energyType;
-
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
     @TableField("created_by")
     private String createdBy;
 
     @TableField("created_time")
     private LocalDateTime createdTime;
 
-    @TableField("updated_by")
-    private String updatedBy;
-
     @TableField("updated_time")
     private LocalDateTime updatedTime;
 

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

@@ -17,9 +17,9 @@ public interface EmsEnergyItemCodeService extends CrudService<EmsEnergyItemCode>
     List<EnergyItemCodeVO> querySpaceEnergyItem(Long spaceId);
 
     /**
-     * 能耗类型列表:parent_code = 0
+     * 能耗类型列表:parent_code = 0,可按 energyType 筛选
      */
-    List<EmsEnergyItemCode> listEnergyTypes();
+    List<EmsEnergyItemCode> listEnergyTypes(Integer energyType);
 
     /**
      * 能耗分项列表:parent_code 为能耗类型(parent_code = 0)的编码

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java

@@ -1,10 +1,14 @@
 package com.usky.ems.service;
 
+import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
 import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsProjectSaveRequest;
 
+import java.util.List;
+
 /**
  * 项目(ems_project)维护:新增、修改、删除(含空间树、省市区校验、设备系统关联)
  */
@@ -25,4 +29,14 @@ public interface EmsProjectService {
      * 保存设备关联的能源分项:先删除当前租户下该分项编码的记录,再按设备列表插入
      */
     void saveDeviceItemCodes(EmsDeviceItemCodeSaveRequest request);
+
+    /**
+     * 区域设备信息:按空间及其子空间关联网关,查询 dmp_device 子设备列表
+     */
+    List<DmpDevice> listAreaDevices(Integer spaceId);
+
+    /**
+     * 区域能耗分项类型信息:按区域设备关联的分项编码查询 ems_energy_item_code
+     */
+    List<EmsEnergyItemCode> listAreaDeviceItemCodes(Integer spaceId);
 }

+ 193 - 88
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java

@@ -23,6 +23,7 @@ import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
 import com.usky.ems.service.EmsAnalysisService;
 import com.usky.ems.service.TdengineService;
 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;
@@ -36,6 +37,7 @@ import java.sql.ResultSet;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -43,6 +45,7 @@ import java.util.stream.Collectors;
 /**
  * 能耗分析服务实现
  */
+@Slf4j
 @Service
 @DS("mysql")
 public class EmsAnalysisServiceImpl implements EmsAnalysisService {
@@ -1026,126 +1029,228 @@ public class EmsAnalysisServiceImpl implements EmsAnalysisService {
                 resp.setEnergyTypeName(id >= 1 && id <= 3 ? ENERGY_TYPE_NAMES[(int) id] : "");
             }
 
-            // 设置时间维度
-            resp.setDimension(request.getTimeDimension());
-
-            // 为每个设备查询对比数据
-            List<EmsCompareSeriesItemVO> series = new ArrayList<>();
-
-            for (String deviceId : request.getDeviceIds()) {
-                EmsCompareSeriesItemVO seriesItem = getDeviceCompareData(deviceId, request.getEnergyTypeId(),
-                        request.getTimeDimension(), request.getTimeValues());
-                if (seriesItem != null) {
-                    series.add(seriesItem);
-                }
-            }
+            // 根据查询类型和起止时间生成完整时间轴
+            List<String> timeSeries = generateCompareTimeSeries(request.getTimeDimension(), request.getTimeValues());
+            resp.setTimeDimension(request.getTimeDimension());
+            resp.setDimension(timeSeries);
 
+            // 批量查询设备历史数据并按时间轴计算对比值
+            List<EmsCompareSeriesItemVO> series = buildCompareSeries(
+                    request.getDeviceIds(), request.getTimeDimension(), timeSeries);
             resp.setSeries(series);
 
         } catch (Exception e) {
-            System.err.println("Error getting compare data: " + e.getMessage());
+            log.warn("获取对比数据异常: {}", e.getMessage(), e);
         }
 
         return resp;
     }
 
     /**
-     * 获取设备对比数据
+     * 根据查询类型(日/月/年)和起止时间,生成完整时间序列
      */
-    private EmsCompareSeriesItemVO getDeviceCompareData(String deviceId, Long energyTypeId, String timeDimension, List<String> timeValues) {
-        try (Connection conn = mysqlDataSource.getConnection()) {
-            // 获取设备名称
-            String deviceName = getDeviceName(deviceId, conn);
-
-            List<EmsCompareValueVO> values = new ArrayList<>();
-
-            // 为每个时间点查询数据
-            for (String timeValue : timeValues) {
-                String[] timeRange = getTimeRange(timeDimension, timeValue);
-                BigDecimal usage = queryDeviceEnergyForCompare(deviceId, energyTypeId, timeRange[0], timeRange[1], conn);
+    private List<String> generateCompareTimeSeries(String timeDimension, List<String> timeValues) {
+        if (timeValues == null || timeValues.isEmpty()) {
+            return new ArrayList<>();
+        }
+        String start = timeValues.get(0).trim();
+        String end = (timeValues.size() > 1 ? timeValues.get(timeValues.size() - 1) : start).trim();
+        if (start.isEmpty()) {
+            return new ArrayList<>();
+        }
+        if (end.isEmpty()) {
+            end = start;
+        }
 
-                EmsCompareValueVO valueItem = new EmsCompareValueVO();
-                valueItem.setTimeValue(timeValue);
-                valueItem.setUsage(usage.setScale(2, RoundingMode.HALF_UP));
-                values.add(valueItem);
+        try {
+            switch (timeDimension.toUpperCase()) {
+                case "D":
+                    return generateDailyRange(start, end);
+                case "M":
+                    return generateMonthlyRange(start, end);
+                case "Y":
+                    return generateYearlyRange(start, end);
+                default:
+                    return new ArrayList<>(timeValues);
             }
+        } catch (Exception e) {
+            log.warn("生成对比时间序列失败: timeDimension={}, timeValues={}", timeDimension, timeValues, e);
+            return new ArrayList<>(timeValues);
+        }
+    }
 
-            EmsCompareSeriesItemVO seriesItem = new EmsCompareSeriesItemVO();
-            seriesItem.setDeviceId(deviceId);
-            seriesItem.setDeviceName(deviceName);
-            seriesItem.setValues(values);
-
-            return seriesItem;
+    private List<String> generateDailyRange(String start, String end) {
+        List<String> points = new ArrayList<>();
+        LocalDate startDate = LocalDate.parse(start, DateTimeFormatter.ISO_LOCAL_DATE);
+        LocalDate endDate = LocalDate.parse(end, DateTimeFormatter.ISO_LOCAL_DATE);
+        if (endDate.isBefore(startDate)) {
+            LocalDate tmp = startDate;
+            startDate = endDate;
+            endDate = tmp;
+        }
+        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;
+        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
+            points.add(date.format(formatter));
+        }
+        return points;
+    }
 
-        } catch (Exception e) {
-            System.err.println("Error getting device compare data: " + e.getMessage());
-            return null;
+    private List<String> generateMonthlyRange(String start, String end) {
+        List<String> points = new ArrayList<>();
+        YearMonth startMonth = YearMonth.parse(start, DateTimeFormatter.ofPattern("yyyy-MM"));
+        YearMonth endMonth = YearMonth.parse(end, DateTimeFormatter.ofPattern("yyyy-MM"));
+        if (endMonth.isBefore(startMonth)) {
+            YearMonth tmp = startMonth;
+            startMonth = endMonth;
+            endMonth = tmp;
+        }
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
+        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
+            points.add(month.format(formatter));
         }
+        return points;
     }
 
-    /**
-     * 获取设备名称
-     */
-    private String getDeviceName(String deviceId, Connection conn) {
-        try {
-            String sql = "SELECT name FROM ems_device WHERE id = ?";
-            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
-                stmt.setString(1, deviceId);
-                try (ResultSet rs = stmt.executeQuery()) {
-                    if (rs.next()) {
-                        return rs.getString("name");
-                    }
-                }
-            }
-        } catch (Exception e) {
-            System.err.println("Error getting device name: " + e.getMessage());
+    private List<String> generateYearlyRange(String start, String end) {
+        List<String> points = new ArrayList<>();
+        int startYear = Integer.parseInt(start);
+        int endYear = Integer.parseInt(end);
+        if (endYear < startYear) {
+            int tmp = startYear;
+            startYear = endYear;
+            endYear = tmp;
+        }
+        for (int year = startYear; year <= endYear; year++) {
+            points.add(String.valueOf(year));
         }
-        return "未知设备";
+        return points;
     }
 
     /**
-     * 查询设备能耗(用于对比)
+     * 批量查询设备对比数据:一次拉取 TSDB 历史数据,再按时间轴分段计算用量
      */
-    private BigDecimal queryDeviceEnergyForCompare(String deviceId, Long energyTypeId, String startTime, String endTime, Connection conn) {
-        try {
-            StringBuilder sql = new StringBuilder();
-            sql.append("SELECT SUM(eed.end_value - eed.start_value) as total_usage ");
-            sql.append("FROM ems_energy_consumption_device_data eed ");
-            if (energyTypeId != null) {
-                sql.append("INNER JOIN ems_device ed ON eed.device_id = ed.id ");
-                sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
-            }
-            sql.append("WHERE eed.device_id = ? ");
-            sql.append("AND eed.start_time >= ? AND eed.end_time <= ? ");
-
-            if (energyTypeId != null) {
-                sql.append("AND epe.energy_type = ? ");
-            }
+    private List<EmsCompareSeriesItemVO> buildCompareSeries(List<String> deviceIds, String timeDimension,
+                                                            List<String> timeSeries) {
+        if (deviceIds == null || deviceIds.isEmpty() || timeSeries == null || timeSeries.isEmpty()) {
+            return Collections.emptyList();
+        }
 
-            try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
-                int paramIndex = 1;
-                stmt.setString(paramIndex++, deviceId);
-                stmt.setString(paramIndex++, startTime);
-                stmt.setString(paramIndex++, endTime);
+        Map<String, DmpDevice> deviceMap = loadCompareDevices(deviceIds);
+        Set<String> deviceUuids = deviceMap.values().stream()
+                .map(DmpDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
 
-                if (energyTypeId != null) {
-                    stmt.setLong(paramIndex++, energyTypeId);
+        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricData = Collections.emptyMap();
+        if (!deviceUuids.isEmpty()) {
+            String[] queryRange = getOverallCompareQueryRange(timeDimension, timeSeries);
+            deviceMetricData = queryDeviceMetricHistory(deviceUuids, queryRange[0], queryRange[1]);
+        }
+
+        List<AverageTimeSegment> segments = buildCompareTimeSegments(timeDimension, timeSeries);
+        List<EmsCompareSeriesItemVO> series = new ArrayList<>();
+        for (String deviceId : deviceIds) {
+            DmpDevice device = deviceMap.get(deviceId);
+            String deviceUuid = device != null && StringUtils.hasText(device.getDeviceUuid())
+                    ? device.getDeviceUuid().trim() : null;
+            TreeMap<LocalDateTime, Double> timeValueMap = null;
+            if (deviceUuid != null) {
+                Map<String, TreeMap<LocalDateTime, Double>> metricMap = deviceMetricData.get(deviceUuid);
+                if (metricMap != null) {
+                    timeValueMap = metricMap.get(ENERGY_METRIC);
                 }
+            }
 
-                try (ResultSet rs = stmt.executeQuery()) {
-                    if (rs.next()) {
-                        double usage = rs.getDouble("total_usage");
-                        if (!rs.wasNull()) {
-                            return new BigDecimal(usage);
-                        }
+            List<EmsCompareValueVO> values = new ArrayList<>();
+            for (AverageTimeSegment segment : segments) {
+                BigDecimal usage = BigDecimal.ZERO.setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+                if (timeValueMap != null && !timeValueMap.isEmpty()) {
+                    SegmentUsageResult usageResult = calculateSegmentUsage(
+                            timeValueMap, segment.getStartInclusive(), segment.getEndExclusive());
+                    if (usageResult.isHasValidReading()) {
+                        usage = BigDecimal.valueOf(usageResult.getUsage())
+                                .setScale(USAGE_SCALE, RoundingMode.HALF_UP);
                     }
                 }
+                EmsCompareValueVO valueItem = new EmsCompareValueVO();
+                valueItem.setTimeValue(segment.getLabel());
+                valueItem.setUsage(usage);
+                values.add(valueItem);
             }
-        } catch (Exception e) {
-            System.err.println("Error querying device energy for compare: " + e.getMessage());
+
+            EmsCompareSeriesItemVO seriesItem = new EmsCompareSeriesItemVO();
+            seriesItem.setDeviceId(deviceId);
+            seriesItem.setDeviceName(device != null && StringUtils.hasText(device.getDeviceName())
+                    ? device.getDeviceName() : "未知设备");
+            seriesItem.setValues(values);
+            series.add(seriesItem);
         }
+        return series;
+    }
 
-        return BigDecimal.ZERO;
+    private Map<String, DmpDevice> loadCompareDevices(List<String> deviceIds) {
+        List<DmpDevice> devices = dmpDeviceMapper.selectList(
+                new LambdaQueryWrapper<DmpDevice>()
+                        .in(DmpDevice::getDeviceId, deviceIds)
+                        .eq(DmpDevice::getDeleteFlag, 0));
+        Map<String, DmpDevice> deviceMap = new LinkedHashMap<>();
+        for (DmpDevice device : devices) {
+            if (device != null && StringUtils.hasText(device.getDeviceId())) {
+                deviceMap.putIfAbsent(device.getDeviceId().trim(), device);
+            }
+        }
+        return deviceMap;
+    }
+
+    private String[] getOverallCompareQueryRange(String timeDimension, List<String> timeSeries) {
+        LocalDateTime[] firstSegment = resolveCompareSegmentRange(timeDimension, timeSeries.get(0));
+        LocalDateTime[] lastSegment = resolveCompareSegmentRange(timeDimension, timeSeries.get(timeSeries.size() - 1));
+        return new String[]{
+                firstSegment[0].format(DATE_TIME_FORMATTER),
+                lastSegment[1].minusSeconds(1).format(DATE_TIME_FORMATTER)
+        };
+    }
+
+    private List<AverageTimeSegment> buildCompareTimeSegments(String timeDimension, List<String> timeSeries) {
+        List<AverageTimeSegment> segments = new ArrayList<>();
+        for (String timeValue : timeSeries) {
+            LocalDateTime[] range = resolveCompareSegmentRange(timeDimension, timeValue);
+            segments.add(new AverageTimeSegment(timeValue, range[0], range[1]));
+        }
+        return segments;
+    }
+
+    /**
+     * 对比分析时间段:日=自然日,月=自然月,年=自然年
+     */
+    private LocalDateTime[] resolveCompareSegmentRange(String timeDimension, String timePoint) {
+        switch (timeDimension.toUpperCase()) {
+            case "D":
+                LocalDate day = LocalDate.parse(timePoint, DateTimeFormatter.ISO_LOCAL_DATE);
+                return new LocalDateTime[]{
+                        day.atStartOfDay(),
+                        day.plusDays(1).atStartOfDay()
+                };
+            case "M":
+                YearMonth month = YearMonth.parse(timePoint, DateTimeFormatter.ofPattern("yyyy-MM"));
+                return new LocalDateTime[]{
+                        month.atDay(1).atStartOfDay(),
+                        month.plusMonths(1).atDay(1).atStartOfDay()
+                };
+            case "Y":
+                int year = Integer.parseInt(timePoint);
+                return new LocalDateTime[]{
+                        LocalDate.of(year, 1, 1).atStartOfDay(),
+                        LocalDate.of(year + 1, 1, 1).atStartOfDay()
+                };
+            default:
+                String[] range = getTimeRange(timeDimension, timePoint);
+                return new LocalDateTime[]{
+                        parseDateTime(range[0]),
+                        parseDateTime(range[1]).plusSeconds(1)
+                };
+        }
     }
 
     @Override

+ 12 - 11
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsEnergyItemCodeServiceImpl.java

@@ -117,22 +117,22 @@ public class EmsEnergyItemCodeServiceImpl
     }
 
     @Override
-    public List<EmsEnergyItemCode> listEnergyTypes() {
-        List<EmsEnergyItemCode> energyTypes = listRootEnergyTypes();
+    public List<EmsEnergyItemCode> listEnergyTypes(Integer energyType) {
+        List<EmsEnergyItemCode> energyTypes = listRootEnergyTypes(energyType);
         if (energyTypes.isEmpty()) {
             return energyTypes;
         }
         Map<Integer, List<Long>> productIdsByEnergyType = loadProductIdsByEnergyType(SecurityUtils.getTenantId());
-        for (EmsEnergyItemCode energyType : energyTypes) {
-            energyType.setProductIdList(
-                    productIdsByEnergyType.getOrDefault(energyType.getEnergyType(), Collections.emptyList()));
+        for (EmsEnergyItemCode energyType1 : energyTypes) {
+            energyType1.setProductIdList(
+                    productIdsByEnergyType.getOrDefault(energyType1.getEnergyType(), Collections.emptyList()));
         }
         return energyTypes;
     }
 
     @Override
     public List<EmsEnergyItemCode> listEnergySubItems() {
-        List<String> rootCodes = listRootEnergyTypes().stream()
+        List<String> rootCodes = listRootEnergyTypes(null).stream()
                 .map(EmsEnergyItemCode::getCode)
                 .filter(Objects::nonNull)
                 .distinct()
@@ -155,11 +155,12 @@ public class EmsEnergyItemCodeServiceImpl
         return energySubItems;
     }
 
-    private List<EmsEnergyItemCode> listRootEnergyTypes() {
-        return this.list(
-                Wrappers.<EmsEnergyItemCode>lambdaQuery()
-                        .eq(EmsEnergyItemCode::getParentCode, "0")
-                        .orderByAsc(EmsEnergyItemCode::getCode));
+    private List<EmsEnergyItemCode> listRootEnergyTypes(Integer energyType) {
+        LambdaQueryWrapper<EmsEnergyItemCode> wrapper = Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                .eq(EmsEnergyItemCode::getParentCode, "0")
+                .eq(energyType != null, EmsEnergyItemCode::getEnergyType, energyType)
+                .orderByAsc(EmsEnergyItemCode::getCode);
+        return this.list(wrapper);
     }
 
     private Map<Integer, List<Long>> loadProductIdsByEnergyType(Integer tenantId) {

+ 91 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java

@@ -1,14 +1,21 @@
 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.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.ems.domain.BaseSpace;
+import com.usky.ems.domain.BaseSpaceGateway;
+import com.usky.ems.domain.DmpDevice;
 import com.usky.ems.domain.EmsDeviceItemCode;
+import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.domain.EmsProductEnergyType;
 import com.usky.ems.domain.EmsProject;
 import com.usky.ems.domain.EmsProjectDeviceSystem;
+import com.usky.ems.mapper.BaseSpaceGatewayMapper;
+import com.usky.ems.mapper.DmpDeviceMapper;
 import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
+import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
 import com.usky.ems.mapper.EmsProductEnergyTypeMapper;
 import com.usky.ems.mapper.EmsProjectDeviceSystemMapper;
 import com.usky.ems.mapper.EmsProjectMapper;
@@ -26,9 +33,11 @@ import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 项目维护实现(逻辑参考原 ProjectServiceImpl:省市区校验、创建项目空间、设备系统关联、删除前校验叶子节点等)
@@ -50,6 +59,12 @@ public class EmsProjectServiceImpl implements EmsProjectService {
     private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
     @Autowired
     private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
+    @Autowired
+    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
+    @Autowired
+    private DmpDeviceMapper dmpDeviceMapper;
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -182,6 +197,82 @@ public class EmsProjectServiceImpl implements EmsProjectService {
         }
     }
 
+    @Override
+    public List<DmpDevice> listAreaDevices(Integer spaceId) {
+        List<String> gatewayUuids = resolveGatewayUuidsBySpaceId(spaceId);
+        if (gatewayUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return dmpDeviceMapper.selectList(
+                Wrappers.<DmpDevice>lambdaQuery()
+                        .in(DmpDevice::getGatewayUuid, gatewayUuids)
+                        .eq(DmpDevice::getCategoryType, 3)
+                        .eq(DmpDevice::getDeleteFlag, 0));
+    }
+
+    @Override
+    public List<EmsEnergyItemCode> listAreaDeviceItemCodes(Integer spaceId) {
+        List<DmpDevice> devices = listAreaDevices(spaceId);
+        if (devices.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> deviceUuids = devices.stream()
+                .map(DmpDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<EmsDeviceItemCode> deviceItemCodes = emsDeviceItemCodeMapper.selectList(
+                Wrappers.<EmsDeviceItemCode>lambdaQuery()
+                        .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
+                        .eq(EmsDeviceItemCode::getTenantId, SecurityUtils.getTenantId()));
+        if (deviceItemCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> itemCodes = deviceItemCodes.stream()
+                .map(EmsDeviceItemCode::getItemCode)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+        if (itemCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return emsEnergyItemCodeMapper.selectList(
+                Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                        .in(EmsEnergyItemCode::getCode, itemCodes)
+                        .orderByAsc(EmsEnergyItemCode::getCode));
+    }
+
+    private List<String> resolveGatewayUuidsBySpaceId(Integer spaceId) {
+        if (spaceId == null) {
+            throw new BusinessException("空间id不能为空");
+        }
+        BaseSpace space = baseSpaceService.getById(spaceId.longValue());
+        if (space == null) {
+            throw new BusinessException("空间不存在");
+        }
+        List<Long> spaceIds = baseSpaceService.getAuthorizedSpaceIds(space.getId());
+        if (spaceIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<BaseSpaceGateway> links = baseSpaceGatewayMapper.selectList(
+                Wrappers.<BaseSpaceGateway>lambdaQuery()
+                        .in(BaseSpaceGateway::getSpaceId, spaceIds));
+        if (CollectionUtils.isEmpty(links)) {
+            return Collections.emptyList();
+        }
+        return links.stream()
+                .map(BaseSpaceGateway::getGatewayUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
     /**
      * 对应原 UserService.hasLoggedUser(projectId)。接入统一用户/权限模块后可在此查询用户与项目绑定关系。
      */

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

@@ -9,6 +9,9 @@ import java.util.List;
 public class EmsCompareResponse {
 
     private String energyTypeName;
-    private String dimension;
+    /** 查询粒度:D/M/Y */
+    private String timeDimension;
+    /** 时间轴标签,如日 2026-06-01…2026-06-25 */
+    private List<String> dimension = new ArrayList<>();
     private List<EmsCompareSeriesItemVO> series = new ArrayList<>();
 }