Explorar o código

1、开发能耗用能趋势接口;
2、开发能耗分项趋势Api接口;

james hai 5 días
pai
achega
940c4a08e8

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

@@ -11,6 +11,9 @@ import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 能源能耗 - 概览模块 API
  * 建筑排名等涉及空间树的数据来自 base_space(见 {@link com.usky.ems.service.impl.EmsOverviewServiceImpl})。
@@ -45,7 +48,7 @@ public class EmsOverviewController {
      * 获取概览页能源类型条目(如电/水/气)
      */
     @GetMapping("/item")
-    public ApiResult<java.util.List<EmsOverviewEnergyItemVO>> queryOverviewItem() {
+    public ApiResult<List<EmsOverviewEnergyItemVO>> queryOverviewItem() {
         return ApiResult.success(emsOverviewService.queryOverviewItem());
     }
 
@@ -53,7 +56,7 @@ public class EmsOverviewController {
      * 获取概览页设备系统统计(每个系统下设备数量)
      */
     @GetMapping("/device-info")
-    public ApiResult<java.util.List<EmsOverviewDeviceSystemStatVO>> queryOverviewDeviceInfo(
+    public ApiResult<List<EmsOverviewDeviceSystemStatVO>> queryOverviewDeviceInfo(
             @RequestParam(required = false) Long projectId) {
         return ApiResult.success(emsOverviewService.queryOverviewDeviceInfo(projectId));
     }
@@ -67,7 +70,7 @@ public class EmsOverviewController {
      * - projectId:项目 ID(可选,不传则取当前租户第一个项目;用于查询折算系数)
      */
     @GetMapping("/classification-energy")
-    public ApiResult<java.util.Map<String, Object>> queryClassificationEnergy(
+    public ApiResult<Map<String, Object>> queryClassificationEnergy(
             @RequestParam Integer dateType,
             @RequestParam String identifier,
             @RequestParam Integer energyType,
@@ -76,18 +79,18 @@ public class EmsOverviewController {
     }
 
     /**
-     * 能耗用能趋势(模拟数据
+     * 能耗用能趋势(按能源类型关联产品,调用 TSDB 分项按时间粒度汇总,并与去年同期对比
      * 参数说明:
-     * - dateType:时间类型(1-日,2-月,3-年)
-     * - itemCode:能耗条目编码
-     * - spaceId:空间ID(可选,当前仍为模拟数据占位
+     * - dateType:时间类型(1-日/小时,2-月/天,3-年/月
+     * - identifier:分项字段编码
+     * - energyType:能源类型(1电 2水 3冷 4热
      */
     @GetMapping("/energy-trend")
-    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryEnergyTrend(
+    public ApiResult<List<Map<String, Object>>> queryEnergyTrend(
             @RequestParam Integer dateType,
-            @RequestParam String itemCode,
-            @RequestParam(required = false) Long spaceId) {
-        return ApiResult.success(emsOverviewService.queryEnergyTrend(dateType, itemCode, spaceId));
+            @RequestParam String identifier,
+            @RequestParam Integer energyType) {
+        return ApiResult.success(emsOverviewService.queryEnergyTrend(dateType, identifier, energyType));
     }
 
     /**
@@ -98,7 +101,7 @@ public class EmsOverviewController {
      * - spaceId:空间ID(可选;下钻时与 base_space 子节点一致)
      */
     @GetMapping("/building-ranking")
-    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryBuildingRanking(
+    public ApiResult<List<Map<String, Object>>> queryBuildingRanking(
             @RequestParam Integer dateType,
             @RequestParam String itemCode,
             @RequestParam(required = false) Long spaceId) {
@@ -112,7 +115,7 @@ public class EmsOverviewController {
      * - projectId:项目ID(可选,不传则取第一个项目)
      */
     @GetMapping("/top")
-    public ApiResult<java.util.Map<String, Object>> queryOverviewTop(
+    public ApiResult<Map<String, Object>> queryOverviewTop(
             @RequestParam Integer dateType,
             @RequestParam(required = false) Long projectId) {
         return ApiResult.success(emsOverviewService.queryOverviewTop(dateType, projectId));
@@ -126,7 +129,7 @@ public class EmsOverviewController {
      * - projectId:项目ID(可选,不传则取第一个项目)
      */
     @GetMapping("/item-ratio")
-    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryItemRatio(
+    public ApiResult<List<Map<String, Object>>> queryItemRatio(
             @RequestParam Integer dateType,
             @RequestParam String itemCode,
             @RequestParam(required = false) Long projectId) {

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

@@ -40,9 +40,9 @@ public interface EmsOverviewService {
 
     /**
      * 能耗用能趋势(按时间维度:日/月/年)
-     * 返回当前期与去年同期的用能趋势数据列表(当前为模拟数据实现)
+     * 按能源类型关联产品调用 TSDB 汇总,返回当前期与去年同期各时间粒度的用能数据。
      */
-    java.util.List<java.util.Map<String, Object>> queryEnergyTrend(Integer dateType, String itemCode, Long spaceId);
+    java.util.List<java.util.Map<String, Object>> queryEnergyTrend(Integer dateType, String identifier, Integer energyType);
 
     /**
      * 建筑能耗分析(建筑/楼层能耗排名)

+ 257 - 54
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java

@@ -7,6 +7,9 @@ import com.usky.common.security.utils.SecurityUtils;
 import com.usky.demo.RemoteTsdbProxyService;
 import com.usky.demo.domain.EnergyItemSumQueryVO;
 import com.usky.demo.domain.EnergyItemSumResultVO;
+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.enums.EnergyTypeEnum;
 import com.usky.ems.mapper.DmpProductMapper;
@@ -33,6 +36,8 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -50,6 +55,13 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
 
     private static final DateTimeFormatter TSDB_TIME_FORMAT =
             DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+    /** 兼容 TDengine/JDBC 返回的可变小数秒,如 2026-06-03 00:00:00.0 */
+    private static final DateTimeFormatter TSDB_FLEXIBLE_TIME_FORMAT = new DateTimeFormatterBuilder()
+            .appendPattern("yyyy-MM-dd HH:mm:ss")
+            .optionalStart()
+            .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
+            .optionalEnd()
+            .toFormatter();
     private static final BigDecimal FALLBACK_CO2_FACTOR = new BigDecimal("2.4589");
     private static final BigDecimal FALLBACK_COAL_FACTOR = new BigDecimal("0.70");
     private static final String FACTOR_NAME_COAL = "coal";
@@ -406,78 +418,269 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     }
 
     /**
-     * 能耗用能趋势(模拟数据版本)
-     * 参考原系统 queryEnergyTrend 逻辑,仅保留时间维度与同比的结构,数据采用固定规则模拟。
+     * 能耗用能趋势:按产品+时间范围查询 TSDB 得 time/value,再按原系统 getListResult 逻辑封装。
      */
     @Override
-    public List<Map<String, Object>> queryEnergyTrend(Integer dateType, String itemCode, Long spaceId) {
-        List<Map<String, Object>> list = new ArrayList<>();
-
-        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
-            return list;
+    public List<Map<String, Object>> queryEnergyTrend(Integer dateType, String identifier, Integer energyType) {
+        if (dateType == null || StringUtils.isBlank(identifier) || energyType == null) {
+            return new ArrayList<>();
         }
 
-        // 当前期与去年同期的时间范围
-        LocalDateTime endThisTime;
-        LocalDateTime startThisTime;
-        LocalDateTime endOldTime;
-        LocalDateTime startOldTime;
+        List<String> productCodeList = dmpProductMapper.selectProductCodesByEnergyType(
+                SecurityUtils.getTenantId(), energyType);
+        if (productCodeList == null || productCodeList.isEmpty()) {
+            return new ArrayList<>();
+        }
 
-        if (dateType == 1) {
-            // 日:按小时
-            endThisTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
-        } else if (dateType == 2) {
-            // 月:按天
-            endThisTime = LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
-        } else if (dateType == 3) {
-            // 年:按月
-            endThisTime = LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfYear()), LocalTime.MAX);
-        } else {
-            return list;
+        if (dateType == 1 || dateType == 2 || dateType == 3) {
+            return buildEnergyTrendResult(productCodeList, identifier, dateType);
         }
+        return new ArrayList<>();
+    }
 
-        startThisTime = getStartTime(dateType, endThisTime);
-        endOldTime = endThisTime.minusYears(1);
-        startOldTime = getStartTime(dateType, endOldTime);
+    /**
+     * 与原系统 queryEnergyTrend 一致:拉取当前期/去年同期 time-value 序列,构建 dateList 后 getListResult 封装。
+     */
+    private List<Map<String, Object>> buildEnergyTrendResult(List<String> productCodeList, String identifier,
+                                                             Integer dateType) {
+        LocalDateTime endThisTime = getTrendEndTime(dateType);
+        LocalDateTime startThisTime = getStartTime(dateType, endThisTime);
+        LocalDateTime endOldTime = endThisTime.minusYears(1);
+        LocalDateTime startOldTime = getStartTime(dateType, endOldTime);
 
-        // 按时间步长循环生成当前期与去年同期的模拟趋势数据
-        LocalDateTime curTime = startThisTime;
-        LocalDateTime oldTime = startOldTime;
-        int index = 0;
+        String fieldIdentifier = identifier.toLowerCase();
+
+        List<TrendDataPoint> pointVOList = queryTrendDataPoints(productCodeList, fieldIdentifier,
+                startThisTime, endThisTime, dateType);
+        Map<Integer, TrendDataPoint> pointVOMap = toTrendPointMap(pointVOList, dateType);
+
+        List<TrendDataPoint> pointOldList = queryTrendDataPoints(productCodeList, fieldIdentifier,
+                startOldTime, endOldTime, dateType);
+        Map<Integer, TrendDataPoint> pointOldMap = toTrendPointMap(pointOldList, dateType);
+
+        List<String> dateList = buildTrendDateList(dateType, startThisTime, endThisTime);
+
+        return getTrendListResult(dateList, pointVOMap, pointOldMap, dateType);
+    }
 
-        while (!curTime.isAfter(endThisTime)) {
-            Map<String, Object> row = new HashMap<>();
-            // 时间标签:当前期时间
-            row.put("time", curTime.toString());
-            // 当前期用能:示例 100 + index * 5
-            BigDecimal currentValue = BigDecimal.valueOf(100 + index * 5L);
-            // 去年同期用能:示例 80 + index * 5
-            BigDecimal lastYearValue = BigDecimal.valueOf(80 + index * 5L);
-            row.put("currentValue", currentValue);
-            row.put("lastYearValue", lastYearValue);
-            // 去年同期时间标签
-            row.put("lastYearTime", oldTime.toString());
-
-            list.add(row);
-
-            // 根据时间类型推进时间
+    /**
+     * 按产品列表查询 TSDB,返回合并后的 time/value 序列(同时间点跨产品累加)。
+     */
+    private List<TrendDataPoint> queryTrendDataPoints(List<String> productCodeList, String fieldIdentifier,
+                                                      LocalDateTime startTime, LocalDateTime endTime,
+                                                      Integer dateType) {
+        Map<String, BigDecimal> merged = new HashMap<>();
+        String start = formatTsdbTime(startTime);
+        String end = formatTsdbTime(endTime);
+        for (String productCode : productCodeList) {
+            if (StringUtils.isBlank(productCode)) {
+                continue;
+            }
+            EnergyItemTrendQueryVO queryVO = new EnergyItemTrendQueryVO();
+            queryVO.setIdentifier(fieldIdentifier);
+            queryVO.setSuperTable("super_" + productCode.trim());
+            queryVO.setStartTime(start);
+            queryVO.setEndTime(end);
+            queryVO.setDateType(dateType);
+            EnergyItemTrendResultVO apiResult = remoteTsdbProxyService.sumEnergyItemTrend(queryVO);
+            if (apiResult == null || apiResult.getPoints() == null) {
+                continue;
+            }
+            for (EnergyItemTrendPointVO point : apiResult.getPoints()) {
+                if (point == null || StringUtils.isBlank(point.getTime())) {
+                    continue;
+                }
+                BigDecimal value = point.getValue() != null ? point.getValue() : BigDecimal.ZERO;
+                merged.merge(point.getTime(), value, BigDecimal::add);
+            }
+        }
+        List<TrendDataPoint> result = new ArrayList<>();
+        for (Map.Entry<String, BigDecimal> entry : merged.entrySet()) {
+            TrendDataPoint dataPoint = new TrendDataPoint();
+            dataPoint.setTime(entry.getKey());
+            dataPoint.setValue(entry.getValue());
+            result.add(dataPoint);
+        }
+        return result;
+    }
+
+    /** 将 time/value 列表转为以时间刻度为 key 的 Map(与原系统 pointVOMap 一致) */
+    private Map<Integer, TrendDataPoint> toTrendPointMap(List<TrendDataPoint> points, Integer dateType) {
+        Map<Integer, TrendDataPoint> map = new HashMap<>();
+        for (TrendDataPoint point : points) {
+            LocalDateTime parsed = parseTrendTimestamp(point.getTime());
+            if (parsed == null) {
+                continue;
+            }
+            int timeKey = getTrendTimeKey(dateType, parsed);
+            map.merge(timeKey, point, (existing, incoming) -> {
+                TrendDataPoint merged = new TrendDataPoint();
+                merged.setTime(existing.getTime());
+                merged.setValue(existing.getValue().add(incoming.getValue()));
+                return merged;
+            });
+        }
+        return map;
+    }
+
+    /** 构建时间轴 dateList(与原系统一致) */
+    private List<String> buildTrendDateList(Integer dateType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<String> dateList = new ArrayList<>();
+        LocalDateTime time = startTime;
+        while (true) {
             if (dateType == 1) {
-                curTime = curTime.plusHours(1);
-                oldTime = oldTime.plusHours(1);
+                if (time.getHour() > endTime.getHour()) {
+                    break;
+                }
             } else if (dateType == 2) {
-                curTime = curTime.plusDays(1);
-                oldTime = oldTime.plusDays(1);
+                if (time.getDayOfMonth() > endTime.getDayOfMonth()) {
+                    break;
+                }
+            } else if (dateType == 3) {
+                if (time.getMonthValue() > endTime.getMonthValue()) {
+                    break;
+                }
             } else {
-                curTime = curTime.plusMonths(1);
-                oldTime = oldTime.plusMonths(1);
+                break;
+            }
+            dateList.add(formatTsdbTime(time));
+            if (dateType == 1) {
+                time = time.plusHours(1);
+            } else if (dateType == 2) {
+                time = time.plusDays(1);
+            } else {
+                time = time.plusMonths(1);
+            }
+            if (time.isAfter(endTime)) {
+                break;
             }
-
-            index++;
         }
+        return dateList;
+    }
+
+    /**
+     * 与原系统 getListResult 一致:遍历 dateList,匹配 thisValue/oldValue/time/unit。
+     */
+    private List<Map<String, Object>> getTrendListResult(List<String> dateList,
+                                                         Map<Integer, TrendDataPoint> pointVOMap,
+                                                         Map<Integer, TrendDataPoint> pointOldMap,
+                                                         Integer dateType) {
+        List<Map<String, Object>> list = new ArrayList<>();
 
+        for (String date : dateList) {
+            Map<String, Object> map = new HashMap<>();
+            int time = parseTrendTimeKey(dateType, date);
+
+            TrendDataPoint pointVO = pointVOMap.get(time);
+            if (pointVO == null) {
+                map.put("thisValue", "-");
+            } else {
+                map.put("thisValue", pointVO.getValue());
+            }
+
+            TrendDataPoint pointOldVO = pointOldMap.get(time);
+            if (pointOldVO == null) {
+                map.put("oldValue", "-");
+            } else {
+                map.put("oldValue", pointOldVO.getValue());
+            }
+
+            map.put("time", time);
+            list.add(map);
+        }
         return list;
     }
 
+    private EmsEnergyItemCode resolveEnergyItemCode(String identifier) {
+        EmsEnergyItemCode itemCode = emsEnergyItemCodeMapper.selectOne(
+                Wrappers.lambdaQuery(EmsEnergyItemCode.class).eq(EmsEnergyItemCode::getCode, identifier));
+        if (itemCode == null) {
+            itemCode = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class).eq(EmsEnergyItemCode::getIdentifier, identifier));
+        }
+        return itemCode;
+    }
+
+    /** 趋势时序点(对应原系统 DataPointVO 的 time/value) */
+    private static class TrendDataPoint {
+        private String time;
+        private BigDecimal value;
+
+        public String getTime() {
+            return time;
+        }
+
+        public void setTime(String time) {
+            this.time = time;
+        }
+
+        public BigDecimal getValue() {
+            return value;
+        }
+
+        public void setValue(BigDecimal value) {
+            this.value = value;
+        }
+    }
+
+    private int parseTrendTimeKey(Integer dateType, String date) {
+        LocalDateTime time = parseTrendTimestamp(date);
+        return time != null ? getTrendTimeKey(dateType, time) : 0;
+    }
+
+    private LocalDateTime parseTrendTimestamp(String ts) {
+        if (StringUtils.isBlank(ts)) {
+            return null;
+        }
+        String normalized = ts.trim();
+        DateTimeFormatter[] formatters = {
+                TSDB_FLEXIBLE_TIME_FORMAT,
+                TSDB_TIME_FORMAT,
+                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
+                DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
+        };
+        for (DateTimeFormatter formatter : formatters) {
+            try {
+                return LocalDateTime.parse(normalized, formatter);
+            } catch (Exception ignored) {
+                // try next formatter
+            }
+        }
+        int dotIndex = normalized.indexOf('.');
+        if (dotIndex > 0) {
+            try {
+                return LocalDateTime.parse(normalized.substring(0, dotIndex),
+                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            } catch (Exception ignored) {
+                // fall through
+            }
+        }
+        return null;
+    }
+
+    private LocalDateTime getTrendEndTime(Integer dateType) {
+        if (dateType == 1) {
+            return LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
+        }
+        if (dateType == 2) {
+            return LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
+        }
+        if (dateType == 3) {
+            return LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfYear()), LocalTime.MAX);
+        }
+        return null;
+    }
+
+    private int getTrendTimeKey(Integer dateType, LocalDateTime time) {
+        if (dateType == 1) {
+            return time.getHour();
+        }
+        if (dateType == 2) {
+            return time.getDayOfMonth();
+        }
+        return time.getMonthValue();
+    }
+
     /**
      * 计算指定时间类型的开始时间
      * dateType: 1-日(当天 00:00:00)、2-月(当月第一天 00:00:00)、3-年(当年第一天 00:00:00)

+ 6 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/RemoteTsdbProxyService.java

@@ -94,4 +94,10 @@ public interface RemoteTsdbProxyService {
      */
     @PostMapping("/energyItemSum")
     EnergyItemSumResultVO sumEnergyItem(@RequestBody EnergyItemSumQueryVO requestVO);
+
+    /**
+     * 能耗分项趋势(按产品+时间范围返回 time/value 序列)
+     */
+    @PostMapping("/energyItemTrend")
+    EnergyItemTrendResultVO sumEnergyItemTrend(@RequestBody EnergyItemTrendQueryVO requestVO);
 }

+ 23 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemTrendPointVO.java

@@ -0,0 +1,23 @@
+package com.usky.demo.domain;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 能耗分项趋势单点数据(time + value)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EnergyItemTrendPointVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 时间点 */
+    private String time;
+
+    /** 用量值 */
+    private BigDecimal value;
+}

+ 33 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemTrendQueryVO.java

@@ -0,0 +1,33 @@
+package com.usky.demo.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 能耗分项趋势查询参数(TDengine 超级表按时间粒度聚合)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EnergyItemTrendQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 超级表字段名(差值计算列) */
+    private String identifier;
+
+    /** 超级表名 */
+    @JsonProperty("super_table")
+    private String superTable;
+
+    /** 开始时间(含) */
+    private String startTime;
+
+    /** 结束时间(不含) */
+    private String endTime;
+
+    /** 时间类型:1-小时、2-天、3-月 */
+    private Integer dateType;
+}

+ 20 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemTrendResultVO.java

@@ -0,0 +1,20 @@
+package com.usky.demo.domain;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 能耗分项趋势查询结果
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EnergyItemTrendResultVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<EnergyItemTrendPointVO> points = new ArrayList<>();
+}

+ 6 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/factory/RemoteTsdbProxyFallbackFactory.java

@@ -98,6 +98,12 @@ public class RemoteTsdbProxyFallbackFactory implements FallbackFactory<RemoteTsd
             {
                 throw new BusinessException("能耗分项汇总查询:" + throwable.getMessage());
             }
+
+            @Override
+            public EnergyItemTrendResultVO sumEnergyItemTrend(EnergyItemTrendQueryVO requestVO)
+            {
+                throw new BusinessException("能耗分项趋势查询:" + throwable.getMessage());
+            }
         };
     }
 }

+ 8 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/api/DataTsdbProxyControllerApi.java

@@ -167,4 +167,12 @@ public class DataTsdbProxyControllerApi implements RemoteTsdbProxyService {
         return queryTdengineDataService.sumEnergyItemDiff(requestVO);
     }
 
+    @Override
+    public EnergyItemTrendResultVO sumEnergyItemTrend(@RequestBody EnergyItemTrendQueryVO requestVO) {
+        if (!"taos".equals(sourcetype)) {
+            throw new BusinessException("当前数据源不支持能耗分项趋势查询");
+        }
+        return queryTdengineDataService.sumEnergyItemTrend(requestVO);
+    }
+
 }

+ 5 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/QueryTdengineDataService.java

@@ -43,4 +43,9 @@ public interface QueryTdengineDataService extends CrudService<QueryTdengineData>
      * 能耗分项汇总:按设备计算 LAST(identifier)-FIRST(identifier) 后求和
      */
     EnergyItemSumResultVO sumEnergyItemDiff(EnergyItemSumQueryVO requestVO);
+
+    /**
+     * 能耗分项趋势:按时间粒度 INTERVAL 聚合,返回 time/value 列表
+     */
+    EnergyItemTrendResultVO sumEnergyItemTrend(EnergyItemTrendQueryVO requestVO);
 }

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

@@ -355,6 +355,74 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
         return result;
     }
 
+    @Override
+    public EnergyItemTrendResultVO sumEnergyItemTrend(EnergyItemTrendQueryVO requestVO) {
+        if (requestVO == null) {
+            throw new BusinessException("查询参数不能为空");
+        }
+        String superTable = requestVO.getSuperTable();
+        String identifier = requestVO.getIdentifier();
+        String startTime = requestVO.getStartTime();
+        String endTime = requestVO.getEndTime();
+        Integer dateType = requestVO.getDateType();
+        if (StringUtils.isAnyBlank(superTable, identifier, startTime, endTime) || dateType == null) {
+            throw new BusinessException("super_table、identifier、startTime、endTime、dateType 均不能为空");
+        }
+        assertSqlIdentifier(superTable, "super_table");
+        assertSqlIdentifier(identifier, "identifier");
+        String interval = resolveTrendInterval(dateType);
+
+        String sql = "SELECT _wstart, SUM(diff) AS sum_diff FROM ("
+                + " SELECT _wstart, device_id, LAST(" + identifier + ") - FIRST(" + identifier + ") AS diff"
+                + " FROM " + superTable
+                + " WHERE ts >= '" + startTime + "' AND ts < '" + endTime + "'"
+                + " PARTITION BY device_id"
+                + " INTERVAL(" + interval + ")"
+                + ") AS trend_diff GROUP BY _wstart ORDER BY _wstart";
+
+        EnergyItemTrendResultVO result = new EnergyItemTrendResultVO();
+        List<EnergyItemTrendPointVO> points = new ArrayList<>();
+
+        try (Connection connection = dataSource.getConnection();
+             PreparedStatement stmt = connection.prepareStatement(sql);
+             ResultSet rs = stmt.executeQuery()) {
+            while (rs.next()) {
+                Object wstart = rs.getObject("_wstart");
+                if (wstart == null) {
+                    continue;
+                }
+                Object sumDiff = rs.getObject("sum_diff");
+                EnergyItemTrendPointVO point = new EnergyItemTrendPointVO();
+                point.setTime(wstart.toString());
+                if (sumDiff != null) {
+                    point.setValue(new BigDecimal(sumDiff.toString()));
+                } else {
+                    point.setValue(BigDecimal.ZERO);
+                }
+                points.add(point);
+            }
+        } catch (SQLException e) {
+            log.error("能耗分项趋势查询失败, sql=" + sql + ", " + e.getMessage());
+            throw new BusinessException("能耗分项趋势查询失败: " + e.getMessage());
+        }
+
+        result.setPoints(points);
+        return result;
+    }
+
+    private static String resolveTrendInterval(Integer dateType) {
+        if (dateType == 1) {
+            return "1h";
+        }
+        if (dateType == 2) {
+            return "1d";
+        }
+        if (dateType == 3) {
+            return "1n";
+        }
+        throw new BusinessException("dateType 仅支持 1-小时、2-天、3-月");
+    }
+
     private static void assertSqlIdentifier(String name, String paramName) {
         if (!SQL_IDENTIFIER_PATTERN.matcher(name).matches()) {
             throw new BusinessException(paramName + " 格式非法,仅允许字母、数字与下划线,且不能以数字开头");