Просмотр исходного кода

[0917] 短期预测报表-优化预测功率汇总统计方式,避免内存过大

taosj 1 месяц назад
Родитель
Сommit
f392d0b655

+ 122 - 119
forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/ShortTermPowerForecastReportServiceImpl.java

@@ -27,6 +27,7 @@ import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportQuery;
 import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportVO;
 import com.ufo.project.data.service.IShortTermPowerForecastReportService;
 import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastIssued;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
 import com.ufo.project.forecast.shortterm.power.service.IDataShortTermPowerForecastIssuedService;
 import com.ufo.project.model.domain.DataElectricityLocal;
 import com.ufo.project.model.service.IDataElectricityLocalService;
@@ -85,8 +86,9 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         // 2. 获取预测功率数据
         long utcStartTime = parseLong(startTime);
         long utcEndTime = parseLong(endTime);
-        List<DataShortTermPowerForecastIssued> forecastList = powerForecastIssuedService.selectByTimeRange(
+        List<ForecastPowerAverage> forecastList = powerForecastIssuedService.selectAveragePowerByTimeRange(
                 entityId, utcStartTime, utcEndTime);
+        this.dataShortTermPowerForecastIssued2Gmt8(forecastList);
 
         // 3. 按基准类型分组
         switch (type)
@@ -102,21 +104,32 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         }
     }
 
+    private void dataShortTermPowerForecastIssued2Gmt8(List<ForecastPowerAverage> forecastList)
+    {
+        for (ForecastPowerAverage record : forecastList)
+        {
+            ZonedDateTime forecastZdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(record.getForecastTime()),
+                    ZoneId.systemDefault());
+            ZonedDateTime forecastGmt8 = forecastZdt.withZoneSameInstant(ZONE_SHANGHAI);
+            record.setForecastTime(forecastGmt8.toEpochSecond());
+        }
+    }
+
     /**
      * 构建日报
      */
     private List<ShortTermPowerForecastReportVO> buildDailyReport(List<DataElectricityLocal> actualList,
-            List<DataShortTermPowerForecastIssued> forecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
-        Map<String, List<DataShortTermPowerForecastIssued>> forecastDailyMap = groupForecastByDay(forecastList);
+        Map<String, List<ForecastPowerAverage>> forecastDailyMap = groupForecastByDay(forecastList);
 
         List<ShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : dailyMap.entrySet())
         {
             String day = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataShortTermPowerForecastIssued> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
+            List<ForecastPowerAverage> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
             vo.setTime(day);
@@ -130,17 +143,17 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
      * 构建月报
      */
     private List<ShortTermPowerForecastReportVO> buildMonthlyReport(List<DataElectricityLocal> actualList,
-            List<DataShortTermPowerForecastIssued> forecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
-        Map<String, List<DataShortTermPowerForecastIssued>> forecastMonthlyMap = groupForecastByMonth(forecastList);
+        Map<String, List<ForecastPowerAverage>> forecastMonthlyMap = groupForecastByMonth(forecastList);
 
         List<ShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : monthlyMap.entrySet())
         {
             String month = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataShortTermPowerForecastIssued> forecastRecords =
+            List<ForecastPowerAverage> forecastRecords =
                     forecastMonthlyMap.getOrDefault(month, Collections.emptyList());
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
@@ -155,17 +168,17 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
      * 构建年报
      */
     private List<ShortTermPowerForecastReportVO> buildYearlyReport(List<DataElectricityLocal> actualList,
-            List<DataShortTermPowerForecastIssued> forecastList)
+            List<ForecastPowerAverage> forecastList)
     {
         Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
-        Map<String, List<DataShortTermPowerForecastIssued>> forecastYearlyMap = groupForecastByYear(forecastList);
+        Map<String, List<ForecastPowerAverage>> forecastYearlyMap = groupForecastByYear(forecastList);
 
         List<ShortTermPowerForecastReportVO> result = new ArrayList<>();
         for (Entry<String, List<DataElectricityLocal>> entry : yearlyMap.entrySet())
         {
             String year = entry.getKey();
             List<DataElectricityLocal> records = entry.getValue();
-            List<DataShortTermPowerForecastIssued> forecastRecords =
+            List<ForecastPowerAverage> forecastRecords =
                     forecastYearlyMap.getOrDefault(year, Collections.emptyList());
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
@@ -180,8 +193,11 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
      * 计算指标(日报/月报/年报通用)
      */
     private void computeDailyMetrics(ShortTermPowerForecastReportVO vo,
-            List<DataElectricityLocal> actualRecords, List<DataShortTermPowerForecastIssued> forecastRecords)
+            List<DataElectricityLocal> actualRecords, List<ForecastPowerAverage> forecastRecords)
     {
+        // 预先构建预测数据映射(避免各方法重复调用 buildForecastMap)
+        Map<Long, BigDecimal> forecastMap = buildForecastMap(forecastRecords);
+
         // 1. operatingCapacity: 筛选 device_status = 0 AND grid_status = 1, MAX(actual_power) / 1000
         BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
         vo.setOperatingCapacity(operatingCapacity);
@@ -191,23 +207,23 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         vo.setActualPower(actualPower);
 
         // 3. forecastPower: AVG(power) / 1000
-        BigDecimal forecastPower = computeForecastPower(forecastRecords);
+        BigDecimal forecastPower = computeForecastPower(forecastMap);
         vo.setForecastPower(forecastPower);
 
         // 4. RMSE
-        BigDecimal rmse = computeRmse(actualRecords, forecastRecords);
+        BigDecimal rmse = computeRmse(actualRecords, forecastMap);
         vo.setRootMeanSquareError(rmse);
 
         // 5. MAE
-        BigDecimal mae = computeMae(actualRecords, forecastRecords);
+        BigDecimal mae = computeMae(actualRecords, forecastMap);
         vo.setMeanAbsoluteError(mae);
 
         // 6. correlationCoefficientR
-        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords);
+        BigDecimal corr = computeCorrelation(actualRecords, forecastMap);
         vo.setCorrelationCoefficientR(corr);
 
         // 7. maximumErrorPassRate: 基于逐点校验的合格率
-        BigDecimal passRate = computeMaximumErrorPassRate(actualRecords, forecastRecords, operatingCapacity);
+        BigDecimal passRate = computeMaximumErrorPassRate(actualRecords, forecastMap, operatingCapacity);
         vo.setMaximumErrorPassRate(passRate);
     }
 
@@ -261,62 +277,43 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 计算预测功率
      */
-    private BigDecimal computeForecastPower(List<DataShortTermPowerForecastIssued> records)
+    private BigDecimal computeForecastPower(Map<Long, BigDecimal> forecastMap)
     {
-        BigDecimal sum = BigDecimal.ZERO;
-        int count = 0;
-        for (DataShortTermPowerForecastIssued r : records)
-        {
-            String power = r.getPower();
-            if (StringUtils.isNotEmpty(power))
-            {
-                try
-                {
-                    sum = sum.add(new BigDecimal(power.trim()));
-                    count++;
-                }
-                catch (NumberFormatException e)
-                {
-                    // skip
-                }
-            }
-        }
-        if (count == 0)
+        if (forecastMap == null || forecastMap.isEmpty())
         {
             return BigDecimal.ZERO;
         }
-        return sum.divide(new BigDecimal(1000)).divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
+        BigDecimal sum = forecastMap.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal avg = sum.divide(BigDecimal.valueOf(forecastMap.size()), 10, RoundingMode.HALF_UP);
+        return avg.divide(new BigDecimal(1000), 3, RoundingMode.HALF_UP);
     }
 
     /**
      * 计算RMSE
      */
     private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
-            List<DataShortTermPowerForecastIssued> forecastRecords)
+            Map<Long, BigDecimal> forecastMap)
     {
-        // 按时间戳聚合预测数据(以分钟级时间戳为key)
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
-
         if (forecastMap.isEmpty())
         {
             return BigDecimal.ZERO;
         }
 
-        double sumSquaredError = 0.0;
+        BigDecimal sumSquaredError = BigDecimal.ZERO;
         int count = 0;
+        BigDecimal thousand = BigDecimal.valueOf(1000);
 
         for (DataElectricityLocal r : actualRecords)
         {
-            if (r.getTm() == null) continue;
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
             Long ts = r.getTm().getTime() / 1000;
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
-            if (ap == null) continue;
-
-            double diffMW = (ap - fp) / 1000.0;
-            sumSquaredError += diffMW * diffMW;
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
+            BigDecimal diffMW = ap.subtract(fp).divide(thousand, 10, RoundingMode.HALF_UP);
+            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
             count++;
         }
 
@@ -325,7 +322,9 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
             return BigDecimal.ZERO;
         }
 
-        double rmse = Math.sqrt(sumSquaredError / count);
+        BigDecimal meanSquaredError = sumSquaredError.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP);
+        // 用 Math.sqrt 开方(平方根无法用 BigDecimal 精确表示,转为 double 计算后格式化)
+        double rmse = Math.sqrt(meanSquaredError.doubleValue());
         return BigDecimal.valueOf(rmse).setScale(3, RoundingMode.HALF_UP);
     }
 
@@ -333,30 +332,28 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
      * 计算MAE
      */
     private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
-            List<DataShortTermPowerForecastIssued> forecastRecords)
+            Map<Long, BigDecimal> forecastMap)
     {
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
-
         if (forecastMap.isEmpty())
         {
             return BigDecimal.ZERO;
         }
 
-        double sumAbsError = 0.0;
+        BigDecimal sumAbsError = BigDecimal.ZERO;
         int count = 0;
+        BigDecimal thousand = BigDecimal.valueOf(1000);
 
         for (DataElectricityLocal r : actualRecords)
         {
-            if (r.getTm() == null) continue;
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
             Long ts = r.getTm().getTime() / 1000;
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
-            if (ap == null) continue;
-
-            double absErrorMW = Math.abs(ap - fp) / 1000.0;
-            sumAbsError += absErrorMW;
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
+            BigDecimal absErrorMW = ap.subtract(fp).abs().divide(thousand, 10, RoundingMode.HALF_UP);
+            sumAbsError = sumAbsError.add(absErrorMW);
             count++;
         }
 
@@ -365,39 +362,38 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
             return BigDecimal.ZERO;
         }
 
-        double mae = sumAbsError / count;
-        return BigDecimal.valueOf(mae).setScale(3, RoundingMode.HALF_UP);
+        BigDecimal mae = sumAbsError.divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
+        return mae;
     }
 
     /**
      * 计算相关性系数 R
      */
     private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
-            List<DataShortTermPowerForecastIssued> forecastRecords)
+            Map<Long, BigDecimal> forecastMap)
     {
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
-
         if (forecastMap.isEmpty())
         {
             return BigDecimal.ZERO;
         }
 
-        // 收集成对数据
-        List<Double> xList = new ArrayList<>();
-        List<Double> yList = new ArrayList<>();
+        // 收集成对数据(MW)
+        List<BigDecimal> xList = new ArrayList<>();
+        List<BigDecimal> yList = new ArrayList<>();
+        BigDecimal thousand = BigDecimal.valueOf(1000);
 
         for (DataElectricityLocal r : actualRecords)
         {
-            if (r.getTm() == null) continue;
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
             Long ts = r.getTm().getTime() / 1000;
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            Double ap = r.getActualPower();
-            if (ap == null) continue;
-
-            xList.add(ap / 1000.0);
-            yList.add(fp / 1000.0);
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower()).divide(thousand, 10, RoundingMode.HALF_UP);
+            BigDecimal fy = fp.divide(thousand, 10, RoundingMode.HALF_UP);
+            xList.add(ap);
+            yList.add(fy);
         }
 
         int n = xList.size();
@@ -406,31 +402,36 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
             return BigDecimal.ZERO;
         }
 
+        BigDecimal nBd = BigDecimal.valueOf(n);
         // 计算均值
-        double meanX = xList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
-        double meanY = yList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+        BigDecimal sumX = xList.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal sumY = yList.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal meanX = sumX.divide(nBd, 10, RoundingMode.HALF_UP);
+        BigDecimal meanY = sumY.divide(nBd, 10, RoundingMode.HALF_UP);
 
-        // 计算相关系数
-        double numerator = 0.0;
-        double sumSqX = 0.0;
-        double sumSqY = 0.0;
+        // 计算相关系数:逐步累加,保持精度
+        BigDecimal numerator = BigDecimal.ZERO;
+        BigDecimal sumSqX = BigDecimal.ZERO;
+        BigDecimal sumSqY = BigDecimal.ZERO;
 
         for (int i = 0; i < n; i++)
         {
-            double dx = xList.get(i) - meanX;
-            double dy = yList.get(i) - meanY;
-            numerator += dx * dy;
-            sumSqX += dx * dx;
-            sumSqY += dy * dy;
+            BigDecimal dx = xList.get(i).subtract(meanX);
+            BigDecimal dy = yList.get(i).subtract(meanY);
+            numerator = numerator.add(dx.multiply(dy));
+            sumSqX = sumSqX.add(dx.multiply(dx));
+            sumSqY = sumSqY.add(dy.multiply(dy));
         }
 
-        double denominator = Math.sqrt(sumSqX * sumSqY);
-        if (denominator == 0.0)
+        BigDecimal denominatorSquared = sumSqX.multiply(sumSqY);
+        if (denominatorSquared.compareTo(BigDecimal.ZERO) == 0)
         {
             return BigDecimal.ZERO;
         }
 
-        double r = numerator / denominator;
+        // 用 Math.sqrt 开方(平方根无法用 BigDecimal 精确表示)
+        double denominator = Math.sqrt(denominatorSquared.doubleValue());
+        double r = numerator.doubleValue() / denominator;
         return BigDecimal.valueOf(r).setScale(3, RoundingMode.HALF_UP);
     }
 
@@ -438,7 +439,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
      * 计算最大误差合格率
      */
     private BigDecimal computeMaximumErrorPassRate(List<DataElectricityLocal> actualRecords,
-            List<DataShortTermPowerForecastIssued> forecastRecords,
+            Map<Long, BigDecimal> forecastMap,
             BigDecimal operatingCapacity)
     {
         if (operatingCapacity == null || operatingCapacity.compareTo(BigDecimal.ZERO) == 0)
@@ -452,65 +453,67 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
             return BigDecimal.ZERO;
         }
 
-        double thresholdValue;
+        BigDecimal thresholdValue;
         try
         {
-            thresholdValue = Double.parseDouble(threshold);
+            thresholdValue = new BigDecimal(threshold.trim());
         }
         catch (NumberFormatException e)
         {
             return BigDecimal.ZERO;
         }
 
-        double capacityMW = operatingCapacity.doubleValue();
-        double passCount = 0;
-        double totalCount = 0;
-
-        // 基于时间戳逐点校验:ABS(actual - forecast) / 开机容量 <= 阈值
-        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+        BigDecimal passCount = BigDecimal.ZERO;
+        BigDecimal totalCount = BigDecimal.ZERO;
+        BigDecimal thousand = BigDecimal.valueOf(1000);
 
         for (DataElectricityLocal r : actualRecords)
         {
             if (r.getTm() == null || r.getActualPower() == null) continue;
 
             Long ts = r.getTm().getTime() / 1000;
-            Double fp = forecastMap.get(ts);
+            BigDecimal fp = forecastMap.get(ts);
             if (fp == null) continue;
 
-            totalCount++;
-            double errorMW = Math.abs(r.getActualPower() - fp) / 1000.0;
-            if (errorMW <= thresholdValue * capacityMW)
+            totalCount = totalCount.add(BigDecimal.ONE);
+            BigDecimal errorMW = BigDecimal.valueOf(r.getActualPower())
+                    .subtract(fp)
+                    .abs()
+                    .divide(thousand, 10, RoundingMode.HALF_UP);
+            BigDecimal allowed = thresholdValue.multiply(operatingCapacity);
+            if (errorMW.compareTo(allowed) <= 0)
             {
-                passCount++;
+                passCount = passCount.add(BigDecimal.ONE);
             }
         }
 
-        if (totalCount == 0)
+        if (totalCount.compareTo(BigDecimal.ZERO) == 0)
         {
             return BigDecimal.ZERO;
         }
 
-        double rate = (passCount / totalCount) * 100.0;
-        return BigDecimal.valueOf(rate).setScale(3, RoundingMode.HALF_UP);
+        BigDecimal rate = passCount.divide(totalCount, 10, RoundingMode.HALF_UP)
+                .multiply(BigDecimal.valueOf(100));
+        return rate.setScale(3, RoundingMode.HALF_UP);
     }
 
     /**
      * 构建预测数据映射:时间戳(秒) -> power(kW)
      */
-    private Map<Long, Double> buildForecastMap(List<DataShortTermPowerForecastIssued> records)
+    private Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
     {
-        Map<Long, Double> map = new LinkedHashMap<>();
+        Map<Long, BigDecimal> map = new LinkedHashMap<>();
         if (records == null) return map;
 
-        for (DataShortTermPowerForecastIssued r : records)
+        for (ForecastPowerAverage r : records)
         {
             if (r.getForecastTime() == null) continue;
-            String power = r.getPower();
-            if (StringUtils.isEmpty(power)) continue;
+            BigDecimal power = r.getAvgPower();
+            if (power == null) continue;
 
             try
             {
-                map.put(r.getForecastTime(), Double.parseDouble(power.trim()));
+                map.put(r.getForecastTime(), power);
             }
             catch (NumberFormatException e)
             {
@@ -571,8 +574,8 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 按日分组预测数据
      */
-    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByDay(
-            List<DataShortTermPowerForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByDay(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)
@@ -588,8 +591,8 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 按月分组预测数据
      */
-    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByMonth(
-            List<DataShortTermPowerForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByMonth(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)
@@ -608,8 +611,8 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 按年分组预测数据
      */
-    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByYear(
-            List<DataShortTermPowerForecastIssued> records)
+    private Map<String, List<ForecastPowerAverage>> groupForecastByYear(
+            List<ForecastPowerAverage> records)
     {
         return records.stream()
                 .filter(r -> r.getForecastTime() != null)

+ 41 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/ForecastPowerAverage.java

@@ -0,0 +1,41 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 预测功率平均值查询结果
+ *
+ * @author ufo
+ * @date 2026-09-16
+ */
+public class ForecastPowerAverage implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 预测时间 */
+    private Long forecastTime;
+
+    /** 平均功率 */
+    private BigDecimal avgPower;
+
+    public Long getForecastTime()
+    {
+        return forecastTime;
+    }
+
+    public void setForecastTime(Long forecastTime)
+    {
+        this.forecastTime = forecastTime;
+    }
+
+    public BigDecimal getAvgPower()
+    {
+        return avgPower;
+    }
+
+    public void setAvgPower(BigDecimal avgPower)
+    {
+        this.avgPower = avgPower;
+    }
+}

+ 12 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/mapper/DataShortTermPowerForecastIssuedMapper.java

@@ -3,6 +3,7 @@ package com.ufo.project.forecast.shortterm.power.mapper;
 import java.util.List;
 import org.apache.ibatis.annotations.Param;
 import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastIssued;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
 
 /**
  * 短期功率下发数据Mapper接口
@@ -70,4 +71,15 @@ public interface DataShortTermPowerForecastIssuedMapper
      */
     public List<DataShortTermPowerForecastIssued> selectByTimeRange(@Param("entityId") String entityId,
             @Param("startTime") Long startTime, @Param("endTime") Long endTime);
+
+    /**
+     * 查询场站指定时间范围内按预测时间分组的平均功率
+     *
+     * @param entityId 实体ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 按 forecast_time 分组的平均功率列表(按 forecast_time 升序)
+     */
+    public List<ForecastPowerAverage> selectAveragePowerByTimeRange(@Param("entityId") String entityId,
+            @Param("startTime") Long startTime, @Param("endTime") Long endTime);
 }

+ 11 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/service/IDataShortTermPowerForecastIssuedService.java

@@ -2,6 +2,7 @@ package com.ufo.project.forecast.shortterm.power.service;
 
 import java.util.List;
 import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastIssued;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
 
 /**
  * 短期功率下发数据Service接口
@@ -68,4 +69,14 @@ public interface IDataShortTermPowerForecastIssuedService
      * @return 预测功率下发数据集合(按 forecast_time 升序)
      */
     public List<DataShortTermPowerForecastIssued> selectByTimeRange(String entityId, Long startTime, Long endTime);
+
+    /**
+     * 查询场站指定时间范围内按预测时间分组的平均功率
+     *
+     * @param entityId 实体ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 按 forecast_time 分组的平均功率列表(按 forecast_time 升序)
+     */
+    public List<ForecastPowerAverage> selectAveragePowerByTimeRange(String entityId, Long startTime, Long endTime);
 }

+ 15 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/service/impl/DataShortTermPowerForecastIssuedServiceImpl.java

@@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.ufo.project.forecast.shortterm.power.mapper.DataShortTermPowerForecastIssuedMapper;
 import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastIssued;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
 import com.ufo.project.forecast.shortterm.power.service.IDataShortTermPowerForecastIssuedService;
 
 /**
@@ -106,4 +107,18 @@ public class DataShortTermPowerForecastIssuedServiceImpl implements IDataShortTe
     {
         return dataShortTermPowerForecastIssuedMapper.selectByTimeRange(entityId, startTime, endTime);
     }
+
+    /**
+     * 查询场站指定时间范围内按预测时间分组的平均功率
+     *
+     * @param entityId 实体ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 按 forecast_time 分组的平均功率列表
+     */
+    @Override
+    public List<ForecastPowerAverage> selectAveragePowerByTimeRange(String entityId, Long startTime, Long endTime)
+    {
+        return dataShortTermPowerForecastIssuedMapper.selectAveragePowerByTimeRange(entityId, startTime, endTime);
+    }
 }

+ 17 - 0
forecast-backend-server/src/main/resources/mybatis/power/DataShortTermPowerForecastIssuedMapper.xml

@@ -111,6 +111,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by forecast_time asc
     </select>
 
+    <!-- 按预测时间分组查询平均功率 -->
+    <resultMap type="ForecastPowerAverage" id="ForecastPowerAverageResult">
+        <result property="forecastTime" column="forecast_time" />
+        <result property="avgPower" column="avg_power" />
+    </resultMap>
+
+    <select id="selectAveragePowerByTimeRange" resultMap="ForecastPowerAverageResult">
+        select forecast_time,
+               avg(power::numeric) as avg_power
+        from data_short_term_power_forecast_issued
+        where entity_id = #{entityId}
+          and forecast_time &gt;= #{startTime}
+          and forecast_time &lt;= #{endTime}
+        group by forecast_time
+        order by forecast_time asc
+    </select>
+
     <delete id="deleteDataShortTermPowerForecastIssuedByIds" parameterType="String">
         delete from data_short_term_power_forecast_issued where _id in 
         <foreach item="Id" collection="array" open="(" separator="," close=")">