Selaa lähdekoodia

3、开发能耗分项汇总查询接口;
4、优化分类能耗统计接口,调整请求参数和访问数据库处理逻辑;

james 1 päivä sitten
vanhempi
commit
f2d37c4138
15 muutettua tiedostoa jossa 465 lisäystä ja 52 poistoa
  1. 7 7
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsOverviewController.java
  2. 48 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java
  3. 9 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/DmpProductMapper.java
  4. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConversionFactorMapper.java
  5. 1 1
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java
  6. 208 44
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java
  7. 16 0
      service-ems/service-ems-biz/src/main/resources/mapper/ems/DmpProductMapper.xml
  8. 6 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/RemoteTsdbProxyService.java
  9. 30 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemSumQueryVO.java
  10. 22 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemSumResultVO.java
  11. 6 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/factory/RemoteTsdbProxyFallbackFactory.java
  12. 11 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/api/DataTsdbProxyControllerApi.java
  13. 34 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/web/QueryInfluxdbDataController.java
  14. 5 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/QueryTdengineDataService.java
  15. 52 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/impl/QueryTdengineDataServiceImpl.java

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

@@ -59,20 +59,20 @@ public class EmsOverviewController {
     }
 
     /**
-     * 分类能耗统计(模拟数据
+     * 分类能耗统计(按能源类型关联产品,调用 TSDB 分项汇总
      * 参数说明:
-     * - dateType:时间类型(1-日,2-月,3-年等,暂仅作为占位,不影响当前模拟结果
-     * - itemCode:能耗条目编码
-     * - energyType:能耗类型
-     * - projectId:项目 ID(可选,当前实现未做真实查询,仅占位
+     * - dateType:时间类型(1-日,2-月,3-年)
+     * - identifier:分项字段编码
+     * - energyType:能源类型(1电 2水 3冷 4热)
+     * - projectId:项目 ID(可选,不传则取当前租户第一个项目;用于查询折算系数
      */
     @GetMapping("/classification-energy")
     public ApiResult<java.util.Map<String, Object>> queryClassificationEnergy(
             @RequestParam Integer dateType,
-            @RequestParam String itemCode,
+            @RequestParam String identifier,
             @RequestParam Integer energyType,
             @RequestParam(required = false) Long projectId) {
-        return ApiResult.success(emsOverviewService.queryClassificationEnergy(dateType, itemCode, energyType, projectId));
+        return ApiResult.success(emsOverviewService.queryClassificationEnergy(dateType, identifier, energyType, projectId));
     }
 
     /**

+ 48 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java

@@ -0,0 +1,48 @@
+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;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 项目能源折算系数(ems_project_conversion_factor)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_project_conversion_factor")
+public class EmsProjectConversionFactor implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("project_id")
+    private Long projectId;
+
+    @TableField("energy_type")
+    private Integer energyType;
+
+    private String name;
+
+    private BigDecimal value;
+
+    @TableField("updated_by")
+    private String updatedBy;
+
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+
+    @TableField("created_by")
+    private String createdBy;
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 9 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/DmpProductMapper.java

@@ -2,10 +2,19 @@ package com.usky.ems.mapper;
 
 import com.usky.common.mybatis.core.CrudMapper;
 import com.usky.ems.domain.DmpProduct;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * 产品信息表 Mapper(dmp_product)
  */
 public interface DmpProductMapper extends CrudMapper<DmpProduct> {
+
+    /**
+     * 按租户与能源类型查询关联产品编码
+     */
+    List<String> selectProductCodesByEnergyType(@Param("tenantId") Integer tenantId,
+                                                @Param("energyType") Integer energyType);
 }
 

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConversionFactorMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsProjectConversionFactor;
+
+/**
+ * 项目能源折算系数 Mapper(ems_project_conversion_factor)
+ */
+public interface EmsProjectConversionFactorMapper extends CrudMapper<EmsProjectConversionFactor> {
+}

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

@@ -36,7 +36,7 @@ public interface EmsOverviewService {
      * 分类能耗统计(按时间维度、能耗类型等)
      * 先按 Map 结构返回,后续可以再封装 VO。
      */
-    Map<String, Object> queryClassificationEnergy(Integer dateType, String itemCode, Integer energyType, Long projectId);
+    Map<String, Object> queryClassificationEnergy(Integer dateType, String identifier, Integer energyType, Long projectId);
 
     /**
      * 能耗用能趋势(按时间维度:日/月/年)

+ 208 - 44
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java

@@ -1,10 +1,17 @@
 package com.usky.ems.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.core.bean.ApiResult;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.demo.RemoteTsdbProxyService;
+import com.usky.demo.domain.EnergyItemSumQueryVO;
+import com.usky.demo.domain.EnergyItemSumResultVO;
 import com.usky.ems.domain.*;
+import com.usky.ems.mapper.DmpProductMapper;
 import com.usky.ems.mapper.EmsDeviceMapper;
 import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.mapper.EmsProjectConversionFactorMapper;
 import com.usky.ems.mapper.EmsProjectDeviceSystemMapper;
 import com.usky.ems.mapper.EmsProjectMapper;
 import com.usky.ems.service.EmsModelService;
@@ -15,6 +22,7 @@ import com.usky.ems.service.vo.EmsOverviewDeviceSystemStatVO;
 import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsSummaryResponse;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -23,6 +31,7 @@ import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -38,6 +47,13 @@ import java.util.stream.Collectors;
 @Service
 public class EmsOverviewServiceImpl implements EmsOverviewService {
 
+    private static final DateTimeFormatter TSDB_TIME_FORMAT =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+    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";
+    private static final String FACTOR_NAME_CO2 = "co2";
+
     @Autowired
     private EmsModelService emsModelService;
 
@@ -53,9 +69,18 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     @Autowired
     private EmsProjectMapper emsProjectMapper;
 
+    @Autowired
+    private EmsProjectConversionFactorMapper emsProjectConversionFactorMapper;
+
     @Autowired
     private EmsProjectDeviceSystemMapper emsProjectDeviceSystemMapper;
 
+    @Autowired
+    private DmpProductMapper dmpProductMapper;
+
+    @Autowired
+    private RemoteTsdbProxyService remoteTsdbProxyService;
+
     @Override
     public EmsProjectResponse getProject(Integer projectId) {
         LambdaQueryWrapper<EmsProject> wrapper = new LambdaQueryWrapper<>();
@@ -192,54 +217,193 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     }
 
     /**
-     * 分类能耗统计(模拟数据版本)
-     * 说明:
-     * - 目前不接真实能耗库,仅按固定示例数据做一套完整计算逻辑,保证前端联调正常。
-     * - 返回结构与原系统保持一致:Map<String, Object>。
+     * 分类能耗统计:按能源类型关联产品汇总 TSDB 用量,并计算环比/同比及折标煤、碳排放。
      */
     @Override
-    public Map<String, Object> queryClassificationEnergy(Integer dateType, String itemCode, Integer energyType, Long projectId) {
-        Map<String, Object> result = new HashMap<>();
-
-        // 基础模拟数据:本期/环比/同比能耗(单位:kWh 或折算后的统一单位)
-        BigDecimal consume = new BigDecimal("100.00");
-        BigDecimal sequentialCon = new BigDecimal("80.00");   // 上一期
-        BigDecimal pariPassCon = new BigDecimal("90.00");     // 同期
-
-        // 模拟项目能耗折标系数、单价
-        BigDecimal coal = new BigDecimal("0.70");              // 折标系数(每单位能耗折算标煤)
-        BigDecimal unitPrice = new BigDecimal("1.00");         // 单价(每单位能耗金额)
-
-        // 本期费用 = 本期能耗 * 单价
-        BigDecimal cost = consume.multiply(unitPrice).setScale(2, RoundingMode.UP);
-        BigDecimal sequentialCost = sequentialCon.multiply(unitPrice).setScale(2, RoundingMode.UP);
-        BigDecimal pariPassCost = pariPassCon.multiply(unitPrice).setScale(2, RoundingMode.UP);
-
-        // 标煤量 = 能耗 * 折标系数
-        BigDecimal coalAmount = consume.multiply(coal).setScale(2, RoundingMode.UP);
-        BigDecimal sequentialCoal = sequentialCon.multiply(coal).setScale(2, RoundingMode.UP);
-        BigDecimal pariPassCoal = pariPassCon.multiply(coal).setScale(2, RoundingMode.UP);
-
-        // CO2 排放量 = 标煤量 * 系数 2.4589(固定常量,模拟用)
-        BigDecimal co2Factor = new BigDecimal("2.4589");
+    public Map<String, Object> queryClassificationEnergy(Integer dateType, String identifier, Integer energyType, Long projectId) {
+        if (dateType == null || StringUtils.isBlank(identifier) || energyType == null) {
+            return null;
+        }
+
+        Long resolvedProjectId = resolveProjectId(projectId);
+        Map<String, BigDecimal> factorMap = loadConversionFactorMap(resolvedProjectId, energyType);
+        BigDecimal coalFactor = factorMap.getOrDefault(FACTOR_NAME_COAL, FALLBACK_COAL_FACTOR);
+        BigDecimal co2Factor = factorMap.getOrDefault(FACTOR_NAME_CO2, FALLBACK_CO2_FACTOR);
+
+        List<String> productCodeList = dmpProductMapper.selectProductCodesByEnergyType(
+                SecurityUtils.getTenantId(), energyType);
+        if (productCodeList == null || productCodeList.isEmpty()) {
+            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, startTime, endTime);
+
+        LocalDateTime pariPassuEndTime = endTime.minusYears(1);
+        LocalDateTime pariPassuStartTime = getStartTime(dateType, pariPassuEndTime);
+        BigDecimal pariPassuData = sumEnergyByProductCodes(productCodeList, identifier,
+                pariPassuStartTime, pariPassuEndTime);
+
+        LocalDateTime sequentialEndTime;
+        if (dateType == 1) {
+            sequentialEndTime = endTime.minusDays(1);
+        } else if (dateType == 2) {
+            sequentialEndTime = endTime.minusMonths(1);
+        } else {
+            sequentialEndTime = endTime.minusYears(1);
+        }
+        LocalDateTime sequentialStartTime = getStartTime(dateType, sequentialEndTime);
+        BigDecimal sequentialData = sumEnergyByProductCodes(productCodeList, identifier,
+                sequentialStartTime, sequentialEndTime);
+
+        return buildClassificationResult(data, sequentialData, pariPassuData, coalFactor, co2Factor);
+    }
+
+    /**
+     * 解析当前租户项目 ID(未传则取第一个项目)
+     */
+    private Long resolveProjectId(Long projectId) {
+        LambdaQueryWrapper<EmsProject> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(EmsProject::getTenantId, SecurityUtils.getTenantId());
+        if (projectId != null) {
+            wrapper.eq(EmsProject::getId, projectId);
+            EmsProject project = emsProjectMapper.selectOne(wrapper);
+            return project != null ? project.getId() : null;
+        }
+        List<EmsProject> list = emsProjectMapper.selectList(wrapper);
+        return list.isEmpty() ? null : list.get(0).getId();
+    }
+
+    /**
+     * 加载折算系数:优先项目专属配置,否则退回 project_id=0 的公共配置
+     */
+    private Map<String, BigDecimal> loadConversionFactorMap(Long projectId, Integer energyType) {
+        LambdaQueryWrapper<EmsProjectConversionFactor> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(EmsProjectConversionFactor::getEnergyType, energyType);
+        if (projectId != null) {
+            wrapper.in(EmsProjectConversionFactor::getProjectId, projectId, 0L);
+        } else {
+            wrapper.eq(EmsProjectConversionFactor::getProjectId, 0L);
+        }
+        List<EmsProjectConversionFactor> factorList = emsProjectConversionFactorMapper.selectList(wrapper);
+        if (factorList == null || factorList.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<EmsProjectConversionFactor> effectiveFactors = factorList;
+        if (projectId != null) {
+            effectiveFactors = factorList.stream()
+                    .filter(f -> Objects.equals(f.getProjectId(), projectId))
+                    .collect(Collectors.toList());
+            if (effectiveFactors.isEmpty()) {
+                effectiveFactors = factorList;
+            }
+        }
+        return effectiveFactors.stream()
+                .filter(f -> f.getName() != null && f.getValue() != null)
+                .collect(Collectors.toMap(EmsProjectConversionFactor::getName,
+                        EmsProjectConversionFactor::getValue, (oldVal, newVal) -> oldVal));
+    }
+
+    private BigDecimal sumEnergyByProductCodes(List<String> productCodeList, String fieldIdentifier,
+                                               LocalDateTime startTime, LocalDateTime endTime) {
+        BigDecimal total = BigDecimal.ZERO;
+        String start = formatTsdbTime(startTime);
+        String end = formatTsdbTime(endTime);
+        for (String productCode : productCodeList) {
+            if (StringUtils.isBlank(productCode)) {
+                continue;
+            }
+            EnergyItemSumQueryVO queryVO = new EnergyItemSumQueryVO();
+            queryVO.setIdentifier(fieldIdentifier);
+            queryVO.setSuperTable("super_" + productCode.trim());
+            queryVO.setStartTime(start);
+            queryVO.setEndTime(end);
+            ApiResult<EnergyItemSumResultVO> apiResult = remoteTsdbProxyService.sumEnergyItem(queryVO);
+            if (apiResult == null || apiResult.getData() == null || apiResult.getData().getSumDiff() == null) {
+                continue;
+            }
+            total = total.add(apiResult.getData().getSumDiff());
+        }
+        return total;
+    }
+
+    private Map<String, Object> buildClassificationResult(BigDecimal data, BigDecimal sequentialData,
+                                                          BigDecimal pariPassuData,
+                                                          BigDecimal coal, BigDecimal co2Factor) {
+        if (data == null) {
+            data = BigDecimal.ZERO;
+        }
+        if (sequentialData == null) {
+            sequentialData = BigDecimal.ZERO;
+        }
+        if (pariPassuData == null) {
+            pariPassuData = BigDecimal.ZERO;
+        }
+
+        BigDecimal coalAmount = data.multiply(coal).setScale(2, RoundingMode.UP);
         BigDecimal co2Amount = coalAmount.multiply(co2Factor).setScale(2, RoundingMode.UP);
-        BigDecimal sequentialCo2 = sequentialCoal.multiply(co2Factor).setScale(2, RoundingMode.UP);
-        BigDecimal pariPassCo2 = pariPassCoal.multiply(co2Factor).setScale(2, RoundingMode.UP);
-
-        result.put("consume", consume);
-        result.put("sequentialCon", sequentialCon);
-        result.put("pariPassCon", pariPassCon);
-        result.put("cost", cost);
-        result.put("sequentialCost", sequentialCost);
-        result.put("pariPassCost", pariPassCost);
-        result.put("coalAmount", coalAmount);
-        result.put("sequentialCoal", sequentialCoal);
-        result.put("pariPassCoal", pariPassCoal);
-        result.put("co2Amount", co2Amount);
-        result.put("sequentialCo2", sequentialCo2);
-        result.put("pariPassCo2", pariPassCo2);
 
-        return result;
+        BigDecimal sequentialCon = BigDecimal.ZERO;
+        BigDecimal sequentialCost = BigDecimal.ZERO;
+        BigDecimal sequentialCoal = BigDecimal.ZERO;
+        BigDecimal sequentialCo2 = BigDecimal.ZERO;
+        if (sequentialData.compareTo(BigDecimal.ZERO) != 0) {
+            BigDecimal sequentialCoalAmountData = sequentialData.multiply(coal).setScale(2, RoundingMode.UP);
+            BigDecimal sequentialCo2AmountData = sequentialCoalAmountData.multiply(co2Factor)
+                    .setScale(2, RoundingMode.UP);
+            sequentialCon = data.subtract(sequentialData).divide(sequentialData, 2, RoundingMode.UP);
+            sequentialCoal = coalAmount.subtract(sequentialCoalAmountData)
+                    .divide(sequentialCoalAmountData, 2, RoundingMode.UP);
+            sequentialCo2 = co2Amount.subtract(sequentialCo2AmountData)
+                    .divide(sequentialCo2AmountData, 2, RoundingMode.UP);
+        }
+
+        BigDecimal pariPassCon = BigDecimal.ZERO;
+        BigDecimal pariPassCost = BigDecimal.ZERO;
+        BigDecimal pariPassCoal = BigDecimal.ZERO;
+        BigDecimal pariPassCo2 = BigDecimal.ZERO;
+        if (pariPassuData.compareTo(BigDecimal.ZERO) != 0) {
+            BigDecimal pariPassCoalAmountData = pariPassuData.multiply(coal).setScale(2, RoundingMode.UP);
+            BigDecimal pariPassCo2AmountData = pariPassCoalAmountData.multiply(co2Factor)
+                    .setScale(2, RoundingMode.UP);
+            pariPassCon = data.subtract(pariPassuData).divide(pariPassuData, 2, RoundingMode.UP);
+            pariPassCoal = coalAmount.subtract(pariPassCoalAmountData)
+                    .divide(pariPassCoalAmountData, 2, RoundingMode.UP);
+            pariPassCo2 = co2Amount.subtract(pariPassCo2AmountData)
+                    .divide(pariPassCo2AmountData, 2, RoundingMode.UP);
+        }
+
+        BigDecimal cost = BigDecimal.ZERO;
+        if (co2Amount.compareTo(BigDecimal.ZERO) > 0) {
+            cost = co2Amount.divide(new BigDecimal("18"), 2, RoundingMode.UP);
+        }
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("consume", data);
+        map.put("sequentialCon", sequentialCon);
+        map.put("pariPassCon", pariPassCon);
+        map.put("sequential", sequentialData);
+        map.put("pariPass", pariPassuData);
+        map.put("cost", cost);
+        map.put("sequentialCost", sequentialCost);
+        map.put("pariPassCost", pariPassCost);
+        map.put("coalAmount", coalAmount);
+        map.put("sequentialCoal", sequentialCoal);
+        map.put("pariPassCoal", pariPassCoal);
+        map.put("co2Amount", co2Amount);
+        map.put("sequentialCo2", sequentialCo2);
+        map.put("pariPassCo2", pariPassCo2);
+        return map;
+    }
+
+    private static String formatTsdbTime(LocalDateTime time) {
+        return time.format(TSDB_TIME_FORMAT);
     }
 
     /**

+ 16 - 0
service-ems/service-ems-biz/src/main/resources/mapper/ems/DmpProductMapper.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.DmpProductMapper">
+
+    <select id="selectProductCodesByEnergyType" resultType="java.lang.String">
+        SELECT dp.product_code
+        FROM dmp_product dp
+        INNER JOIN ems_product_energy_type ep ON dp.id = ep.product_id
+        WHERE dp.tenant_id = #{tenantId}
+          AND dp.delete_flag = 0
+          AND ep.energy_type = #{energyType}
+          AND dp.product_code IS NOT NULL
+          AND dp.product_code != ''
+    </select>
+
+</mapper>

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

@@ -88,4 +88,10 @@ public interface RemoteTsdbProxyService {
      */
     @PostMapping("/dropSuperTableColumn")
     Void dropSuperTableColumn(@RequestBody SuperTableDTO superTableDTO);
+
+    /**
+     * 能耗分项汇总(TDengine 超级表 LAST-FIRST 按设备聚合后求和)
+     */
+    @PostMapping("/energyItemSum")
+    ApiResult<EnergyItemSumResultVO> sumEnergyItem(@RequestBody EnergyItemSumQueryVO requestVO);
 }

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

@@ -0,0 +1,30 @@
+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 EnergyItemSumQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 超级表字段名(差值计算列) */
+    private String identifier;
+
+    /** 超级表名 */
+    @JsonProperty("super_table")
+    private String superTable;
+
+    /** 开始时间(含),如 2024-01-01 00:00:00.000 */
+    private String startTime;
+
+    /** 结束时间(不含),如 2024-01-02 00:00:00.000 */
+    private String endTime;
+}

+ 22 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/EnergyItemSumResultVO.java

@@ -0,0 +1,22 @@
+package com.usky.demo.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 能耗分项汇总查询结果
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EnergyItemSumResultVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 各设备区间用量差值之和 */
+    @JsonProperty("sum_diff")
+    private BigDecimal sumDiff;
+}

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

@@ -92,6 +92,12 @@ public class RemoteTsdbProxyFallbackFactory implements FallbackFactory<RemoteTsd
             {
                 throw new BusinessException("删除超级表列:" + throwable.getMessage());
             }
+
+            @Override
+            public ApiResult<EnergyItemSumResultVO> sumEnergyItem(EnergyItemSumQueryVO requestVO)
+            {
+                throw new BusinessException("能耗分项汇总查询:" + throwable.getMessage());
+            }
         };
     }
 }

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

@@ -156,4 +156,15 @@ public class DataTsdbProxyControllerApi implements RemoteTsdbProxyService {
         return null;
     }
 
+    /**
+     * 能耗分项汇总(对外 Feign)
+     */
+    @Override
+    public ApiResult<EnergyItemSumResultVO> sumEnergyItem(@RequestBody EnergyItemSumQueryVO requestVO) {
+        if (!"taos".equals(sourcetype)) {
+            throw new BusinessException("当前数据源不支持能耗分项汇总查询");
+        }
+        return ApiResult.success(queryTdengineDataService.sumEnergyItemDiff(requestVO));
+    }
+
 }

+ 34 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/web/QueryInfluxdbDataController.java

@@ -4,6 +4,7 @@ package com.usky.demo.controller.web;
 import cn.hutool.core.util.StrUtil;
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.domain.R;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.demo.constant.TdsConstants;
 import com.usky.demo.domain.*;
 import com.usky.demo.service.QueryInfluxdbDataService;
@@ -150,5 +151,38 @@ public class QueryInfluxdbDataController {
         return ApiResult.success(superTableDescribeVOS);
     }
 
+    /**
+     * 能耗分项汇总(按超级表、设备维度 LAST-FIRST 后求和)
+     */
+    @ApiOperation("能耗分项汇总查询")
+    @PostMapping("/energy-item/sum")
+    public ApiResult<EnergyItemSumResultVO> sumEnergyItem(@RequestBody EnergyItemSumQueryVO requestVO) {
+        if (!"taos".equals(sourcetype)) {
+            throw new BusinessException("当前数据源不支持能耗分项汇总查询");
+        }
+        return ApiResult.success(queryTdengineDataService.sumEnergyItemDiff(requestVO));
+    }
+
+    /**
+     * 能耗分项汇总(GET,便于调试)
+     */
+    @ApiOperation("能耗分项汇总查询(GET)")
+    @GetMapping("/energy-item/sum")
+    public ApiResult<EnergyItemSumResultVO> sumEnergyItemGet(
+            @RequestParam String identifier,
+            @RequestParam("super_table") String superTable,
+            @RequestParam String startTime,
+            @RequestParam String endTime) {
+        if (!"taos".equals(sourcetype)) {
+            throw new BusinessException("当前数据源不支持能耗分项汇总查询");
+        }
+        EnergyItemSumQueryVO requestVO = new EnergyItemSumQueryVO();
+        requestVO.setIdentifier(identifier);
+        requestVO.setSuperTable(superTable);
+        requestVO.setStartTime(startTime);
+        requestVO.setEndTime(endTime);
+        return ApiResult.success(queryTdengineDataService.sumEnergyItemDiff(requestVO));
+    }
+
 }
 

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

@@ -38,4 +38,9 @@ public interface QueryTdengineDataService extends CrudService<QueryTdengineData>
     void dropSuperTableTag(String superTableName, Fields fields);
 
     List<SuperTableDescribeVO> describeSuperOrSubTable(String tableName);
+
+    /**
+     * 能耗分项汇总:按设备计算 LAST(identifier)-FIRST(identifier) 后求和
+     */
+    EnergyItemSumResultVO sumEnergyItemDiff(EnergyItemSumQueryVO requestVO);
 }

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

@@ -25,10 +25,12 @@ import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 
 import javax.sql.DataSource;
+import java.math.BigDecimal;
 import java.sql.*;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -42,6 +44,9 @@ import java.util.stream.Collectors;
 @Service
 @DS("taos")
 public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdengineDataMapper, QueryTdengineData> implements QueryTdengineDataService {
+
+    private static final Pattern SQL_IDENTIFIER_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
+
     @Autowired
     private TsdbUtils tsdbUtils;
     @Autowired
@@ -308,4 +313,51 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
     public void dropSuperTableTag(String superTableName, Fields fields) {
         queryTdengineDataMapper.dropSuperTableTag(superTableName, fields);
     }
+
+    @Override
+    public EnergyItemSumResultVO sumEnergyItemDiff(EnergyItemSumQueryVO requestVO) {
+        if (requestVO == null) {
+            throw new BusinessException("查询参数不能为空");
+        }
+        String superTable = requestVO.getSuperTable();
+        String identifier = requestVO.getIdentifier();
+        String startTime = requestVO.getStartTime();
+        String endTime = requestVO.getEndTime();
+        if (StringUtils.isAnyBlank(superTable, identifier, startTime, endTime)) {
+            throw new BusinessException("super_table、identifier、startTime、endTime 均不能为空");
+        }
+        assertSqlIdentifier(superTable, "super_table");
+        assertSqlIdentifier(identifier, "identifier");
+
+        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 + "'"
+                + " GROUP BY device_id"
+                + ") AS sub_table_diff";
+
+        EnergyItemSumResultVO result = new EnergyItemSumResultVO();
+        result.setSumDiff(BigDecimal.ZERO);
+
+        try (Connection connection = dataSource.getConnection();
+             PreparedStatement stmt = connection.prepareStatement(sql);
+             ResultSet rs = stmt.executeQuery()) {
+            if (rs.next()) {
+                Object value = rs.getObject("sum_diff");
+                if (value != null) {
+                    result.setSumDiff(new BigDecimal(value.toString()));
+                }
+            }
+        } catch (SQLException e) {
+            log.error("能耗分项汇总查询失败, sql=" + sql + ", " + e.getMessage());
+            throw new BusinessException("能耗分项汇总查询失败: " + e.getMessage());
+        }
+        return result;
+    }
+
+    private static void assertSqlIdentifier(String name, String paramName) {
+        if (!SQL_IDENTIFIER_PATTERN.matcher(name).matches()) {
+            throw new BusinessException(paramName + " 格式非法,仅允许字母、数字与下划线,且不能以数字开头");
+        }
+    }
 }