Ver Fonte

[0924] 优化短期、中期、超短期报表代码结构

taosj há 5 dias atrás
pai
commit
d8c0b61a8c

+ 292 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/AbstractPowerForecastReportServiceImpl.java

@@ -0,0 +1,292 @@
+package com.ufo.project.data.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.ufo.project.config.service.IConfigStationService;
+import com.ufo.project.forecast.shortterm.power.domain.ForecastPowerAverage;
+import com.ufo.project.model.domain.DataElectricityLocal;
+
+public class AbstractPowerForecastReportServiceImpl {
+
+    protected static final int SCALE = 3;
+    protected static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
+
+    @Autowired
+    protected IConfigStationService configStationService;
+
+    /**
+     * 计算开机容量
+     */
+    protected  BigDecimal computeOperatingCapacity(String entityId, List<DataElectricityLocal> records)
+    {
+        return BigDecimal.valueOf(configStationService.selectConfigStationById(Long.parseLong(entityId)).getInstalledCapacity());
+    }
+
+    /**
+     * 计算实发功率
+     */
+    protected BigDecimal computeActualPower(List<DataElectricityLocal> records)
+    {
+        BigDecimal sum = BigDecimal.ZERO;
+        int count = 0;
+        for (DataElectricityLocal r : records)
+        {
+            if (r.getGridStatus() != null && r.getGridStatus() == 1)
+            {
+                Double ap = r.getActualPower();
+                if (ap != null)
+                {
+                    sum = sum.add(BigDecimal.valueOf(ap));
+                    count++;
+                }
+            }
+        }
+        if (count == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+        return sum.divide(new BigDecimal(1000)).divide(BigDecimal.valueOf(count), SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 计算预测功率
+     */
+    protected BigDecimal computeForecastPower(Map<Long, BigDecimal> forecastMap)
+    {
+        if (forecastMap == null || forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+        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), SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 构建预测数据映射:时间戳(秒) -> power(kW)
+     */
+    protected Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
+    {
+        Map<Long, BigDecimal> map = new LinkedHashMap<>();
+        if (records == null) return map;
+
+        for (ForecastPowerAverage r : records)
+        {
+            if (r.getForecastTime() == null) continue;
+            BigDecimal power = r.getAvgPower();
+            if (power == null) continue;
+
+            try
+            {
+                map.put(r.getForecastTime(), power);
+            }
+            catch (NumberFormatException e)
+            {
+                // skip
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 计算RMSE
+     * @param actualRecords
+     * @param forecastMap
+     * @return
+     */
+    protected BigDecimal computeRmse(BigDecimal operatingCapacity, List<DataElectricityLocal> actualRecords,
+            Map<Long, BigDecimal> forecastMap)
+    {
+        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
+
+        BigDecimal sumSquaredError = BigDecimal.ZERO;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+            long ts = r.getTm().getTime() / 1000;
+            BigDecimal fp = forecastMap.get(ts);
+            if (fp == null) continue;
+
+            BigDecimal ap = new BigDecimal(r.getActualPower());
+
+            BigDecimal diffMW = (ap.subtract(fp)).divide(operatingCapacity, 10, RoundingMode.HALF_UP);
+            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
+            count++;
+        }
+
+        if (count == 0) return BigDecimal.ZERO;
+        BigDecimal mse = sumSquaredError.divide(new BigDecimal(count), 10, RoundingMode.HALF_UP);
+        return BigDecimal.valueOf(Math.sqrt(mse.doubleValue())).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+    /**
+     * 计算MAE
+     */
+    protected BigDecimal computeMae(BigDecimal operatingCapacity, List<DataElectricityLocal> actualRecords,
+            Map<Long, BigDecimal> forecastMap)
+    {
+        if (forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal sumAbsError = BigDecimal.ZERO;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
+            Long ts = r.getTm().getTime() / 1000;
+            BigDecimal fp = forecastMap.get(ts);
+            if (fp == null) continue;
+
+            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
+            BigDecimal absErrorMW = (ap.subtract(fp)).abs().divide(operatingCapacity, 10, RoundingMode.HALF_UP);
+            sumAbsError = sumAbsError.add(absErrorMW);
+            count++;
+        }
+
+        if (count == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal mae = sumAbsError.divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
+        return mae;
+    }
+
+    /**
+     * 计算相关性系数 R
+     */
+    protected BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
+            Map<Long, BigDecimal> forecastMap)
+    {
+        if (forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+
+        // 收集成对数据(MW)
+        List<BigDecimal> xList = new ArrayList<>();
+        List<BigDecimal> yList = new ArrayList<>();
+        BigDecimal thousand = BigDecimal.valueOf(1000);
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
+            Long ts = r.getTm().getTime() / 1000;
+            BigDecimal fp = forecastMap.get(ts);
+            if (fp == null) continue;
+
+            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();
+        if (n < 2)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal nBd = BigDecimal.valueOf(n);
+        // 计算均值
+        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);
+
+        // 计算相关系数:逐步累加,保持精度
+        BigDecimal numerator = BigDecimal.ZERO;
+        BigDecimal sumSqX = BigDecimal.ZERO;
+        BigDecimal sumSqY = BigDecimal.ZERO;
+
+        for (int i = 0; i < n; i++)
+        {
+            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));
+        }
+
+        BigDecimal denominatorSquared = sumSqX.multiply(sumSqY);
+        if (denominatorSquared.compareTo(BigDecimal.ZERO) == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        // 用 Math.sqrt 开方(平方根无法用 BigDecimal 精确表示)
+        double denominator = Math.sqrt(denominatorSquared.doubleValue());
+        double r = numerator.doubleValue() / denominator;
+        return BigDecimal.valueOf(r).setScale(3, RoundingMode.HALF_UP);
+    }
+
+        /**
+     * 计算准确率 acc = (1 - sqrt(ΣSi / N)) * 100
+     *
+     * <p>其中:
+     * <ul>
+     *   <li>阈值 = 开机容量 * 0.5</li>
+     *   <li>Si = ((Pmi - Ppi) / Pmi)² 当 Pmi >= 阈值</li>
+     *   <li>Si = ((Pmi - Ppi) / 阈值)² 当 Pmi &lt; 阈值</li>
+     * </ul>
+     */
+    protected <T> BigDecimal computeAccuracy(List<DataElectricityLocal> actualRecords,
+            Map<Long, BigDecimal> forecastMap,
+            BigDecimal operatingCapacity,
+            List<T> rawForecastList,
+            BiFunction<List<T>, Long, BigDecimal> getAccForecastReportPower)
+    {
+        if (forecastMap == null || forecastMap.isEmpty() || operatingCapacity == null
+                || operatingCapacity.compareTo(BigDecimal.ZERO) == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        // 阈值 = 开机容量(MW) * 0.25
+        BigDecimal baseThreshold = new BigDecimal("0.25");
+        int n = 0;
+        int totlaN = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
+            Long ts = r.getTm().getTime() / 1000;
+            BigDecimal fp = getAccForecastReportPower.apply(rawForecastList, ts);
+            if (fp == null) continue;
+
+            BigDecimal diff = (BigDecimal.valueOf(r.getActualPower()).subtract(fp)).abs().divide(operatingCapacity, SCALE, RoundingMode.HALF_UP);
+            if (diff.compareTo(baseThreshold) < 0)
+            {
+                totlaN++;
+            }
+            n++;
+        }
+
+        if (n == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal acc = BigDecimal.valueOf(totlaN * 100).divide(BigDecimal.valueOf(n), SCALE, RoundingMode.HALF_UP);
+        // 边界处理
+        if (acc.compareTo(BigDecimal.ZERO) < 0) acc = BigDecimal.ZERO;
+        if (acc.compareTo(BigDecimal.valueOf(100)) > 0) acc = BigDecimal.valueOf(100);
+
+        return acc;
+    }
+}

+ 17 - 330
forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/MediumTermPowerForecastReportServiceImpl.java

@@ -40,12 +40,11 @@ import lombok.extern.slf4j.Slf4j;
  */
 @Slf4j 
 @Service
-public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowerForecastReportService
+public class MediumTermPowerForecastReportServiceImpl extends AbstractPowerForecastReportServiceImpl implements IMediumTermPowerForecastReportService
 {
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
     private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-    private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
 
     @Autowired
     private IDataElectricityLocalService electricityLocalService;
@@ -79,11 +78,11 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
         switch (type)
         {
             case "daily":
-                return buildDailyReport(actualList, pureForecastList, forecastList);
+                return buildDailyReport(entityId, actualList, pureForecastList, forecastList);
             case "monthly":
-                return buildMonthlyReport(actualList, pureForecastList, forecastList);
+                return buildMonthlyReport(entityId, actualList, pureForecastList, forecastList);
             case "yearly":
-                return buildYearlyReport(actualList, pureForecastList, forecastList);
+                return buildYearlyReport(entityId, actualList, pureForecastList, forecastList);
             default:
                 return Collections.emptyList();
         }
@@ -154,7 +153,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
     /**
      * 构建日报
      */
-    private List<MediumTermPowerForecastReportVO> buildDailyReport(List<DataElectricityLocal> actualList,
+    private List<MediumTermPowerForecastReportVO> buildDailyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataMediumTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
@@ -169,7 +168,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
 
             MediumTermPowerForecastReportVO vo = new MediumTermPowerForecastReportVO();
             vo.setTime(day);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -178,7 +177,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
     /**
      * 构建月报
      */
-    private List<MediumTermPowerForecastReportVO> buildMonthlyReport(List<DataElectricityLocal> actualList,
+    private List<MediumTermPowerForecastReportVO> buildMonthlyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataMediumTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
@@ -194,7 +193,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
 
             MediumTermPowerForecastReportVO vo = new MediumTermPowerForecastReportVO();
             vo.setTime(month);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -203,7 +202,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
     /**
      * 构建年报
      */
-    private List<MediumTermPowerForecastReportVO> buildYearlyReport(List<DataElectricityLocal> actualList,
+    private List<MediumTermPowerForecastReportVO> buildYearlyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataMediumTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
@@ -219,7 +218,7 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
 
             MediumTermPowerForecastReportVO vo = new MediumTermPowerForecastReportVO();
             vo.setTime(year);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -228,15 +227,15 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
     /**
      * 计算指标(日报/月报/年报通用)
      */
-    private void computeDailyMetrics(MediumTermPowerForecastReportVO vo,
+    private void computeDailyMetrics(String entityId, MediumTermPowerForecastReportVO vo,
             List<DataElectricityLocal> actualRecords, List<ForecastPowerAverage> forecastRecords, List<DataMediumTermPowerForecastReport> rawForecastList)
     {
         // 预先构建预测数据映射(避免各方法重复调用 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);
+        BigDecimal operatingCapacity = computeOperatingCapacity(entityId, actualRecords);
+        vo.setOperatingCapacity(operatingCapacity.divide(new BigDecimal(1000), SCALE, RoundingMode.HALF_UP));
 
         // 2. actualPower: 筛选 grid_status = 1, AVG(actual_power) / 1000
         BigDecimal actualPower = computeActualPower(actualRecords);
@@ -247,11 +246,11 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
         vo.setForecastPower(forecastPower);
 
         // 4. RMSE
-        BigDecimal rmse = computeRmse(actualRecords, forecastMap);
+        BigDecimal rmse = computeRmse(operatingCapacity, actualRecords, forecastMap);
         vo.setRootMeanSquareError(rmse);
 
         // 5. MAE
-        BigDecimal mae = computeMae(actualRecords, forecastMap);
+        BigDecimal mae = computeMae(operatingCapacity, actualRecords, forecastMap);
         vo.setMeanAbsoluteError(mae);
 
         // 6. correlationCoefficientR
@@ -267,303 +266,17 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
         vo.setMaximumErrorPassRate(passRate);
 
         // 8. acc: 合格率
-        BigDecimal acc = computeAccuracy(actualRecords, forecastMap, operatingCapacity, rawForecastList);
+        BigDecimal acc = computeAccuracy(actualRecords, forecastMap, operatingCapacity, rawForecastList, this::getAccForecastReportPower);
         vo.setAcc(acc);
     }
 
-    /**
-     * 计算开机容量
-     */
-    private BigDecimal computeOperatingCapacity(List<DataElectricityLocal> records)
-    {
-        double maxPower = 0.0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getDeviceStatus() != null && r.getDeviceStatus() == 0
-                    && r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null && ap > maxPower)
-                {
-                    maxPower = ap;
-                }
-            }
-        }
-        return BigDecimal.valueOf(maxPower / 1000.0).setScale(3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算实发功率
-     */
-    private BigDecimal computeActualPower(List<DataElectricityLocal> records)
-    {
-        BigDecimal sum = BigDecimal.ZERO;
-        int count = 0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null)
-                {
-                    sum = sum.add(BigDecimal.valueOf(ap));
-                    count++;
-                }
-            }
-        }
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-        return sum.divide(new BigDecimal(1000)).divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算预测功率
-     */
-    private BigDecimal computeForecastPower(Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap == null || forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-        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,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal sumSquaredError = BigDecimal.ZERO;
-        int count = 0;
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            BigDecimal diffMW = ap.subtract(fp).divide(thousand, 10, RoundingMode.HALF_UP);
-            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
-            count++;
-        }
-
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        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);
-    }
-
-    /**
-     * 计算MAE
-     */
-    private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal sumAbsError = BigDecimal.ZERO;
-        int count = 0;
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            BigDecimal absErrorMW = ap.subtract(fp).abs().divide(thousand, 10, RoundingMode.HALF_UP);
-            sumAbsError = sumAbsError.add(absErrorMW);
-            count++;
-        }
-
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal mae = sumAbsError.divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
-        return mae;
-    }
-
-    /**
-     * 计算相关性系数 R
-     */
-    private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 收集成对数据(MW)
-        List<BigDecimal> xList = new ArrayList<>();
-        List<BigDecimal> yList = new ArrayList<>();
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            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();
-        if (n < 2)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal nBd = BigDecimal.valueOf(n);
-        // 计算均值
-        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);
-
-        // 计算相关系数:逐步累加,保持精度
-        BigDecimal numerator = BigDecimal.ZERO;
-        BigDecimal sumSqX = BigDecimal.ZERO;
-        BigDecimal sumSqY = BigDecimal.ZERO;
-
-        for (int i = 0; i < n; i++)
-        {
-            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));
-        }
-
-        BigDecimal denominatorSquared = sumSqX.multiply(sumSqY);
-        if (denominatorSquared.compareTo(BigDecimal.ZERO) == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 用 Math.sqrt 开方(平方根无法用 BigDecimal 精确表示)
-        double denominator = Math.sqrt(denominatorSquared.doubleValue());
-        double r = numerator.doubleValue() / denominator;
-        return BigDecimal.valueOf(r).setScale(3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算准确率 acc = (1 - sqrt(ΣSi / N)) * 100
-     *
-     * <p>其中:
-     * <ul>
-     *   <li>阈值 = 开机容量 * 0.25</li>
-     *   <li>Si = ((Pmi - Ppi) / Pmi)² 当 Pmi >= 阈值</li>
-     *   <li>Si = ((Pmi - Ppi) / 阈值)² 当 Pmi &lt; 阈值</li>
-     * </ul>
-     */
-    private BigDecimal computeAccuracy(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap,
-            BigDecimal operatingCapacity, List<DataMediumTermPowerForecastReport> rawForecastList)
-    {
-        if (operatingCapacity == null
-                || operatingCapacity.compareTo(BigDecimal.ZERO) == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 阈值 = 开机容量(MW) * 0.25
-        BigDecimal baseThreshold = new BigDecimal("0.25");
-        BigDecimal threshold = operatingCapacity.multiply(baseThreshold).setScale(3, RoundingMode.HALF_UP);
-
-        BigDecimal stotal = BigDecimal.ZERO;
-        int n = 0;
-        int precisionLength = 20;
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = getAccForecastReportPower(ts, rawForecastList);
-            if (fp == null) continue;
-
-            BigDecimal pmiMW = BigDecimal.valueOf(r.getActualPower()).divide(BigDecimal.valueOf(1000), precisionLength, RoundingMode.HALF_UP);
-            BigDecimal ppiMW = fp.divide(BigDecimal.valueOf(1000), precisionLength, RoundingMode.HALF_UP);
-
-            BigDecimal diff = pmiMW.subtract(ppiMW);
-            BigDecimal si;
-            if (pmiMW.compareTo(threshold) >= 0)
-            {
-                BigDecimal tmpSi = diff.divide(pmiMW, precisionLength, RoundingMode.HALF_UP);
-                si = tmpSi.multiply(tmpSi);
-                log.debug("actualRecord tm {}, actualRecord power {}, pmiMW {}, forecast power {}, ppiMW {}, si {}", r.getTm(), r.getActualPower(), pmiMW, fp, ppiMW, si);
-            }
-            else
-            {
-                BigDecimal tmpSi = diff.divide(threshold, precisionLength, RoundingMode.HALF_UP);
-                si = tmpSi.multiply(tmpSi);
-                log.debug("actualRecord tm {}, actualRecord power {}, pmiMW {}, forecast power {}, ppiMW {}, si {}", r.getTm(), r.getActualPower(), pmiMW, fp, ppiMW, si);
-            }
-
-            stotal = stotal.add(si);
-            n++;
-        }
-
-        if (n == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // acc = (1 - sqrt(stotal / n)) * 100
-        double meanSquare = stotal.divide(BigDecimal.valueOf(n), precisionLength, RoundingMode.HALF_UP).doubleValue();
-        log.debug("meanSquare: {}", meanSquare);
-        double deviation = Math.sqrt(meanSquare);
-        log.debug("deviation: {}", deviation);
-        double acc = deviation * 100.0;
-        log.debug("acc: {}", acc);
-
-        // 边界处理
-        if (acc < 0) acc = 0;
-        if (acc > 100) acc = 100;
-
-        return BigDecimal.valueOf(acc).setScale(3, RoundingMode.HALF_UP);
-    }
-
     /**
      * 获取日前timestamp预报数据
      * @param timestamp
      * @param rawForecastList
      * @return
      */
-    private BigDecimal getAccForecastReportPower(long timestamp, List<DataMediumTermPowerForecastReport> rawForecastList)
+    private BigDecimal getAccForecastReportPower(List<DataMediumTermPowerForecastReport> rawForecastList, long timestamp)
     {
         return rawForecastList.stream()
                 .filter(r -> null != r.getPower() && r.getForecastTime() != null && r.getStartTime() != null && r.getForecastTime() == timestamp && is4DaysBefore(timestamp, r.getStartTime()))
@@ -585,32 +298,6 @@ public class MediumTermPowerForecastReportServiceImpl implements IMediumTermPowe
                 .equals(LocalDateTime.ofInstant(Instant.ofEpochSecond(datTimestamp), ZONE_SHANGHAI).toLocalDate());
     }
 
-    /**
-     * 构建预测数据映射:时间戳(秒) -> power(kW)
-     */
-    private Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
-    {
-        Map<Long, BigDecimal> map = new LinkedHashMap<>();
-        if (records == null) return map;
-
-        for (ForecastPowerAverage r : records)
-        {
-            if (r.getForecastTime() == null) continue;
-            BigDecimal power = r.getAvgPower();
-            if (power == null) continue;
-
-            try
-            {
-                map.put(r.getForecastTime(), power);
-            }
-            catch (NumberFormatException e)
-            {
-                // skip
-            }
-        }
-        return map;
-    }
-
     /**
      * 按日分组
      */

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

@@ -6,7 +6,6 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
-import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
@@ -43,7 +42,7 @@ import lombok.extern.slf4j.Slf4j;
  */
 @Slf4j 
 @Service
-public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerForecastReportService
+public class ShortTermPowerForecastReportServiceImpl extends AbstractPowerForecastReportServiceImpl implements IShortTermPowerForecastReportService
 {
     /** Dict类型:短期预测误差阈值 */
     private static final String DICT_TYPE_SHORT_TERM_ERROR_THRESHOLD = "short_term_error_threshold";
@@ -51,7 +50,6 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
     private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-    private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
 
     @Autowired
     private IDataElectricityLocalService electricityLocalService;
@@ -97,11 +95,11 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         switch (type)
         {
             case "daily":
-                return buildDailyReport(actualList, pureForecastList, forecastList);
+                return buildDailyReport(entityId, actualList, pureForecastList, forecastList);
             case "monthly":
-                return buildMonthlyReport(actualList, pureForecastList, forecastList);
+                return buildMonthlyReport(entityId, actualList, pureForecastList, forecastList);
             case "yearly":
-                return buildYearlyReport(actualList, pureForecastList, forecastList);
+                return buildYearlyReport(entityId, actualList, pureForecastList, forecastList);
             default:
                 return Collections.emptyList();
         }
@@ -172,7 +170,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 构建日报
      */
-    private List<ShortTermPowerForecastReportVO> buildDailyReport(List<DataElectricityLocal> actualList,
+    private List<ShortTermPowerForecastReportVO> buildDailyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataShortTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
@@ -187,7 +185,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
             vo.setTime(day);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -196,7 +194,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 构建月报
      */
-    private List<ShortTermPowerForecastReportVO> buildMonthlyReport(List<DataElectricityLocal> actualList,
+    private List<ShortTermPowerForecastReportVO> buildMonthlyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataShortTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
@@ -212,7 +210,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
             vo.setTime(month);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -221,7 +219,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 构建年报
      */
-    private List<ShortTermPowerForecastReportVO> buildYearlyReport(List<DataElectricityLocal> actualList,
+    private List<ShortTermPowerForecastReportVO> buildYearlyReport(String entityId, List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList, List<DataShortTermPowerForecastReport> rawForecastList)
     {
         Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
@@ -237,7 +235,7 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
 
             ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
             vo.setTime(year);
-            computeDailyMetrics(vo, records, forecastRecords, rawForecastList);
+            computeDailyMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -246,15 +244,15 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
     /**
      * 计算指标(日报/月报/年报通用)
      */
-    private void computeDailyMetrics(ShortTermPowerForecastReportVO vo,
+    private void computeDailyMetrics(String entityId, ShortTermPowerForecastReportVO vo,
             List<DataElectricityLocal> actualRecords, List<ForecastPowerAverage> forecastRecords, List<DataShortTermPowerForecastReport> rawForecastList)
     {
         // 预先构建预测数据映射(避免各方法重复调用 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);
+        BigDecimal operatingCapacity = computeOperatingCapacity(entityId, actualRecords);
+        vo.setOperatingCapacity(operatingCapacity.divide(new BigDecimal(1000), SCALE, RoundingMode.HALF_UP));
 
         // 2. actualPower: 筛选 grid_status = 1, AVG(actual_power) / 1000
         BigDecimal actualPower = computeActualPower(actualRecords);
@@ -265,11 +263,11 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         vo.setForecastPower(forecastPower);
 
         // 4. RMSE
-        BigDecimal rmse = computeRmse(actualRecords, forecastMap);
+        BigDecimal rmse = computeRmse(operatingCapacity, actualRecords, forecastMap);
         vo.setRootMeanSquareError(rmse);
 
         // 5. MAE
-        BigDecimal mae = computeMae(actualRecords, forecastMap);
+        BigDecimal mae = computeMae(operatingCapacity, actualRecords, forecastMap);
         vo.setMeanAbsoluteError(mae);
 
         // 6. correlationCoefficientR
@@ -285,302 +283,17 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
         vo.setMaximumErrorPassRate(passRate);
 
         // 8. acc: 合格率
-        BigDecimal acc = computeAccuracy(actualRecords, forecastMap, operatingCapacity, rawForecastList);
+        BigDecimal acc = computeAccuracy(actualRecords, forecastMap, operatingCapacity, rawForecastList, this::getAccForecastReportPower);
         vo.setAcc(acc);
     }
 
-    /**
-     * 计算开机容量
-     */
-    private BigDecimal computeOperatingCapacity(List<DataElectricityLocal> records)
-    {
-        double maxPower = 0.0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getDeviceStatus() != null && r.getDeviceStatus() == 0
-                    && r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null && ap > maxPower)
-                {
-                    maxPower = ap;
-                }
-            }
-        }
-        return BigDecimal.valueOf(maxPower / 1000.0).setScale(3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算实发功率
-     */
-    private BigDecimal computeActualPower(List<DataElectricityLocal> records)
-    {
-        BigDecimal sum = BigDecimal.ZERO;
-        int count = 0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null)
-                {
-                    sum = sum.add(BigDecimal.valueOf(ap));
-                    count++;
-                }
-            }
-        }
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-        return sum.divide(new BigDecimal(1000)).divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算预测功率
-     */
-    private BigDecimal computeForecastPower(Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap == null || forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-        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,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal sumSquaredError = BigDecimal.ZERO;
-        int count = 0;
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            BigDecimal diffMW = ap.subtract(fp).divide(thousand, 10, RoundingMode.HALF_UP);
-            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
-            count++;
-        }
-
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        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);
-    }
-
-    /**
-     * 计算MAE
-     */
-    private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal sumAbsError = BigDecimal.ZERO;
-        int count = 0;
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            BigDecimal absErrorMW = ap.subtract(fp).abs().divide(thousand, 10, RoundingMode.HALF_UP);
-            sumAbsError = sumAbsError.add(absErrorMW);
-            count++;
-        }
-
-        if (count == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal mae = sumAbsError.divide(BigDecimal.valueOf(count), 3, RoundingMode.HALF_UP);
-        return mae;
-    }
-
-    /**
-     * 计算相关性系数 R
-     */
-    private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty())
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 收集成对数据(MW)
-        List<BigDecimal> xList = new ArrayList<>();
-        List<BigDecimal> yList = new ArrayList<>();
-        BigDecimal thousand = BigDecimal.valueOf(1000);
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            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();
-        if (n < 2)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        BigDecimal nBd = BigDecimal.valueOf(n);
-        // 计算均值
-        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);
-
-        // 计算相关系数:逐步累加,保持精度
-        BigDecimal numerator = BigDecimal.ZERO;
-        BigDecimal sumSqX = BigDecimal.ZERO;
-        BigDecimal sumSqY = BigDecimal.ZERO;
-
-        for (int i = 0; i < n; i++)
-        {
-            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));
-        }
-
-        BigDecimal denominatorSquared = sumSqX.multiply(sumSqY);
-        if (denominatorSquared.compareTo(BigDecimal.ZERO) == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 用 Math.sqrt 开方(平方根无法用 BigDecimal 精确表示)
-        double denominator = Math.sqrt(denominatorSquared.doubleValue());
-        double r = numerator.doubleValue() / denominator;
-        return BigDecimal.valueOf(r).setScale(3, RoundingMode.HALF_UP);
-    }
-
-    /**
-     * 计算准确率 acc = (1 - sqrt(ΣSi / N)) * 100
-     *
-     * <p>其中:
-     * <ul>
-     *   <li>阈值 = 开机容量 * 0.5</li>
-     *   <li>Si = ((Pmi - Ppi) / Pmi)² 当 Pmi >= 阈值</li>
-     *   <li>Si = ((Pmi - Ppi) / 阈值)² 当 Pmi &lt; 阈值</li>
-     * </ul>
-     */
-    private BigDecimal computeAccuracy(List<DataElectricityLocal> actualRecords,
-            Map<Long, BigDecimal> forecastMap,
-            BigDecimal operatingCapacity,
-            List<DataShortTermPowerForecastReport> rawForecastList)
-    {
-        if (forecastMap == null || forecastMap.isEmpty() || operatingCapacity == null
-                || operatingCapacity.compareTo(BigDecimal.ZERO) == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // 阈值 = 开机容量(MW) * 0.25
-        BigDecimal baseThreshold = new BigDecimal("0.25");
-        BigDecimal threshold = operatingCapacity.multiply(baseThreshold).setScale(3, RoundingMode.HALF_UP);
-
-        BigDecimal stotal = BigDecimal.ZERO;
-        int n = 0;
-        int precisionLength = 20;
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null || r.getActualPower() == null) continue;
-
-            Long ts = r.getTm().getTime() / 1000;
-            BigDecimal fp = getAccForecastReportPower(ts, rawForecastList);
-            if (fp == null) continue;
-
-            BigDecimal pmiMW = BigDecimal.valueOf(r.getActualPower()).divide(BigDecimal.valueOf(1000), precisionLength, RoundingMode.HALF_UP);
-            BigDecimal ppiMW = fp.divide(BigDecimal.valueOf(1000), precisionLength, RoundingMode.HALF_UP);
-
-            BigDecimal diff = pmiMW.subtract(ppiMW);
-            BigDecimal si;
-            if (pmiMW.compareTo(threshold) >= 0)
-            {
-                BigDecimal tmpSi = diff.divide(pmiMW, precisionLength, RoundingMode.HALF_UP);
-                si = tmpSi.multiply(tmpSi);
-            }
-            else
-            {
-                BigDecimal tmpSi = diff.divide(threshold, precisionLength, RoundingMode.HALF_UP);
-                si = tmpSi.multiply(tmpSi);
-            }
-
-            stotal = stotal.add(si);
-            n++;
-        }
-
-        if (n == 0)
-        {
-            return BigDecimal.ZERO;
-        }
-
-        // acc = (1 - sqrt(stotal / n)) * 100
-        double meanSquare = stotal.divide(BigDecimal.valueOf(n), precisionLength, RoundingMode.HALF_UP).doubleValue();
-        log.debug("meanSquare: {}", meanSquare);
-        double deviation = Math.sqrt(meanSquare);
-        log.debug("deviation: {}", deviation);
-        double acc = deviation * 100.0;
-        log.debug("acc: {}", acc);
-
-        // 边界处理
-        if (acc < 0) acc = 0;
-        if (acc > 100) acc = 100;
-
-        return BigDecimal.valueOf(acc).setScale(3, RoundingMode.HALF_UP);
-    }
-
     /**
      * 获取日前timestamp预报数据
      * @param timestamp
      * @param rawForecastList
      * @return
      */
-    private BigDecimal getAccForecastReportPower(long timestamp, List<DataShortTermPowerForecastReport> rawForecastList)
+    private BigDecimal getAccForecastReportPower(List<DataShortTermPowerForecastReport> rawForecastList, long timestamp)
     {
         return rawForecastList.stream()
                 .filter(r -> null != r.getPower() && r.getForecastTime() != null && r.getStartTime() != null && r.getForecastTime() == timestamp && isYesterday(timestamp, r.getStartTime()))
@@ -602,32 +315,6 @@ public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerF
                 .equals(LocalDateTime.ofInstant(Instant.ofEpochSecond(datTimestamp), ZONE_SHANGHAI).toLocalDate());
     }
 
-    /**
-     * 构建预测数据映射:时间戳(秒) -> power(kW)
-     */
-    private Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
-    {
-        Map<Long, BigDecimal> map = new LinkedHashMap<>();
-        if (records == null) return map;
-
-        for (ForecastPowerAverage r : records)
-        {
-            if (r.getForecastTime() == null) continue;
-            BigDecimal power = r.getAvgPower();
-            if (power == null) continue;
-
-            try
-            {
-                map.put(r.getForecastTime(), power);
-            }
-            catch (NumberFormatException e)
-            {
-                // skip
-            }
-        }
-        return map;
-    }
-
     /**
      * 按日分组
      */

+ 21 - 298
forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/UltraShortTermPowerForecastReportServiceImpl.java

@@ -37,11 +37,9 @@ import com.ufo.project.model.service.IDataElectricityLocalService;
  * @date 2026-09-07
  */
 @Service
-public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShortTermPowerForecastReportService
+public class UltraShortTermPowerForecastReportServiceImpl extends AbstractPowerForecastReportServiceImpl implements IUltraShortTermPowerForecastReportService
 {
-    private static final int SCALE = 3;
     private static final long SECONDS_PER_DAY = 86400L;
-
     private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
     private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
@@ -84,11 +82,11 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         switch (type)
         {
             case "daily":
-                return buildDailyReport(actualList, pureForecastList, forecastList);
+                return buildDailyReport(entityId, actualList, pureForecastList, forecastList);
             case "monthly":
-                return buildMonthlyReport(actualList, pureForecastList, forecastList);
+                return buildMonthlyReport(entityId, actualList, pureForecastList, forecastList);
             case "yearly":
-                return buildYearlyReport(actualList, pureForecastList, forecastList);
+                return buildYearlyReport(entityId, actualList, pureForecastList, forecastList);
             default:
                 return Collections.emptyList();
         }
@@ -159,7 +157,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 构建日报
      */
-    private List<UltraShortTermPowerForecastReportVO> buildDailyReport(
+    private List<UltraShortTermPowerForecastReportVO> buildDailyReport(String entityId,
             List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList,
             List<DataSuperShortTermForecastReport> rawForecastList)
@@ -176,7 +174,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(day);
-            computeMetrics(vo, records, forecastRecords, rawForecastList);
+            computeMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -185,7 +183,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 构建月报
      */
-    private List<UltraShortTermPowerForecastReportVO> buildMonthlyReport(
+    private List<UltraShortTermPowerForecastReportVO> buildMonthlyReport(String entityId,
             List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList,
             List<DataSuperShortTermForecastReport> rawForecastList)
@@ -203,7 +201,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(month);
-            computeMetrics(vo, records, forecastRecords, rawForecastList);
+            computeMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -212,7 +210,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 构建年报
      */
-    private List<UltraShortTermPowerForecastReportVO> buildYearlyReport(
+    private List<UltraShortTermPowerForecastReportVO> buildYearlyReport(String entityId,
             List<DataElectricityLocal> actualList,
             List<ForecastPowerAverage> forecastList,
             List<DataSuperShortTermForecastReport> rawForecastList)
@@ -230,7 +228,7 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
 
             UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
             vo.setTime(year);
-            computeMetrics(vo, records, forecastRecords, rawForecastList);
+            computeMetrics(entityId, vo, records, forecastRecords, rawForecastList);
             result.add(vo);
         }
         return result;
@@ -239,34 +237,35 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
     /**
      * 计算指标
      */
-    private void computeMetrics(UltraShortTermPowerForecastReportVO vo,
+    private void computeMetrics(String entityId, UltraShortTermPowerForecastReportVO vo,
             List<DataElectricityLocal> actualRecords,
             List<ForecastPowerAverage> forecastRecords,
             List<DataSuperShortTermForecastReport> rawForecastList)
     {
+        Map<Long, BigDecimal> forecastMap = buildForecastMap(forecastRecords);
+
         // 1. 开机容量
-        BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
-        vo.setOperatingCapacity(operatingCapacity);
+        BigDecimal operatingCapacity = computeOperatingCapacity(entityId, actualRecords);
+        vo.setOperatingCapacity(operatingCapacity.divide(new BigDecimal(1000), SCALE, RoundingMode.HALF_UP));
 
         // 2. 实发功率
         BigDecimal actualPower = computeActualPower(actualRecords);
         vo.setActualPower(actualPower);
 
         // 3. 预测功率(超短期预测功率)
-        BigDecimal forecastPower = computeForecastPower(forecastRecords);
+        BigDecimal forecastPower = computeForecastPower(forecastMap);
         vo.setForecastPower(forecastPower);
 
-        Map<Long, BigDecimal> forecastMap = buildForecastMap(forecastRecords);
         // 4. RMSE
-        BigDecimal rmse = computeRmse(actualRecords, forecastRecords, forecastMap);
+        BigDecimal rmse = computeRmse(operatingCapacity, actualRecords, forecastMap);
         vo.setRootMeanSquareError(rmse);
 
         // 5. MAE
-        BigDecimal mae = computeMae(actualRecords, forecastRecords, forecastMap);
+        BigDecimal mae = computeMae(operatingCapacity, actualRecords, forecastMap);
         vo.setMeanAbsoluteError(mae);
 
         // 6. 相关性系数
-        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords, forecastMap);
+        BigDecimal corr = computeCorrelation(actualRecords, forecastMap);
         vo.setCorrelationCoefficientR(corr);
 
          // 7. 准确率
@@ -278,250 +277,17 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
         vo.setMaximumErrorPassRate(passRate);
 
         // 8. 合格率 (acc)
-        BigDecimal acc = computeAcc(actualRecords, forecastRecords, operatingCapacity, rawForecastList);
+        BigDecimal acc = computeAccuracy(actualRecords, forecastMap, operatingCapacity, rawForecastList, this::getAccForecastReportPower);
         vo.setAcc(acc);
     }
 
-    // ==================== 基础计算 ====================
-
-    private BigDecimal computeOperatingCapacity(List<DataElectricityLocal> records)
-    {
-        double maxPower = 0.0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getDeviceStatus() != null && r.getDeviceStatus() == 0
-                    && r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null && ap > maxPower)
-                {
-                    maxPower = ap;
-                }
-            }
-        }
-        return BigDecimal.valueOf(maxPower / 1000.0).setScale(SCALE, RoundingMode.HALF_UP);
-    }
-
-    private BigDecimal computeActualPower(List<DataElectricityLocal> records)
-    {
-        double sum = 0.0;
-        int count = 0;
-        for (DataElectricityLocal r : records)
-        {
-            if (r.getGridStatus() != null && r.getGridStatus() == 1)
-            {
-                Double ap = r.getActualPower();
-                if (ap != null)
-                {
-                    sum += ap;
-                    count++;
-                }
-            }
-        }
-        if (count == 0) return BigDecimal.ZERO;
-        return BigDecimal.valueOf(sum / count / 1000.0).setScale(SCALE, RoundingMode.HALF_UP);
-    }
-
-    private BigDecimal computeForecastPower(List<ForecastPowerAverage> records)
-    {
-        BigDecimal sum = BigDecimal.ZERO;
-        int count = 0;
-        for (ForecastPowerAverage r : records)
-        {
-            BigDecimal power = r.getAvgPower();
-            if (power != null)
-            {
-                try
-                {
-                    sum = sum.add(power);
-                    count++;
-                }
-                catch (NumberFormatException e)
-                {
-                    // skip
-                }
-            }
-        }
-        if (count == 0) return BigDecimal.ZERO;
-        BigDecimal avg = sum.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP);
-        return avg.divide(new BigDecimal(1000), 3, RoundingMode.HALF_UP);
-    }
-
-    private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
-            List<ForecastPowerAverage> forecastRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
-
-        BigDecimal sumSquaredError = BigDecimal.ZERO;
-        int count = 0;
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null) continue;
-            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = new BigDecimal(r.getActualPower());
-
-            BigDecimal diffMW = ap.subtract(fp).divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP);
-            sumSquaredError = sumSquaredError.add(diffMW.multiply(diffMW));
-            count++;
-        }
-
-        if (count == 0) return BigDecimal.ZERO;
-        BigDecimal mse = sumSquaredError.divide(BigDecimal.valueOf(count), 10, RoundingMode.HALF_UP);
-        return BigDecimal.valueOf(Math.sqrt(mse.doubleValue())).setScale(SCALE, RoundingMode.HALF_UP);
-    }
-
-    private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
-            List<ForecastPowerAverage> forecastRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
-
-        BigDecimal sumAbsError = BigDecimal.ZERO;
-        int count = 0;
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null) continue;
-            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            if (ap == null) continue;
-
-            BigDecimal absErrorMW = ap.subtract(fp).divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP);
-            sumAbsError = sumAbsError.add(absErrorMW);
-            count++;
-        }
-
-        if (count == 0) return BigDecimal.ZERO;
-        return sumAbsError.divide(BigDecimal.valueOf(count), SCALE, RoundingMode.HALF_UP);
-    }
-
-    private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
-            List<ForecastPowerAverage> forecastRecords,
-            Map<Long, BigDecimal> forecastMap)
-    {
-        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
-
-        List<BigDecimal> xList = new ArrayList<>();
-        List<BigDecimal> yList = new ArrayList<>();
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null) continue;
-            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            BigDecimal fp = forecastMap.get(ts);
-            if (fp == null) continue;
-
-            BigDecimal ap = BigDecimal.valueOf(r.getActualPower());
-            if (ap == null) continue;
-
-            xList.add(ap.divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP));
-            yList.add(fp.divide(BigDecimal.valueOf(1000), 10, RoundingMode.HALF_UP));
-        }
-
-        int n = xList.size();
-        if (n < 2) return BigDecimal.ZERO;
-
-        BigDecimal meanX = xList.stream()
-            .reduce(BigDecimal.ZERO, BigDecimal::add)
-            .divide(BigDecimal.valueOf(xList.size()), 10, RoundingMode.HALF_UP);
-        BigDecimal meanY = yList.stream()
-            .reduce(BigDecimal.ZERO, BigDecimal::add)
-            .divide(BigDecimal.valueOf(yList.size()), 10, RoundingMode.HALF_UP);
-
-        BigDecimal numerator = BigDecimal.ZERO;
-        BigDecimal sumSqX = BigDecimal.ZERO;
-        BigDecimal sumSqY = BigDecimal.ZERO;
-
-        for (int i = 0; i < n; i++)
-        {
-            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));
-        }
-
-        BigDecimal denominator = BigDecimal.valueOf(Math.sqrt(sumSqX.multiply(sumSqY).doubleValue()));
-        if (denominator.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
-
-        BigDecimal r = numerator.divide(denominator, SCALE, RoundingMode.HALF_UP);
-        return r;
-    }
-
-    /**
-     * 计算准确率(acc)
-     * acc = (1 - 偏差值) × 100%
-     * 偏差值 = sqrt( sum(Si) / N )
-     * Si = ((Pmi - Ppi) / max(阈值, Pmi))^2
-     * 阈值 = 开机容量(MW) * 0.5
-     */
-    private BigDecimal computeAcc(List<DataElectricityLocal> actualRecords,
-            List<ForecastPowerAverage> forecastRecords,
-            BigDecimal operatingCapacity, List<DataSuperShortTermForecastReport> rawForecastList)
-    {
-        if (forecastRecords == null || forecastRecords.isEmpty()) return BigDecimal.ZERO;
-
-        // 1. 阈值 = 开机容量(MW) * 0.25 (四舍五入保留3位小数)
-        BigDecimal baseThreshold = new BigDecimal("0.25");
-        BigDecimal threshold = operatingCapacity.multiply(baseThreshold).setScale(SCALE, RoundingMode.HALF_UP);
-
-        // 3. 遍历actualRecords计算Si
-        BigDecimal stotal = BigDecimal.ZERO;
-        int n = 0;
-
-        for (DataElectricityLocal r : actualRecords)
-        {
-            if (r.getTm() == null) continue;
-
-            // 将actual的tm转换为unix秒(GMT+8)以匹配forecastMap的key
-            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
-            BigDecimal fp = getAccForecastReportPower(ts, rawForecastList);
-            if (fp == null) continue;
-
-            BigDecimal pm = BigDecimal.valueOf(r.getActualPower()); // kW
-            BigDecimal pp = fp; // kW
-
-            // Si = ((Pmi - Ppi) / max(阈值, Pmi))^2
-            BigDecimal denom = threshold.max(pm);
-            if (denom.compareTo(BigDecimal.ZERO) == 0) continue;
-
-            BigDecimal diff = pm.subtract(pp).divide(denom, 10, RoundingMode.HALF_UP);
-            stotal = stotal.add(diff.multiply(diff));
-            n++;
-        }
-
-        if (n == 0) return BigDecimal.ZERO;
-
-        // 4. 偏差值 = sqrt(sum(Si) / N)
-        BigDecimal meanSq = stotal.divide(BigDecimal.valueOf(n), 20, RoundingMode.HALF_UP);
-        BigDecimal deviation = BigDecimal.valueOf(Math.sqrt(meanSq.doubleValue()));
-
-        // 5. acc = (1 - 偏差值) × 100%
-        BigDecimal acc = deviation.multiply(new BigDecimal("100"))
-                .setScale(SCALE, RoundingMode.HALF_UP);
-
-        // 6. 边界处理
-        if (acc.compareTo(BigDecimal.ZERO) < 0) acc = BigDecimal.ZERO;
-        if (acc.compareTo(new BigDecimal("100")) > 0) acc = new BigDecimal("100");
-
-        return acc;
-    }
-
     /**
      * 获取日前timestamp预报数据
      * @param timestamp
      * @param rawForecastList
      * @return
      */
-    private BigDecimal getAccForecastReportPower(long timestamp, List<DataSuperShortTermForecastReport> rawForecastList)
+    private BigDecimal getAccForecastReportPower(List<DataSuperShortTermForecastReport> rawForecastList, long timestamp)
     {
         List<BigDecimal> matchPowers = rawForecastList.stream()
                 .filter(r -> null != r.getPower() && r.getForecastTime() != null && r.getStartTime() != null && r.getForecastTime() == timestamp && is4HourBeforeTo15MinuteBefore(timestamp, r.getStartTime()))
@@ -552,49 +318,6 @@ public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShort
 
     // ==================== 辅助方法 ====================
 
-    /**
-     * 构建超短期预测数据映射: unix秒 -> power(kW)
-     */
-    private Map<Long, BigDecimal> buildForecastMap(List<ForecastPowerAverage> records)
-    {
-        Map<Long, BigDecimal> map = new LinkedHashMap<>();
-        if (records == null) return map;
-
-        for (ForecastPowerAverage r : records)
-        {
-            BigDecimal power = r.getAvgPower();
-            if (power == null) continue;
-            try
-            {
-                if (r.getForecastTime() != null)
-                {
-                    map.put(r.getForecastTime(), power);
-                }
-            }
-            catch (NumberFormatException e)
-            {
-                // skip
-            }
-        }
-        return map;
-    }
-
-    /**
-     * 安全解析String为Double
-     */
-    private Double parseDouble(String value)
-    {
-        if (value == null || value.isEmpty()) return 0.0;
-        try
-        {
-            return Double.parseDouble(value.trim());
-        }
-        catch (NumberFormatException e)
-        {
-            return 0.0;
-        }
-    }
-
     /**
      * 按日分组
      */