xiaoguohua 1 місяць тому
батько
коміт
b5c174a771
18 змінених файлів з 1980 додано та 32 видалено
  1. 63 3
      forecast-backend-server/src/main/java/com/ufo/project/data/controller/DataController.java
  2. 68 0
      forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/ShortTermPowerForecastReportQuery.java
  3. 128 0
      forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/ShortTermPowerForecastReportVO.java
  4. 68 0
      forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/UltraShortTermPowerForecastReportQuery.java
  5. 129 0
      forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/UltraShortTermPowerForecastReportVO.java
  6. 22 0
      forecast-backend-server/src/main/java/com/ufo/project/data/service/IShortTermPowerForecastReportService.java
  7. 23 0
      forecast-backend-server/src/main/java/com/ufo/project/data/service/IUltraShortTermPowerForecastReportService.java
  8. 661 0
      forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/ShortTermPowerForecastReportServiceImpl.java
  9. 780 0
      forecast-backend-server/src/main/java/com/ufo/project/data/service/impl/UltraShortTermPowerForecastReportServiceImpl.java
  10. 4 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/domain/DataUltraShortTermPowerForecasting.java
  11. 6 6
      forecast-backend-server/src/main/java/com/ufo/project/weather/domain/DataWeatherLocal.java
  12. 5 0
      forecast-backend-server/src/main/resources/mybatis/supershort/DataUltraShortTermPowerForecastingMapper.xml
  13. 6 6
      forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml
  14. 1 0
      forecast-backend-vue/src/components/layout/SideMenu.vue
  15. 2 0
      gateway/forecast-device-gateway/src/main/java/cn/oleauto/device/gateway/dto/DataWeatherPoint.java
  16. 6 17
      gateway/forecast-device-gateway/src/main/java/cn/oleauto/device/gateway/service/WeatherDataSrv.java
  17. 2 0
      release_note/2026.09.03/20260903_dp.sql
  18. 6 0
      release_note/2026.09.17/20260917_dp.sql

+ 63 - 3
forecast-backend-server/src/main/java/com/ufo/project/data/controller/DataController.java

@@ -7,10 +7,8 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -25,6 +23,12 @@ import com.ufo.base.framework.web.page.TableDataInfo;
 import com.ufo.base.common.utils.poi.ExcelUtil;
 import com.ufo.project.data.domain.IrradiancePowerComparisonQuery;
 import com.ufo.project.data.domain.vo.IrradiancePowerComparisonVO;
+import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportQuery;
+import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportVO;
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportQuery;
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
+import com.ufo.project.data.service.IShortTermPowerForecastReportService;
+import com.ufo.project.data.service.IUltraShortTermPowerForecastReportService;
 import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastIssued;
 import com.ufo.project.forecast.shortterm.power.service.IDataShortTermPowerForecastIssuedService;
 import com.ufo.project.forecast.shortterm.weather.domain.DataShortTermWeatherForecastIssued;
@@ -58,6 +62,12 @@ public class DataController extends BaseController
     @Autowired
     private IDataWeatherLocalService weatherLocalService;
 
+    @Autowired
+    private IShortTermPowerForecastReportService shortTermPowerForecastReportService;
+
+    @Autowired
+    private IUltraShortTermPowerForecastReportService ultraShortTermPowerForecastReportService;
+
     /**
      * 辐照度功率对比
      */
@@ -84,6 +94,56 @@ public class DataController extends BaseController
         util.exportExcel(response, list, "辐照度功率对比");
     }
 
+    /**
+     * 短期预测报表(日报、月报、年报)
+     */
+    @PreAuthorize("@ss.hasPermi('data:shortTermPowerForecastReport:list')")
+    @PostMapping("/shortTermPowerForecastReport")
+    public TableDataInfo shortTermPowerForecastReport(@RequestBody ShortTermPowerForecastReportQuery query)
+    {
+        List<ShortTermPowerForecastReportVO> list = shortTermPowerForecastReportService.selectReportList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 短期预测报表导出
+     */
+    @PreAuthorize("@ss.hasPermi('data:shortTermPowerForecastReport:export')")
+    @Log(title = "短期预测报表", businessType = BusinessType.EXPORT)
+    @PostMapping("/shortTermPowerForecastReport/export")
+    public void shortTermPowerForecastReportExport(HttpServletResponse response,
+            @RequestBody ShortTermPowerForecastReportQuery query)
+    {
+        List<ShortTermPowerForecastReportVO> list = shortTermPowerForecastReportService.selectReportList(query);
+        ExcelUtil<ShortTermPowerForecastReportVO> util = new ExcelUtil<>(ShortTermPowerForecastReportVO.class);
+        util.exportExcel(response, list, "短期预测报表");
+    }
+
+    /**
+     * 超短期预测报表(日报、月报、年报)
+     */
+    @PreAuthorize("@ss.hasPermi('data:ultraShortTermPowerForecastReport:list')")
+    @PostMapping("/ultraShortTermPowerForecastReport")
+    public TableDataInfo ultraShortTermPowerForecastReport(@RequestBody UltraShortTermPowerForecastReportQuery query)
+    {
+        List<UltraShortTermPowerForecastReportVO> list = ultraShortTermPowerForecastReportService.selectReportList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 超短期预测报表导出
+     */
+    @PreAuthorize("@ss.hasPermi('data:ultraShortTermPowerForecastReport:export')")
+    @Log(title = "超短期预测报表", businessType = BusinessType.EXPORT)
+    @PostMapping("/ultraShortTermPowerForecastReport/export")
+    public void ultraShortTermPowerForecastReportExport(HttpServletResponse response,
+            @RequestBody UltraShortTermPowerForecastReportQuery query)
+    {
+        List<UltraShortTermPowerForecastReportVO> list = ultraShortTermPowerForecastReportService.selectReportList(query);
+        ExcelUtil<UltraShortTermPowerForecastReportVO> util = new ExcelUtil<>(UltraShortTermPowerForecastReportVO.class);
+        util.exportExcel(response, list, "超短期预测报表");
+    }
+
     /**
      * 构建对比数据列表
      */
@@ -164,7 +224,7 @@ public class DataController extends BaseController
         {
             if (item.getTm() == null) continue;
             long ts = item.getTm().getTime();
-            dataMap.computeIfAbsent(ts, k -> new HashMap<>()).put("measuredIrradiance", item.getInclinedIrradiance());
+            dataMap.computeIfAbsent(ts, k -> new HashMap<>()).put("measuredIrradiance", item.getIrradiance());
         }
     }
 

+ 68 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/ShortTermPowerForecastReportQuery.java

@@ -0,0 +1,68 @@
+package com.ufo.project.data.domain.vo;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 短期预测报表查询参数
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public class ShortTermPowerForecastReportQuery
+{
+    /** 报表类型:daily-日报, monthly-月报, yearly-年报 */
+    @NotBlank(message = "报表类型不能为空")
+    private String type;
+
+    /** 站点/场站 id */
+    @NotBlank(message = "站点id不能为空")
+    private String entityId;
+
+    /** 开始时间 yyyy-MM-dd */
+    @NotBlank(message = "开始时间不能为空")
+    private String startTime;
+
+    /** 结束时间 yyyy-MM-dd */
+    @NotBlank(message = "结束时间不能为空")
+    private String endTime;
+
+    public String getType()
+    {
+        return type;
+    }
+
+    public void setType(String type)
+    {
+        this.type = type;
+    }
+
+    public String getEntityId()
+    {
+        return entityId;
+    }
+
+    public void setEntityId(String entityId)
+    {
+        this.entityId = entityId;
+    }
+
+    public String getStartTime()
+    {
+        return startTime;
+    }
+
+    public void setStartTime(String startTime)
+    {
+        this.startTime = startTime;
+    }
+
+    public String getEndTime()
+    {
+        return endTime;
+    }
+
+    public void setEndTime(String endTime)
+    {
+        this.endTime = endTime;
+    }
+}

+ 128 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/ShortTermPowerForecastReportVO.java

@@ -0,0 +1,128 @@
+package com.ufo.project.data.domain.vo;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 短期预测报表视图对象
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public class ShortTermPowerForecastReportVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 时间(日报: yyyy-MM-dd, 月报: yyyy-MM, 年报: yyyy) */
+    @Excel(name = "时间")
+    private String time;
+
+    /** 开机容量(MW) */
+    @Excel(name = "开机容量(MW)")
+    private BigDecimal operatingCapacity;
+
+    /** 实发功率(MW) */
+    @Excel(name = "实发功率(MW)")
+    private BigDecimal actualPower;
+
+    /** 预测功率(MW) */
+    @Excel(name = "预测功率(MW)")
+    private BigDecimal forecastPower;
+
+    /** 均方根差 */
+    @Excel(name = "均方根差")
+    private BigDecimal rootMeanSquareError;
+
+    /** 平均绝对误差 */
+    @Excel(name = "平均绝对误差")
+    private BigDecimal meanAbsoluteError;
+
+    /** 相关性系数 */
+    @Excel(name = "相关性系数")
+    private BigDecimal correlationCoefficientR;
+
+    /** 最大误差合格率 */
+    @Excel(name = "最大误差合格率")
+    private BigDecimal maximumErrorPassRate;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public BigDecimal getOperatingCapacity()
+    {
+        return operatingCapacity;
+    }
+
+    public void setOperatingCapacity(BigDecimal operatingCapacity)
+    {
+        this.operatingCapacity = operatingCapacity;
+    }
+
+    public BigDecimal getActualPower()
+    {
+        return actualPower;
+    }
+
+    public void setActualPower(BigDecimal actualPower)
+    {
+        this.actualPower = actualPower;
+    }
+
+    public BigDecimal getForecastPower()
+    {
+        return forecastPower;
+    }
+
+    public void setForecastPower(BigDecimal forecastPower)
+    {
+        this.forecastPower = forecastPower;
+    }
+
+    public BigDecimal getRootMeanSquareError()
+    {
+        return rootMeanSquareError;
+    }
+
+    public void setRootMeanSquareError(BigDecimal rootMeanSquareError)
+    {
+        this.rootMeanSquareError = rootMeanSquareError;
+    }
+
+    public BigDecimal getMeanAbsoluteError()
+    {
+        return meanAbsoluteError;
+    }
+
+    public void setMeanAbsoluteError(BigDecimal meanAbsoluteError)
+    {
+        this.meanAbsoluteError = meanAbsoluteError;
+    }
+
+    public BigDecimal getCorrelationCoefficientR()
+    {
+        return correlationCoefficientR;
+    }
+
+    public void setCorrelationCoefficientR(BigDecimal correlationCoefficientR)
+    {
+        this.correlationCoefficientR = correlationCoefficientR;
+    }
+
+    public BigDecimal getMaximumErrorPassRate()
+    {
+        return maximumErrorPassRate;
+    }
+
+    public void setMaximumErrorPassRate(BigDecimal maximumErrorPassRate)
+    {
+        this.maximumErrorPassRate = maximumErrorPassRate;
+    }
+}

+ 68 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/UltraShortTermPowerForecastReportQuery.java

@@ -0,0 +1,68 @@
+package com.ufo.project.data.domain.vo;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 超短期预测报表查询参数
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public class UltraShortTermPowerForecastReportQuery
+{
+    /** 报表类型:daily-日报, monthly-月报, yearly-年报 */
+    @NotBlank(message = "报表类型不能为空")
+    private String type;
+
+    /** 站点/场站 id */
+    @NotBlank(message = "站点id不能为空")
+    private String entityId;
+
+    /** 开始时间 yyyy-MM-dd */
+    @NotBlank(message = "开始时间不能为空")
+    private String startTime;
+
+    /** 结束时间 yyyy-MM-dd */
+    @NotBlank(message = "结束时间不能为空")
+    private String endTime;
+
+    public String getType()
+    {
+        return type;
+    }
+
+    public void setType(String type)
+    {
+        this.type = type;
+    }
+
+    public String getEntityId()
+    {
+        return entityId;
+    }
+
+    public void setEntityId(String entityId)
+    {
+        this.entityId = entityId;
+    }
+
+    public String getStartTime()
+    {
+        return startTime;
+    }
+
+    public void setStartTime(String startTime)
+    {
+        this.startTime = startTime;
+    }
+
+    public String getEndTime()
+    {
+        return endTime;
+    }
+
+    public void setEndTime(String endTime)
+    {
+        this.endTime = endTime;
+    }
+}

+ 129 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/UltraShortTermPowerForecastReportVO.java

@@ -0,0 +1,129 @@
+package com.ufo.project.data.domain.vo;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 超短期预测报表视图对象
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public class UltraShortTermPowerForecastReportVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 时间(日报: yyyy-MM-dd, 月报: yyyy-MM, 年报: yyyy) */
+    @Excel(name = "时间")
+    private String time;
+
+    /** 开机容量(MW) */
+    @Excel(name = "开机容量(MW)")
+    private BigDecimal operatingCapacity;
+
+    /** 实发功率(MW) */
+    @Excel(name = "实发功率(MW)")
+    private BigDecimal actualPower;
+
+    /** 预测功率(MW) */
+    @Excel(name = "预测功率(MW)")
+    private BigDecimal forecastPower;
+
+    /** 均方根差 */
+    @Excel(name = "均方根差")
+    private BigDecimal rootMeanSquareError;
+
+    /** 平均绝对误差 */
+    @Excel(name = "平均绝对误差")
+    private BigDecimal meanAbsoluteError;
+
+    /** 相关性系数 */
+    @Excel(name = "相关性系数")
+    private BigDecimal correlationCoefficientR;
+
+    /** 4小时前预测准确率 */
+    @Excel(name = "4小时前预测准确率")
+    private BigDecimal aheadForecastAccuracy4h;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public BigDecimal getOperatingCapacity()
+    {
+        return operatingCapacity;
+    }
+
+    public void setOperatingCapacity(BigDecimal operatingCapacity)
+    {
+        this.operatingCapacity = operatingCapacity;
+    }
+
+    public BigDecimal getActualPower()
+    {
+        return actualPower;
+    }
+
+    public void setActualPower(BigDecimal actualPower)
+    {
+        this.actualPower = actualPower;
+    }
+
+    public BigDecimal getForecastPower()
+    {
+        return forecastPower;
+    }
+
+    public void setForecastPower(BigDecimal forecastPower)
+    {
+        this.forecastPower = forecastPower;
+    }
+
+    public BigDecimal getRootMeanSquareError()
+    {
+        return rootMeanSquareError;
+    }
+
+    public void setRootMeanSquareError(BigDecimal rootMeanSquareError)
+    {
+        this.rootMeanSquareError = rootMeanSquareError;
+    }
+
+    public BigDecimal getMeanAbsoluteError()
+    {
+        return meanAbsoluteError;
+    }
+
+    public void setMeanAbsoluteError(BigDecimal meanAbsoluteError)
+    {
+        this.meanAbsoluteError = meanAbsoluteError;
+    }
+
+    public BigDecimal getCorrelationCoefficientR()
+    {
+        return correlationCoefficientR;
+    }
+
+    public void setCorrelationCoefficientR(BigDecimal correlationCoefficientR)
+    {
+        this.correlationCoefficientR = correlationCoefficientR;
+    }
+
+    public BigDecimal getAheadForecastAccuracy4h()
+    {
+        return aheadForecastAccuracy4h;
+    }
+
+    public void setAheadForecastAccuracy4h(BigDecimal aheadForecastAccuracy4h)
+    {
+        this.aheadForecastAccuracy4h = aheadForecastAccuracy4h;
+    }
+}

+ 22 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/service/IShortTermPowerForecastReportService.java

@@ -0,0 +1,22 @@
+package com.ufo.project.data.service;
+
+import java.util.List;
+import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportQuery;
+import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportVO;
+
+/**
+ * 短期预测报表Service接口
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public interface IShortTermPowerForecastReportService
+{
+    /**
+     * 查询短期预测报表数据
+     *
+     * @param query 查询参数
+     * @return 报表数据列表
+     */
+    List<ShortTermPowerForecastReportVO> selectReportList(ShortTermPowerForecastReportQuery query);
+}

+ 23 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/service/IUltraShortTermPowerForecastReportService.java

@@ -0,0 +1,23 @@
+package com.ufo.project.data.service;
+
+import java.util.List;
+
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportQuery;
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
+
+/**
+ * 超短期预测报表Service接口
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+public interface IUltraShortTermPowerForecastReportService
+{
+    /**
+     * 查询超短期预测报表列表
+     *
+     * @param query 查询参数
+     * @return 报表列表
+     */
+    List<UltraShortTermPowerForecastReportVO> selectReportList(UltraShortTermPowerForecastReportQuery query);
+}

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

@@ -0,0 +1,661 @@
+package com.ufo.project.data.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import javax.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ufo.base.common.constant.CacheConstants;
+import com.ufo.base.common.utils.StringUtils;
+import com.ufo.module.system.domain.SysDictData;
+import com.ufo.module.system.service.ISysDictTypeService;
+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.service.IDataShortTermPowerForecastIssuedService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
+
+/**
+ * 短期预测报表Service业务层处理
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+@Service
+public class ShortTermPowerForecastReportServiceImpl implements IShortTermPowerForecastReportService
+{
+    private static final int SCALE = 4;
+
+    /** Dict类型:短期预测误差阈值 */
+    private static final String DICT_TYPE_SHORT_TERM_ERROR_THRESHOLD = "short_term_error_threshold";
+
+    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 YEAR_FMT = DateTimeFormatter.ofPattern("yyyy");
+
+    private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
+
+    private String errorThreshold;
+
+    @Autowired
+    private IDataElectricityLocalService electricityLocalService;
+
+    @Autowired
+    private IDataShortTermPowerForecastIssuedService powerForecastIssuedService;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    /**
+     * 初始化阈值配置
+     */
+    @PostConstruct
+    public void init()
+    {
+        refreshErrorThreshold();
+    }
+
+    @Override
+    public List<ShortTermPowerForecastReportVO> selectReportList(ShortTermPowerForecastReportQuery query)
+    {
+        String type = query.getType();
+        String entityId = query.getEntityId();
+        String startTime = query.getStartTime();
+        String endTime = query.getEndTime();
+
+        // 1. 获取实发功率数据
+        List<DataElectricityLocal> actualList = electricityLocalService.selectActualPowerList(
+                parseLong(entityId), startTime, endTime);
+        if (actualList == null || actualList.isEmpty())
+        {
+            return Collections.emptyList();
+        }
+
+        // 2. 获取预测功率数据
+        List<DataShortTermPowerForecastIssued> forecastList = powerForecastIssuedService.selectByTimeRange(
+                entityId, parseLong(startTime), parseLong(endTime));
+
+        // 3. 按基准类型分组
+        switch (type)
+        {
+            case "daily":
+                return buildDailyReport(actualList, forecastList);
+            case "monthly":
+                return buildMonthlyReport(actualList, forecastList);
+            case "yearly":
+                return buildYearlyReport(actualList, forecastList);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 构建日报
+     */
+    private List<ShortTermPowerForecastReportVO> buildDailyReport(List<DataElectricityLocal> actualList,
+            List<DataShortTermPowerForecastIssued> forecastList)
+    {
+        Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
+        Map<String, List<DataShortTermPowerForecastIssued>> 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());
+
+            ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
+            vo.setTime(day);
+            computeDailyMetrics(vo, records, forecastRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 构建月报
+     */
+    private List<ShortTermPowerForecastReportVO> buildMonthlyReport(List<DataElectricityLocal> actualList,
+            List<DataShortTermPowerForecastIssued> forecastList)
+    {
+        Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
+        Map<String, List<DataShortTermPowerForecastIssued>> 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 =
+                    forecastMonthlyMap.getOrDefault(month, Collections.emptyList());
+
+            ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
+            vo.setTime(month);
+            computeDailyMetrics(vo, records, forecastRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 构建年报
+     */
+    private List<ShortTermPowerForecastReportVO> buildYearlyReport(List<DataElectricityLocal> actualList,
+            List<DataShortTermPowerForecastIssued> forecastList)
+    {
+        Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
+        Map<String, List<DataShortTermPowerForecastIssued>> 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 =
+                    forecastYearlyMap.getOrDefault(year, Collections.emptyList());
+
+            ShortTermPowerForecastReportVO vo = new ShortTermPowerForecastReportVO();
+            vo.setTime(year);
+            computeDailyMetrics(vo, records, forecastRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 计算指标(日报/月报/年报通用)
+     */
+    private void computeDailyMetrics(ShortTermPowerForecastReportVO vo,
+            List<DataElectricityLocal> actualRecords, List<DataShortTermPowerForecastIssued> forecastRecords)
+    {
+        // 1. operatingCapacity: 筛选 device_status = 0 AND grid_status = 1, MAX(actual_power) / 1000
+        BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
+        vo.setOperatingCapacity(operatingCapacity);
+
+        // 2. actualPower: 筛选 grid_status = 1, AVG(actual_power) / 1000
+        BigDecimal actualPower = computeActualPower(actualRecords);
+        vo.setActualPower(actualPower);
+
+        // 3. forecastPower: AVG(power) / 1000
+        BigDecimal forecastPower = computeForecastPower(forecastRecords);
+        vo.setForecastPower(forecastPower);
+
+        // 4. RMSE
+        BigDecimal rmse = computeRmse(actualRecords, forecastRecords);
+        vo.setRootMeanSquareError(rmse);
+
+        // 5. MAE
+        BigDecimal mae = computeMae(actualRecords, forecastRecords);
+        vo.setMeanAbsoluteError(mae);
+
+        // 6. correlationCoefficientR
+        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords);
+        vo.setCorrelationCoefficientR(corr);
+
+        // 7. maximumErrorPassRate: 基于逐点校验的合格率
+        BigDecimal passRate = computeMaximumErrorPassRate(actualRecords, forecastRecords, operatingCapacity);
+        vo.setMaximumErrorPassRate(passRate);
+    }
+
+    /**
+     * 计算开机容量
+     */
+    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);
+    }
+
+    /**
+     * 计算实发功率
+     */
+    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);
+    }
+
+    /**
+     * 计算预测功率
+     */
+    private BigDecimal computeForecastPower(List<DataShortTermPowerForecastIssued> records)
+    {
+        double sum = 0.0;
+        int count = 0;
+        for (DataShortTermPowerForecastIssued r : records)
+        {
+            String power = r.getPower();
+            if (StringUtils.isNotEmpty(power))
+            {
+                try
+                {
+                    sum += Double.parseDouble(power.trim());
+                    count++;
+                }
+                catch (NumberFormatException e)
+                {
+                    // skip
+                }
+            }
+        }
+        if (count == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(sum / count / 1000.0);
+    }
+
+    /**
+     * 计算RMSE
+     */
+    private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
+            List<DataShortTermPowerForecastIssued> forecastRecords)
+    {
+        // 按时间戳聚合预测数据(以分钟级时间戳为key)
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+
+        if (forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double sumSquaredError = 0.0;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            Long ts = r.getTm().getTime() / 1000;
+            Double 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;
+            count++;
+        }
+
+        if (count == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double rmse = Math.sqrt(sumSquaredError / count);
+        return BigDecimal.valueOf(rmse).setScale(4, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 计算MAE
+     */
+    private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
+            List<DataShortTermPowerForecastIssued> forecastRecords)
+    {
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+
+        if (forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double sumAbsError = 0.0;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            Long ts = r.getTm().getTime() / 1000;
+            Double 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;
+            count++;
+        }
+
+        if (count == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double mae = sumAbsError / count;
+        return BigDecimal.valueOf(mae).setScale(4, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 计算相关性系数 R
+     */
+    private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
+            List<DataShortTermPowerForecastIssued> forecastRecords)
+    {
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+
+        if (forecastMap.isEmpty())
+        {
+            return BigDecimal.ZERO;
+        }
+
+        // 收集成对数据
+        List<Double> xList = new ArrayList<>();
+        List<Double> yList = new ArrayList<>();
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            Long ts = r.getTm().getTime() / 1000;
+            Double 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);
+        }
+
+        int n = xList.size();
+        if (n < 2)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        // 计算均值
+        double meanX = xList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+        double meanY = yList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+
+        // 计算相关系数
+        double numerator = 0.0;
+        double sumSqX = 0.0;
+        double sumSqY = 0.0;
+
+        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;
+        }
+
+        double denominator = Math.sqrt(sumSqX * sumSqY);
+        if (denominator == 0.0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double r = numerator / denominator;
+        return BigDecimal.valueOf(r).setScale(4, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 计算最大误差合格率
+     */
+    private BigDecimal computeMaximumErrorPassRate(List<DataElectricityLocal> actualRecords,
+            List<DataShortTermPowerForecastIssued> forecastRecords,
+            BigDecimal operatingCapacity)
+    {
+        if (operatingCapacity == null || operatingCapacity.compareTo(BigDecimal.ZERO) == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        String threshold = getErrorThreshold();
+        if (StringUtils.isEmpty(threshold))
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double thresholdValue;
+        try
+        {
+            thresholdValue = Double.parseDouble(threshold);
+        }
+        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);
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null || r.getActualPower() == null) continue;
+
+            Long ts = r.getTm().getTime() / 1000;
+            Double fp = forecastMap.get(ts);
+            if (fp == null) continue;
+
+            totalCount++;
+            double errorMW = Math.abs(r.getActualPower() - fp) / 1000.0;
+            if (errorMW <= thresholdValue * capacityMW)
+            {
+                passCount++;
+            }
+        }
+
+        if (totalCount == 0)
+        {
+            return BigDecimal.ZERO;
+        }
+
+        double rate = (passCount / totalCount) * 100.0;
+        return BigDecimal.valueOf(rate).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 构建预测数据映射:时间戳(秒) -> power(kW)
+     */
+    private Map<Long, Double> buildForecastMap(List<DataShortTermPowerForecastIssued> records)
+    {
+        Map<Long, Double> map = new LinkedHashMap<>();
+        if (records == null) return map;
+
+        for (DataShortTermPowerForecastIssued r : records)
+        {
+            if (r.getForecastTime() == null) continue;
+            String power = r.getPower();
+            if (StringUtils.isEmpty(power)) continue;
+
+            try
+            {
+                map.put(r.getForecastTime(), Double.parseDouble(power.trim()));
+            }
+            catch (NumberFormatException e)
+            {
+                // skip
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 按日分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByDay(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r -> LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI)
+                                .format(DATE_FMT),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按月分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByMonth(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r ->
+                        {
+                            LocalDate ld = LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI);
+                            return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
+                        },
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按年分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByYear(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r -> String.valueOf(LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI).getYear()),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按日分组预测数据
+     */
+    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByDay(
+            List<DataShortTermPowerForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r -> LocalDate.ofEpochDay(r.getForecastTime() / 86400L)
+                                .format(DATE_FMT),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按月分组预测数据
+     */
+    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByMonth(
+            List<DataShortTermPowerForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r ->
+                        {
+                            LocalDate ld = LocalDate.ofEpochDay(r.getForecastTime() / 86400L);
+                            return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
+                        },
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按年分组预测数据
+     */
+    private Map<String, List<DataShortTermPowerForecastIssued>> groupForecastByYear(
+            List<DataShortTermPowerForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r -> String.valueOf(LocalDate.ofEpochDay(r.getForecastTime() / 86400L).getYear()),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 安全解析Long
+     */
+    private Long parseLong(String value)
+    {
+        if (value == null || value.isEmpty()) return 0L;
+        try
+        {
+            return Long.parseLong(value.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return 0L;
+        }
+    }
+
+    /**
+     * 获取误差阈值
+     */
+    private String getErrorThreshold()
+    {
+        List<SysDictData> dictList = dictTypeService.selectDictDataByType(DICT_TYPE_SHORT_TERM_ERROR_THRESHOLD);
+        if (dictList != null && !dictList.isEmpty())
+        {
+            return dictList.get(0).getDictValue();
+        }
+        return null;
+    }
+
+    /**
+     * 刷新阈值配置
+     */
+    private void refreshErrorThreshold()
+    {
+        try
+        {
+            List<SysDictData> dictList = dictTypeService.selectDictDataByType(DICT_TYPE_SHORT_TERM_ERROR_THRESHOLD);
+            if (dictList != null && !dictList.isEmpty())
+            {
+                errorThreshold = dictList.get(0).getDictValue();
+            }
+        }
+        catch (Exception e)
+        {
+            errorThreshold = "0.2"; // 默认阈值 20%
+        }
+    }
+}

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

@@ -0,0 +1,780 @@
+package com.ufo.project.data.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import javax.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ufo.base.common.utils.StringUtils;
+import com.ufo.module.system.domain.SysDictData;
+import com.ufo.module.system.service.ISysDictTypeService;
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportQuery;
+import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
+import com.ufo.project.data.service.IUltraShortTermPowerForecastReportService;
+import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastIssued;
+import com.ufo.project.forecast.supershort.domain.DataUltraShortTermPowerForecasting;
+import com.ufo.project.forecast.supershort.service.IDataSuperShortTermForecastIssuedService;
+import com.ufo.project.forecast.supershort.service.IDataUltraShortTermPowerForecastingService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
+
+/**
+ * 超短期预测报表Service业务层处理
+ *
+ * @author ufo
+ * @date 2026-09-07
+ */
+@Service
+public class UltraShortTermPowerForecastReportServiceImpl implements IUltraShortTermPowerForecastReportService
+{
+    private static final int SCALE = 4;
+    private static final long SECONDS_PER_DAY = 86400L;
+    private static final long SECONDS_PER_HOUR = 3600L;
+    private static final long SECONDS_PER_4H = SECONDS_PER_HOUR * 4L;
+
+    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 YEAR_FMT = DateTimeFormatter.ofPattern("yyyy");
+
+    private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
+    private static final ZoneOffset ZONE_8 = ZoneOffset.ofHours(8);
+
+    @Autowired
+    private IDataElectricityLocalService electricityLocalService;
+
+    @Autowired
+    private IDataSuperShortTermForecastIssuedService superShortService;
+
+    @Autowired
+    private IDataUltraShortTermPowerForecastingService ultraForecastService;
+
+    /**
+     * 超短期预测报表查询
+     */
+    @Override
+    public List<UltraShortTermPowerForecastReportVO> selectReportList(UltraShortTermPowerForecastReportQuery query)
+    {
+        String type = query.getType();
+        String entityId = query.getEntityId();
+        String startTime = query.getStartTime();
+        String endTime = query.getEndTime();
+
+        // 1. 获取实发功率数据
+        String startStr = startTime + " 00:00:00";
+        String endStr = endTime + " 23:59:59";
+        List<DataElectricityLocal> actualList = electricityLocalService.selectActualPowerList(
+                parseLong(entityId), startStr, endStr);
+        if (actualList == null || actualList.isEmpty())
+        {
+            return Collections.emptyList();
+        }
+
+        // 2. 获取超短期预测功率数据
+        Long startSec = toUnixSeconds(startStr);
+        Long endSec = toUnixSeconds(endStr);
+        List<DataSuperShortTermForecastIssued> forecastList = selectSuperShortByTimeRange(entityId, startSec, endSec);
+
+        // 3. 获取超短期预报数据(提前4h)
+        Long startSec4h = startSec - SECONDS_PER_4H;
+        List<DataUltraShortTermPowerForecasting> ultraForecastList = ultraForecastService
+                .selectByTimeRange(entityId, startSec4h, endSec);
+
+        // 4. 按基准类型分组
+        switch (type)
+        {
+            case "daily":
+                return buildDailyReport(actualList, forecastList, ultraForecastList);
+            case "monthly":
+                return buildMonthlyReport(actualList, forecastList, ultraForecastList);
+            case "yearly":
+                return buildYearlyReport(actualList, forecastList, ultraForecastList);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 构建日报
+     */
+    private List<UltraShortTermPowerForecastReportVO> buildDailyReport(
+            List<DataElectricityLocal> actualList,
+            List<DataSuperShortTermForecastIssued> forecastList,
+            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+    {
+        Map<String, List<DataElectricityLocal>> dailyMap = groupByDay(actualList);
+        Map<String, List<DataSuperShortTermForecastIssued>> forecastDailyMap = groupForecastByDay(forecastList);
+        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+
+        List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
+        for (Entry<String, List<DataElectricityLocal>> entry : dailyMap.entrySet())
+        {
+            String day = entry.getKey();
+            List<DataElectricityLocal> records = entry.getValue();
+            List<DataSuperShortTermForecastIssued> forecastRecords = forecastDailyMap.getOrDefault(day, Collections.emptyList());
+            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForDay(day, ultraForecastMap);
+
+            UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
+            vo.setTime(day);
+            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 构建月报
+     */
+    private List<UltraShortTermPowerForecastReportVO> buildMonthlyReport(
+            List<DataElectricityLocal> actualList,
+            List<DataSuperShortTermForecastIssued> forecastList,
+            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+    {
+        Map<String, List<DataElectricityLocal>> monthlyMap = groupByMonth(actualList);
+        Map<String, List<DataSuperShortTermForecastIssued>> forecastMonthlyMap = groupForecastByMonth(forecastList);
+        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+
+        List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
+        for (Entry<String, List<DataElectricityLocal>> entry : monthlyMap.entrySet())
+        {
+            String month = entry.getKey();
+            List<DataElectricityLocal> records = entry.getValue();
+            List<DataSuperShortTermForecastIssued> forecastRecords =
+                    forecastMonthlyMap.getOrDefault(month, Collections.emptyList());
+            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForMonth(month, ultraForecastMap);
+
+            UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
+            vo.setTime(month);
+            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 构建年报
+     */
+    private List<UltraShortTermPowerForecastReportVO> buildYearlyReport(
+            List<DataElectricityLocal> actualList,
+            List<DataSuperShortTermForecastIssued> forecastList,
+            List<DataUltraShortTermPowerForecasting> ultraForecastList)
+    {
+        Map<String, List<DataElectricityLocal>> yearlyMap = groupByYear(actualList);
+        Map<String, List<DataSuperShortTermForecastIssued>> forecastYearlyMap = groupForecastByYear(forecastList);
+        Map<Long, List<DataUltraShortTermPowerForecasting>> ultraForecastMap = groupUltraForecastByStartSec(ultraForecastList);
+
+        List<UltraShortTermPowerForecastReportVO> result = new ArrayList<>();
+        for (Entry<String, List<DataElectricityLocal>> entry : yearlyMap.entrySet())
+        {
+            String year = entry.getKey();
+            List<DataElectricityLocal> records = entry.getValue();
+            List<DataSuperShortTermForecastIssued> forecastRecords =
+                    forecastYearlyMap.getOrDefault(year, Collections.emptyList());
+            List<DataUltraShortTermPowerForecasting> ultraRecords = collectUltraForYear(year, ultraForecastMap);
+
+            UltraShortTermPowerForecastReportVO vo = new UltraShortTermPowerForecastReportVO();
+            vo.setTime(year);
+            computeMetrics(vo, records, forecastRecords, ultraRecords);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 计算指标
+     */
+    private void computeMetrics(UltraShortTermPowerForecastReportVO vo,
+            List<DataElectricityLocal> actualRecords,
+            List<DataSuperShortTermForecastIssued> forecastRecords,
+            List<DataUltraShortTermPowerForecasting> ultraRecords)
+    {
+        // 1. 开机容量
+        BigDecimal operatingCapacity = computeOperatingCapacity(actualRecords);
+        vo.setOperatingCapacity(operatingCapacity);
+
+        // 2. 实发功率
+        BigDecimal actualPower = computeActualPower(actualRecords);
+        vo.setActualPower(actualPower);
+
+        // 3. 预测功率(超短期预测功率)
+        BigDecimal forecastPower = computeForecastPower(forecastRecords);
+        vo.setForecastPower(forecastPower);
+
+        // 4. RMSE
+        BigDecimal rmse = computeRmse(actualRecords, forecastRecords);
+        vo.setRootMeanSquareError(rmse);
+
+        // 5. MAE
+        BigDecimal mae = computeMae(actualRecords, forecastRecords);
+        vo.setMeanAbsoluteError(mae);
+
+        // 6. 相关性系数
+        BigDecimal corr = computeCorrelation(actualRecords, forecastRecords);
+        vo.setCorrelationCoefficientR(corr);
+
+        // 7. 4小时前预测准确率
+        BigDecimal accuracy = computeAheadForecastAccuracy4h(actualRecords, ultraRecords, operatingCapacity);
+        vo.setAheadForecastAccuracy4h(accuracy);
+    }
+
+    // ==================== 基础计算 ====================
+
+    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);
+    }
+
+    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);
+    }
+
+    private BigDecimal computeForecastPower(List<DataSuperShortTermForecastIssued> records)
+    {
+        double sum = 0.0;
+        int count = 0;
+        for (DataSuperShortTermForecastIssued r : records)
+        {
+            String power = r.getPower();
+            if (StringUtils.isNotEmpty(power))
+            {
+                try
+                {
+                    sum += Double.parseDouble(power.trim());
+                    count++;
+                }
+                catch (NumberFormatException e)
+                {
+                    // skip
+                }
+            }
+        }
+        if (count == 0) return BigDecimal.ZERO;
+        return BigDecimal.valueOf(sum / count / 1000.0);
+    }
+
+    private BigDecimal computeRmse(List<DataElectricityLocal> actualRecords,
+            List<DataSuperShortTermForecastIssued> forecastRecords)
+    {
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
+
+        double sumSquaredError = 0.0;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
+            Double 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;
+            count++;
+        }
+
+        if (count == 0) return BigDecimal.ZERO;
+        return BigDecimal.valueOf(Math.sqrt(sumSquaredError / count)).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal computeMae(List<DataElectricityLocal> actualRecords,
+            List<DataSuperShortTermForecastIssued> forecastRecords)
+    {
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
+
+        double sumAbsError = 0.0;
+        int count = 0;
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
+            Double 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;
+            count++;
+        }
+
+        if (count == 0) return BigDecimal.ZERO;
+        return BigDecimal.valueOf(sumAbsError / count).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal computeCorrelation(List<DataElectricityLocal> actualRecords,
+            List<DataSuperShortTermForecastIssued> forecastRecords)
+    {
+        Map<Long, Double> forecastMap = buildForecastMap(forecastRecords);
+        if (forecastMap.isEmpty()) return BigDecimal.ZERO;
+
+        List<Double> xList = new ArrayList<>();
+        List<Double> yList = new ArrayList<>();
+
+        for (DataElectricityLocal r : actualRecords)
+        {
+            if (r.getTm() == null) continue;
+            long ts = r.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
+            Double 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);
+        }
+
+        int n = xList.size();
+        if (n < 2) return BigDecimal.ZERO;
+
+        double meanX = xList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+        double meanY = yList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+
+        double numerator = 0.0;
+        double sumSqX = 0.0;
+        double sumSqY = 0.0;
+
+        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;
+        }
+
+        double denominator = Math.sqrt(sumSqX * sumSqY);
+        if (denominator == 0.0) return BigDecimal.ZERO;
+
+        double r = numerator / denominator;
+        return BigDecimal.valueOf(r).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 计算4小时前预测准确率
+     *
+     * 对每个实际数据点,找到start_time匹配的超短期预报记录
+     * 从该记录的power15~power240字段提取16个预测值,取算术平均得P_Pi
+     * error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
+     * ACC = 1 - sqrt((1/N) * sum(error_i^2))
+     */
+    private BigDecimal computeAheadForecastAccuracy4h(List<DataElectricityLocal> actualRecords,
+            List<DataUltraShortTermPowerForecasting> ultraRecords,
+            BigDecimal operatingCapacity)
+    {
+        if (ultraRecords == null || ultraRecords.isEmpty()) return BigDecimal.ZERO;
+        if (operatingCapacity == null || operatingCapacity.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
+
+        double capacityMW = operatingCapacity.doubleValue();
+        double thresholdMW = Math.max(0.2 * capacityMW, 0.001); // 防止分母过小
+
+        // 构建超短期预报数据映射: start_time(秒) -> 记录
+        Map<Long, DataUltraShortTermPowerForecasting> ultraMap = new LinkedHashMap<>();
+        for (DataUltraShortTermPowerForecasting r : ultraRecords)
+        {
+            if (r.getStartTime() != null)
+            {
+                ultraMap.put(r.getStartTime(), r);
+            }
+        }
+
+        double sumSquaredError = 0.0;
+        int count = 0;
+
+        for (DataElectricityLocal actual : actualRecords)
+        {
+            if (actual.getTm() == null || actual.getActualPower() == null) continue;
+
+            // 实际数据的unix秒时间戳
+            long actualTs = actual.getTm().toInstant().atZone(ZONE_SHANGHAI).toEpochSecond();
+
+            // 查找start_time匹配的超短期预报记录
+            DataUltraShortTermPowerForecasting ultra = ultraMap.get(actualTs);
+            if (ultra == null) continue;
+
+            // 计算P_Pi = (power15 + power30 + ... + power240) / 16 的算术平均
+            double ppSum = 0.0;
+            int ppCount = 0;
+
+            String[] powerFields = {
+                    "power15", "power30", "power45", "power60",
+                    "power75", "power90", "power105", "power120",
+                    "power135", "power150", "power165", "power180",
+                    "power195", "power210", "power225", "power240"
+            };
+
+            for (String field : powerFields)
+            {
+                Double value = getPowerField(ultra, field);
+                if (value != null)
+                {
+                    ppSum += value;
+                    ppCount++;
+                }
+            }
+
+            if (ppCount == 0) continue;
+            double ppMean = ppSum / ppCount; // P_Pi (kW)
+
+            // P_Mi (kW)
+            double pm = actual.getActualPower();
+
+            // error_i = (P_Mi - P_Pi) / max(P_Mi, 0.2 * Cap)
+            double denominator = Math.max(Math.abs(pm), thresholdMW);
+            double error = (pm - ppMean) / denominator;
+
+            sumSquaredError += error * error;
+            count++;
+        }
+
+        if (count == 0) return BigDecimal.ZERO;
+
+        double acc = 1.0 - Math.sqrt(sumSquaredError / count);
+        return BigDecimal.valueOf(acc).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 安全获取Power字段值
+     */
+    private Double getPowerField(DataUltraShortTermPowerForecasting record, String fieldName)
+    {
+        try
+        {
+            switch (fieldName)
+            {
+                case "power15":  return parseDouble(record.getPower15());
+                case "power30":  return parseDouble(record.getPower30());
+                case "power45":  return parseDouble(record.getPower45());
+                case "power60":  return parseDouble(record.getPower60());
+                case "power75":  return parseDouble(record.getPower75());
+                case "power90":  return parseDouble(record.getPower90());
+                case "power105": return parseDouble(record.getPower105());
+                case "power120": return parseDouble(record.getPower120());
+                case "power135": return parseDouble(record.getPower135());
+                case "power150": return parseDouble(record.getPower150());
+                case "power165": return parseDouble(record.getPower165());
+                case "power180": return parseDouble(record.getPower180());
+                case "power195": return parseDouble(record.getPower195());
+                case "power210": return parseDouble(record.getPower210());
+                case "power225": return parseDouble(record.getPower225());
+                case "power240": return parseDouble(record.getPower240());
+                default: return null;
+            }
+        }
+        catch (Exception e)
+        {
+            return null;
+        }
+    }
+
+    // ==================== 辅助方法 ====================
+
+    /**
+     * 构建超短期预测数据映射: unix秒 -> power(kW)
+     */
+    private Map<Long, Double> buildForecastMap(List<DataSuperShortTermForecastIssued> records)
+    {
+        Map<Long, Double> map = new LinkedHashMap<>();
+        if (records == null) return map;
+
+        for (DataSuperShortTermForecastIssued r : records)
+        {
+            String power = r.getPower();
+            if (StringUtils.isEmpty(power)) continue;
+            try
+            {
+                Double p = Double.parseDouble(power.trim());
+                if (r.getForecastTime() != null)
+                {
+                    map.put(r.getForecastTime(), p);
+                }
+                if (r.getStartTime() != null)
+                {
+                    map.putIfAbsent(r.getStartTime(), p);
+                }
+            }
+            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;
+        }
+    }
+
+    /**
+     * 安全解析String为Long
+     */
+    private Long parseLong(String value)
+    {
+        if (value == null || value.isEmpty()) return 0L;
+        try
+        {
+            return Long.parseLong(value.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return 0L;
+        }
+    }
+
+    /**
+     * 将yyyy-MM-dd HH:mm:ss字符串转换为unix秒级时间戳
+     */
+    private Long toUnixSeconds(String dateTime)
+    {
+        return java.time.LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+                .atZone(ZONE_8)
+                .toInstant()
+                .getEpochSecond();
+    }
+
+    /**
+     * 按日分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByDay(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r -> LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI).format(DATE_FMT),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按月分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByMonth(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r ->
+                        {
+                            LocalDate ld = LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI);
+                            return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
+                        },
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按年分组
+     */
+    private Map<String, List<DataElectricityLocal>> groupByYear(List<DataElectricityLocal> records)
+    {
+        return records.stream()
+                .filter(r -> r.getTm() != null)
+                .collect(Collectors.groupingBy(
+                        r -> String.valueOf(LocalDate.ofInstant(r.getTm().toInstant(), ZONE_SHANGHAI).getYear()),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按日分组超短期预测数据
+     */
+    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByDay(
+            List<DataSuperShortTermForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r -> LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY).format(DATE_FMT),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按月分组超短期预测数据
+     */
+    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByMonth(
+            List<DataSuperShortTermForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r ->
+                        {
+                            LocalDate ld = LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY);
+                            return YearMonth.of(ld.getYear(), ld.getMonth()).format(MONTH_FMT);
+                        },
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按年分组超短期预测数据
+     */
+    private Map<String, List<DataSuperShortTermForecastIssued>> groupForecastByYear(
+            List<DataSuperShortTermForecastIssued> records)
+    {
+        return records.stream()
+                .filter(r -> r.getForecastTime() != null)
+                .collect(Collectors.groupingBy(
+                        r -> String.valueOf(LocalDate.ofEpochDay(r.getForecastTime() / SECONDS_PER_DAY).getYear()),
+                        LinkedHashMap::new,
+                        Collectors.toList()));
+    }
+
+    /**
+     * 按start_time秒分组超短期预报数据
+     */
+    private Map<Long, List<DataUltraShortTermPowerForecasting>> groupUltraForecastByStartSec(
+            List<DataUltraShortTermPowerForecasting> records)
+    {
+        Map<Long, List<DataUltraShortTermPowerForecasting>> map = new LinkedHashMap<>();
+        if (records == null) return map;
+        for (DataUltraShortTermPowerForecasting r : records)
+        {
+            if (r.getStartTime() != null)
+            {
+                map.computeIfAbsent(r.getStartTime(), k -> new ArrayList<>()).add(r);
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 收集某日对应的超短期预报记录
+     */
+    private List<DataUltraShortTermPowerForecasting> collectUltraForDay(
+            String day, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
+    {
+        LocalDate ld = LocalDate.parse(day, DATE_FMT);
+        long dayStartSec = ld.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
+        return collectByStartSec(dayStartSec, SECONDS_PER_DAY, ultraMap);
+    }
+
+    /**
+     * 收集某月对应的超短期预报记录
+     */
+    private List<DataUltraShortTermPowerForecasting> collectUltraForMonth(
+            String month, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
+    {
+        YearMonth ym = YearMonth.parse(month, MONTH_FMT);
+        LocalDate startDate = ym.atDay(1);
+        LocalDate endDate = ym.atEndOfMonth();
+        long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
+        long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
+        long duration = endSec - startSec;
+        return collectByStartSecRange(startSec, duration, ultraMap);
+    }
+
+    /**
+     * 收集某年对应的超短期预报记录
+     */
+    private List<DataUltraShortTermPowerForecasting> collectUltraForYear(
+            String year, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
+    {
+        int y = Integer.parseInt(year);
+        LocalDate startDate = LocalDate.of(y, 1, 1);
+        LocalDate endDate = LocalDate.of(y, 12, 31);
+        long startSec = startDate.atStartOfDay(ZONE_SHANGHAI).toInstant().getEpochSecond();
+        long endSec = endDate.atTime(23, 59, 59).atZone(ZONE_SHANGHAI).toInstant().getEpochSecond();
+        long duration = endSec - startSec;
+        return collectByStartSecRange(startSec, duration, ultraMap);
+    }
+
+    /**
+     * 根据start_time范围收集超短期预报记录
+     */
+    private List<DataUltraShortTermPowerForecasting> collectByStartSec(
+            long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
+    {
+        long endSec = startSec + duration;
+        return ultraMap.keySet().stream()
+                .filter(k -> k >= startSec && k < endSec)
+                .flatMap(k -> ultraMap.get(k).stream())
+                .collect(Collectors.toList());
+    }
+
+    private List<DataUltraShortTermPowerForecasting> collectByStartSecRange(
+            long startSec, long duration, Map<Long, List<DataUltraShortTermPowerForecasting>> ultraMap)
+    {
+        long endSec = startSec + duration;
+        List<DataUltraShortTermPowerForecasting> result = new ArrayList<>();
+        for (long k = startSec; k < endSec; k += SECONDS_PER_DAY)
+        {
+            List<DataUltraShortTermPowerForecasting> dayRecords = collectByStartSec(k, SECONDS_PER_DAY, ultraMap);
+            result.addAll(dayRecords);
+        }
+        return result;
+    }
+
+    /**
+     * 查询超短期预测功率数据列表
+     */
+    private List<DataSuperShortTermForecastIssued> selectSuperShortByTimeRange(
+            String entityId, Long startTime, Long endTime)
+    {
+        DataSuperShortTermForecastIssued query = new DataSuperShortTermForecastIssued();
+        query.setEntityId(entityId);
+        query.setForecastStartTime(startTime);
+        query.setForecastEndTime(endTime);
+        return superShortService.selectDataSuperShortTermForecastIssuedList(query);
+    }
+}

+ 4 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/domain/DataUltraShortTermPowerForecasting.java

@@ -47,6 +47,10 @@ public class DataUltraShortTermPowerForecasting extends BaseEntity
     @Excel(name = "起报时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Long startTime;
 
+    /** 预报时间 */
+    @Excel(name = "预报时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Long forecastTime;
+
     /** 实发功率 */
     @Excel(name = "实发功率")
     private String power;

+ 6 - 6
forecast-backend-server/src/main/java/com/ufo/project/weather/domain/DataWeatherLocal.java

@@ -32,7 +32,7 @@ public class DataWeatherLocal extends BaseEntity
 
     /** 斜面辐照度,单位 W/m² */
     @Excel(name = "斜面辐照度,单位 W/m²")
-    private Double inclinedIrradiance;
+    private Double irradiance;
 
     /** 温度,单位 ℃ */
     @Excel(name = "温度,单位 ℃")
@@ -81,14 +81,14 @@ public class DataWeatherLocal extends BaseEntity
     {
         return horizontalIrradiance;
     }
-    public void setInclinedIrradiance(Double inclinedIrradiance) 
+    public void setIrradiance(Double irradiance) 
     {
-        this.inclinedIrradiance = inclinedIrradiance;
+        this.irradiance = irradiance;
     }
 
-    public Double getInclinedIrradiance() 
+    public Double getIrradiance() 
     {
-        return inclinedIrradiance;
+        return irradiance;
     }
     public void setTemperature(Double temperature) 
     {
@@ -142,7 +142,7 @@ public class DataWeatherLocal extends BaseEntity
             .append("tm", getTm())
             .append("deviceId", getConfigStationId())
             .append("horizontalIrradiance", getHorizontalIrradiance())
-            .append("inclinedIrradiance", getInclinedIrradiance())
+            .append("irradiance", getIrradiance())
             .append("temperature", getTemperature())
             .append("pressure", getPressure())
             .append("humidity", getHumidity())

+ 5 - 0
forecast-backend-server/src/main/resources/mybatis/supershort/DataUltraShortTermPowerForecastingMapper.xml

@@ -13,6 +13,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="entityName"    column="entity_name"    />
         <result property="createTime"    column="create_time"    />
         <result property="startTime"    column="start_time"    />
+        <result property="forecastTime"    column="forecast_time"    />
         <result property="power"    column="power"    />
         <result property="power15"    column="power15"    />
         <result property="power30"    column="power30"    />
@@ -45,6 +46,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityType != null "> and entity_type = #{entityType}</if>
             <if test="entityName != null  and entityName != ''"> and entity_name like concat('%', #{entityName}, '%')</if>
             <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="forecastTime != null "> and forecast_time = #{forecastTime}</if>
             <if test="power != null  and power != ''"> and power = #{power}</if>
             <if test="power15 != null  and power15 != ''"> and power15 = #{power15}</if>
             <if test="power30 != null  and power30 != ''"> and power30 = #{power30}</if>
@@ -86,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityName != null">entity_name,</if>
             <if test="createTime != null">create_time,</if>
             <if test="startTime != null">start_time,</if>
+            <if test="startTime != null">forecast_time,</if>
             <if test="power != null">power,</if>
             <if test="power15 != null">power15,</if>
             <if test="power30 != null">power30,</if>
@@ -112,6 +115,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityName != null">#{entityName},</if>
             <if test="createTime != null">#{createTime},</if>
             <if test="startTime != null">#{startTime},</if>
+            <if test="forecastTime != null">#{forecastTime},</if>
             <if test="power != null">#{power},</if>
             <if test="power15 != null">#{power15},</if>
             <if test="power30 != null">#{power30},</if>
@@ -142,6 +146,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityName != null">entity_name = #{entityName},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="forecastTime != null">forecast_time = #{forecastTime},</if>
             <if test="power != null">power = #{power},</if>
             <if test="power15 != null">power15 = #{power15},</if>
             <if test="power30 != null">power30 = #{power30},</if>

+ 6 - 6
forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml

@@ -8,7 +8,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="tm"    column="tm"    />
         <result property="configStationId"    column="config_station_id"    />
         <result property="horizontalIrradiance"    column="horizontal_irradiance"    />
-        <result property="inclinedIrradiance"    column="inclined_irradiance"    />
+        <result property="irradiance"    column="irradiance"    />
         <result property="temperature"    column="temperature"    />
         <result property="pressure"    column="pressure"    />
         <result property="humidity"    column="humidity"    />
@@ -17,7 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectDataWeatherLocalVo">
-        select tm, config_station_id, horizontal_irradiance, inclined_irradiance, temperature, pressure, humidity, wind_speed, wind_direction from data_weather_local
+        select tm, config_station_id, horizontal_irradiance, irradiance, temperature, pressure, humidity, wind_speed, wind_direction from data_weather_local
     </sql>
 
     <select id="selectDataWeatherLocalList" parameterType="DataWeatherLocal" resultMap="DataWeatherLocalResult">
@@ -26,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="tm != null "> and tm = #{tm}</if>
             <if test="configStationId != null "> and config_station_id = #{configStationId}</if>
             <if test="horizontalIrradiance != null "> and horizontal_irradiance = #{horizontalIrradiance}</if>
-            <if test="inclinedIrradiance != null "> and inclined_irradiance = #{inclinedIrradiance}</if>
+            <if test="irradiance != null "> and irradiance = #{irradiance}</if>
             <if test="temperature != null "> and temperature = #{temperature}</if>
             <if test="pressure != null "> and pressure = #{pressure}</if>
             <if test="humidity != null "> and humidity = #{humidity}</if>
@@ -60,7 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="tm != null">tm,</if>
             <if test="configStationId != null">config_station_id,</if>
             <if test="horizontalIrradiance != null">horizontal_irradiance,</if>
-            <if test="inclinedIrradiance != null">inclined_irradiance,</if>
+            <if test="irradiance != null">irradiance,</if>
             <if test="temperature != null">temperature,</if>
             <if test="pressure != null">pressure,</if>
             <if test="humidity != null">humidity,</if>
@@ -71,7 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="tm != null">#{tm},</if>
             <if test="configStationId != null">#{configStationId},</if>
             <if test="horizontalIrradiance != null">#{horizontalIrradiance},</if>
-            <if test="inclinedIrradiance != null">#{inclinedIrradiance},</if>
+            <if test="irradiance != null">#{irradiance},</if>
             <if test="temperature != null">#{temperature},</if>
             <if test="pressure != null">#{pressure},</if>
             <if test="humidity != null">#{humidity},</if>
@@ -85,7 +85,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="SET" suffixOverrides=",">
             <if test="configStationId != null">config_station_id = #{configStationId},</if>
             <if test="horizontalIrradiance != null">horizontal_irradiance = #{horizontalIrradiance},</if>
-            <if test="inclinedIrradiance != null">inclined_irradiance = #{inclinedIrradiance},</if>
+            <if test="irradiance != null">irradiance = #{irradiance},</if>
             <if test="temperature != null">temperature = #{temperature},</if>
             <if test="pressure != null">pressure = #{pressure},</if>
             <if test="humidity != null">humidity = #{humidity},</if>

+ 1 - 0
forecast-backend-vue/src/components/layout/SideMenu.vue

@@ -5,6 +5,7 @@
       :trigger="null"
       collapsible
       :class="{ 'dark-theme': layoutSettings.darkMode }"
+      :width="240"
       @collapse="$emit('update:collapsed', $event)"
   >
     <!-- Logo区域 -->

+ 2 - 0
gateway/forecast-device-gateway/src/main/java/cn/oleauto/device/gateway/dto/DataWeatherPoint.java

@@ -10,6 +10,8 @@ public class DataWeatherPoint {
 
     private PointIdDto irradiance;
 
+    private PointIdDto inclinedIrradiance;
+
     private PointIdDto dailyIrradiance;
 
     private PointIdDto temperature;

+ 6 - 17
gateway/forecast-device-gateway/src/main/java/cn/oleauto/device/gateway/service/WeatherDataSrv.java

@@ -60,10 +60,9 @@ public class WeatherDataSrv {
     private static void refreshStationConfig(ConfigStation r, ConfigWeatherLocal s, HashMap<Long, DataWeatherPoint> tempMap) {
         DataWeatherPoint dataWeatherPoint = new DataWeatherPoint();
         dataWeatherPoint.setConfigStationId(r.getId());
-
         dataWeatherPoint.setIrradiance(JSONUtil.toBean(s.getIrradiance(), PointIdDto.class));
+        dataWeatherPoint.setInclinedIrradiance(JSONUtil.toBean(s.getInclinedIrradiance(), PointIdDto.class));
         dataWeatherPoint.setDailyIrradiance(JSONUtil.toBean(s.getDailyIrradiance(), PointIdDto.class));
-
         dataWeatherPoint.setTemperature(JSONUtil.toBean(s.getTemperature(), PointIdDto.class));
         dataWeatherPoint.setBoardTemperature(JSONUtil.toBean(s.getBoardTemperature(), PointIdDto.class));
         dataWeatherPoint.setPressure(JSONUtil.toBean(s.getPressure(), PointIdDto.class));
@@ -75,9 +74,9 @@ public class WeatherDataSrv {
             log.warn("station={}, HorizontalIrradiance config error,config={}", s.getConfigStationId(), s.getIrradiance());
             dataWeatherPoint.setIrradiance(null);
         }
-        if(!Validator.isValid(dataWeatherPoint.getIrradiance())){
-            log.warn("station={}, Irradiance config error,config={}", s.getConfigStationId(), s.getIrradiance());
-            dataWeatherPoint.setIrradiance(null);
+        if(!Validator.isValid(dataWeatherPoint.getInclinedIrradiance())){
+            log.warn("station={}, InclinedIrradiance config error,config={}", s.getConfigStationId(), s.getInclinedIrradiance());
+            dataWeatherPoint.setInclinedIrradiance(null);
         }
         if(!Validator.isValid(dataWeatherPoint.getDailyIrradiance())){
             log.warn("station={}, dailyIrradiance config error,config={}", s.getConfigStationId(), s.getDailyIrradiance());
@@ -126,13 +125,8 @@ public class WeatherDataSrv {
         data.setTm(new Date());
         data.setConfigStationId(v.getConfigStationId());
         // 赋予初值0
-<<<<<<< HEAD
-        data.setHorizontalIrradiance(0.0);
-        data.setIrradiance(0.0);
-=======
         data.setIrradiance(0.0);
         data.setInclinedIrradiance(0.0);
->>>>>>> master
         data.setTemperature(0.0);
         data.setPressure(0.0);
         data.setHumidity(0.0);
@@ -151,19 +145,14 @@ public class WeatherDataSrv {
                 valid++;
             }
         }
-        if(v.getIrradiance() != null) {
+        if(v.getInclinedIrradiance() != null) {
             PointIdValueDto pointIdValueDto = new PointIdValueDto();
-            BeanUtils.copyProperties(v.getIrradiance(), pointIdValueDto);
+            BeanUtils.copyProperties(v.getInclinedIrradiance(), pointIdValueDto);
             RealDataCache.getInstance().getValue(pointIdValueDto);
-<<<<<<< HEAD
-            if(pointIdValueDto.getPointAttr() != PointAttributeType.INVALID && pointIdValueDto.getValueAttr() != ValueAttributeType.INVALID){
-                data.setIrradiance(pointIdValueDto.getDValue());
-=======
             if(pointIdValueDto.getPointAttr() != PointAttributeType.INVALID
                     && pointIdValueDto.getValueAttr() != ValueAttributeType.INVALID
                     && pointIdValueDto.getDValue() != null){
                 data.setInclinedIrradiance(pointIdValueDto.getDValue());
->>>>>>> master
                 valid++;
             }
         }

+ 2 - 0
release_note/2026.09.03/20260903_dp.sql

@@ -80,6 +80,7 @@ CREATE TABLE data_ultra_short_term_power_forecasting (
     entity_name VARCHAR(255),
     create_time INTEGER NOT NULL,
     start_time INTEGER NOT NULL,
+    forecast_time INTEGER NOT NULL,
     power VARCHAR(255),
     power15 VARCHAR(255),
     power30 VARCHAR(255),
@@ -112,6 +113,7 @@ COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."entity_typ
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."entity_name" IS '实体名称';
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."create_time" IS '创建时间';
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."start_time" IS '起报时间(日期)';
+COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."forecast_time" IS '起报时间(日期)';
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."power" IS '实发功率';
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."power15" IS 't+15min预测功率';
 COMMENT ON COLUMN "public"."data_ultra_short_term_power_forecasting"."power30" IS 't+30min预测功率';

+ 6 - 0
release_note/2026.09.17/20260917_dp.sql

@@ -0,0 +1,6 @@
+-- 隐藏菜单
+UPDATE "public"."sys_menu" set status = 1 where menu_name in ('多源多模型预测', '理论功率预测');
+
+-- data_weather_local 斜面辐照度字段 -> 总辐照度
+ALTER TABLE "public"."data_weather_local" RENAME COLUMN "inclined_irradiance" TO "irradiance";
+COMMENT ON COLUMN "public"."data_weather_local"."irradiance" IS '总辐照度,单位 W/m²';