Browse Source

[20260820] 短期功率预测、超短期功率预测

taosj 2 months ago
parent
commit
255cb18840
30 changed files with 2423 additions and 22 deletions
  1. 6 0
      forecast-backend-server/pom.xml
  2. 110 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/ShortTermPowerForecastingController.java
  3. 277 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/UltraShortTermPowerForecastingController.java
  4. 116 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/ShortTermPowerForecastCsvRow.java
  5. 70 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/ShortTermPowerForecastData.java
  6. 246 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastCsvRow.java
  7. 276 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastData.java
  8. 67 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerSingleExport.java
  9. 138 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/util/ExcelForecastFileUtil.java
  10. 138 0
      forecast-backend-server/src/main/java/com/ufo/project/model/domain/DataElectricityLocal.java
  11. 75 0
      forecast-backend-server/src/main/java/com/ufo/project/model/mapper/DataElectricityLocalMapper.java
  12. 15 0
      forecast-backend-server/src/main/java/com/ufo/project/model/mapper/ModelPredictionTasksMapper.java
  13. 73 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/IDataElectricityLocalService.java
  14. 12 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/IModelPredictionTasksService.java
  15. 109 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/DataElectricityLocalServiceImpl.java
  16. 16 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/ModelPredictionTasksServiceImpl.java
  17. 97 0
      forecast-backend-server/src/main/resources/mybatis/model/DataElectricityLocalMapper.xml
  18. 12 1
      forecast-backend-server/src/main/resources/mybatis/model/ModelPredictionTasksMapper.xml
  19. 12 0
      forecast-backend-server/src/main/resources/sql/20260819_short_term_power_forecasting.sql
  20. 12 0
      forecast-backend-server/src/main/resources/sql/20260819_ultra_short_term_power_forecasting.sql
  21. 21 0
      forecast-backend-vue/src/api/power/shortTermPowerForecasting.js
  22. 21 0
      forecast-backend-vue/src/api/power/ultraShortTermPowerForecasting.js
  23. 12 0
      forecast-backend-vue/src/router/electricity.js
  24. 4 1
      forecast-backend-vue/src/views/ufo/electricity/components/BusinessDataTable.vue
  25. 39 20
      forecast-backend-vue/src/views/ufo/electricity/components/PowerLineChart.vue
  26. 150 0
      forecast-backend-vue/src/views/ufo/electricity/forecast/ShortTermPowerForecasting.vue
  27. 244 0
      forecast-backend-vue/src/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue
  28. 10 0
      release_note/2026.08.20/20260820_dp.sql
  29. 17 0
      release_note/2026.08.20/20260820_release.pdf
  30. 28 0
      release_note/2026.08.20/readme_tsj.md

+ 6 - 0
forecast-backend-server/pom.xml

@@ -37,9 +37,15 @@
         <velocity.version>2.3</velocity.version>
         <ijapy.version>2.9.12</ijapy.version>
         <openai.version>4.8.0</openai.version>
+        <easyexcel.version>4.0.3</easyexcel.version>
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+        </dependency>
 
         <!-- SpringBoot 核心包 -->
         <dependency>

+ 110 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/ShortTermPowerForecastingController.java

@@ -0,0 +1,110 @@
+package com.ufo.project.forecast.shortterm.power.controller;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.alibaba.excel.EasyExcel;
+import com.ufo.base.common.utils.DateUtils;
+import com.ufo.base.framework.aspectj.lang.annotation.Log;
+import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
+import com.ufo.base.framework.web.controller.BaseController;
+import com.ufo.base.framework.web.domain.AjaxResult;
+import com.ufo.project.forecast.shortterm.power.domain.ShortTermPowerForecastCsvRow;
+import com.ufo.project.forecast.shortterm.power.domain.ShortTermPowerForecastData;
+import com.ufo.project.forecast.shortterm.power.util.ExcelForecastFileUtil;
+import com.ufo.project.model.domain.ModelPredictionTasks;
+import com.ufo.project.model.service.IModelPredictionTasksService;
+
+/**
+ * 短期功率预测Controller
+ *
+ * @author ufo
+ */
+@RestController
+@RequestMapping("/power/shortTermPowerForecasting")
+public class ShortTermPowerForecastingController extends BaseController
+{
+    @Autowired
+    private IModelPredictionTasksService modelPredictionTasksService;
+
+    /**
+     * 获取短期功率预测数据
+     *
+     * @param stationId 场站id
+     * @param forecastHours 预测时间(24/48/72)
+     * @return time/power/theoretical_power/curtailment_power 数据列表
+     */
+    @PreAuthorize("@ss.hasPermi('electricity:forecast:shorttermpower')")
+    @GetMapping("/report")
+    public AjaxResult report(@RequestParam("stationId") Long stationId,
+            @RequestParam(value = "forecastHours", defaultValue = "24") Integer forecastHours)
+    {
+        return success(queryForecastData(stationId, forecastHours));
+    }
+
+    /**
+     * 导出短期功率预测数据
+     *
+     * @param response 响应
+     * @param stationId 场站id
+     * @param forecastHours 预测时间(24/48/72)
+     */
+    @PreAuthorize("@ss.hasPermi('electricity:forecast:shorttermpower')")
+    @Log(title = "短期功率预测", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public void export(HttpServletResponse response,
+            @RequestParam("stationId") Long stationId,
+            @RequestParam(value = "forecastHours", defaultValue = "24") Integer forecastHours) throws IOException
+    {
+        List<ShortTermPowerForecastData> data = queryForecastData(stationId, forecastHours);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("短期预测-" + DateUtils.getDate() + ".xlsx", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
+        EasyExcel.write(response.getOutputStream(), ShortTermPowerForecastData.class)
+                .sheet("短期功率预测")
+                .doWrite(data);
+    }
+
+    /**
+     * 查询预测任务并按 time 顺序解析其 result_path 对应的 csv 文件
+     */
+    private List<ShortTermPowerForecastData> queryForecastData(Long stationId, Integer forecastHours)
+    {
+        List<ShortTermPowerForecastData> result = new ArrayList<>();
+        Date startTime = new Date();
+        Date endTime = new Date(System.currentTimeMillis() + (long) forecastHours * 3600L * 1000L);
+        List<ModelPredictionTasks> tasks = modelPredictionTasksService
+                .selectPredictionTasks(stationId, "short_term", startTime, endTime);
+        for (ModelPredictionTasks task : tasks)
+        {
+            List<ShortTermPowerForecastCsvRow> rows = ExcelForecastFileUtil.readResultFile(
+                    task.getResultPath(), ShortTermPowerForecastCsvRow.class);
+            if (rows == null)
+            {
+                continue;
+            }
+            for (ShortTermPowerForecastCsvRow row : rows)
+            {
+                ShortTermPowerForecastData data = new ShortTermPowerForecastData();
+                data.setTime(ExcelForecastFileUtil.formatTime(row.getTime()));
+                data.setPower(row.getPower());
+                data.setTheoreticalPower(row.getTheoreticalPower());
+                data.setCurtailmentPower(row.getCurtailmentPower());
+                result.add(data);
+            }
+        }
+        result.sort(Comparator.comparing(ShortTermPowerForecastData::getTime));
+        return result;
+    }
+}

+ 277 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/UltraShortTermPowerForecastingController.java

@@ -0,0 +1,277 @@
+package com.ufo.project.forecast.shortterm.power.controller;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.alibaba.excel.EasyExcel;
+import com.ufo.base.common.utils.StringUtils;
+import com.ufo.base.framework.aspectj.lang.annotation.Log;
+import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
+import com.ufo.base.framework.web.controller.BaseController;
+import com.ufo.base.framework.web.domain.AjaxResult;
+import com.ufo.project.forecast.shortterm.power.domain.UltraShortTermPowerForecastCsvRow;
+import com.ufo.project.forecast.shortterm.power.domain.UltraShortTermPowerForecastData;
+import com.ufo.project.forecast.shortterm.power.domain.UltraShortTermPowerSingleExport;
+import com.ufo.project.forecast.shortterm.power.util.ExcelForecastFileUtil;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.domain.ModelPredictionTasks;
+import com.ufo.project.model.service.IDataElectricityLocalService;
+import com.ufo.project.model.service.IModelPredictionTasksService;
+
+/**
+ * 超短期功率预测Controller
+ *
+ * @author ufo
+ */
+@RestController
+@RequestMapping("/power/ultraShortTermPowerForecasting")
+public class UltraShortTermPowerForecastingController extends BaseController
+{
+    private static final Logger log = LoggerFactory.getLogger(UltraShortTermPowerForecastingController.class);
+
+    private static final String PATTERN_SECOND = "yyyy-MM-dd HH:mm:ss";
+    private static final String PATTERN_MINUTE = "yyyy-MM-dd HH:mm";
+    private static final String PATTERN_COMPACT = "yyyyMMddHHmm";
+
+    @Autowired
+    private IModelPredictionTasksService modelPredictionTasksService;
+
+    @Autowired
+    private IDataElectricityLocalService dataElectricityLocalService;
+
+    /**
+     * 获取超短期功率预测数据(预测 + 实发功率合并)
+     *
+     * @param stationId 场站id
+     * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss
+     * @param endTime 截止时间 yyyy-MM-dd HH:mm:ss
+     */
+    @PreAuthorize("@ss.hasPermi('electricity:forecast:ultrashorttermpower')")
+    @GetMapping("/report")
+    public AjaxResult report(@RequestParam("stationId") Long stationId,
+            @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime)
+    {
+        return success(queryForecastData(stationId, startTime, endTime));
+    }
+
+    /**
+     * 导出超短期功率预测数据
+     *
+     * @param stationId 场站id
+     * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss
+     * @param endTime 截止时间 yyyy-MM-dd HH:mm:ss
+     * @param type 0 单点预测; 1 单次预测
+     */
+    @PreAuthorize("@ss.hasPermi('electricity:forecast:ultrashorttermpower')")
+    @Log(title = "超短期功率预测", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public void export(HttpServletResponse response,
+            @RequestParam("stationId") Long stationId,
+            @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime,
+            @RequestParam(value = "type", defaultValue = "0") Integer type) throws IOException
+    {
+        List<UltraShortTermPowerForecastData> data = queryForecastData(stationId, startTime, endTime);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode(buildFileName(startTime, endTime), "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
+        if (type != null && type == 1)
+        {
+            List<UltraShortTermPowerSingleExport> rows = new ArrayList<>();
+            for (UltraShortTermPowerForecastData item : data)
+            {
+                UltraShortTermPowerSingleExport row = new UltraShortTermPowerSingleExport();
+                row.setTime(item.getTime());
+                row.setMonitoredPower(item.getMonitoredPower());
+                row.setPower(item.getPower());
+                row.setBias(item.getBias());
+                rows.add(row);
+            }
+            EasyExcel.write(response.getOutputStream(), UltraShortTermPowerSingleExport.class)
+                    .sheet("超短期功率预测").doWrite(rows);
+        }
+        else
+        {
+            EasyExcel.write(response.getOutputStream(), UltraShortTermPowerForecastData.class)
+                    .sheet("超短期功率预测").doWrite(data);
+        }
+    }
+
+    /**
+     * 查询预测任务并解析 result_path 对应文件,与实发功率按时间 merge
+     */
+    private List<UltraShortTermPowerForecastData> queryForecastData(Long stationId, String startTimeStr, String endTimeStr)
+    {
+        List<UltraShortTermPowerForecastData> result = new ArrayList<>();
+        try
+        {
+            SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_SECOND);
+            Date startTime = sdf.parse(startTimeStr);
+            Date endTime = sdf.parse(endTimeStr);
+
+            // 逻辑#1 预测数据: target_time 在 [startTime当日0点, endTime当日23:59:59] 区间
+            Date queryStart = startOfDay(startTime);
+            Date queryEnd = endOfDay(endTime);
+            List<ModelPredictionTasks> tasks = modelPredictionTasksService
+                    .selectPredictionTasks(stationId, "ultra_short", queryStart, queryEnd);
+            Map<String, UltraShortTermPowerForecastData> forecastMap = new TreeMap<>();
+            for (ModelPredictionTasks task : tasks)
+            {
+                List<UltraShortTermPowerForecastCsvRow> rows = ExcelForecastFileUtil.readResultFile(
+                        task.getResultPath(), UltraShortTermPowerForecastCsvRow.class);
+                if (rows == null)
+                {
+                    continue;
+                }
+                for (UltraShortTermPowerForecastCsvRow row : rows)
+                {
+                    String time = ExcelForecastFileUtil.formatTime(row.getTime());
+                    if (StringUtils.isEmpty(time) || !inRange(time, startTime, endTime))
+                    {
+                        continue;
+                    }
+                    forecastMap.put(time, toData(row));
+                }
+            }
+
+            // 逻辑#2 实发功率
+            List<DataElectricityLocal> actualList = dataElectricityLocalService
+                    .selectActualPowerList(stationId, startTime, endTime);
+            Map<String, Double> actualMap = new HashMap<>();
+            for (DataElectricityLocal actual : actualList)
+            {
+                actualMap.put(formatDate(actual.getTm(), PATTERN_MINUTE), actual.getActualPower());
+            }
+
+            // merge:两边时间并集,缺失补 0
+            TreeSet<String> keys = new TreeSet<>(forecastMap.keySet());
+            keys.addAll(actualMap.keySet());
+            for (String key : keys)
+            {
+                UltraShortTermPowerForecastData data = forecastMap.get(key);
+                if (data == null)
+                {
+                    data = new UltraShortTermPowerForecastData();
+                    data.setTime(key);
+                }
+                Double monitored = actualMap.get(key);
+                data.setMonitoredPower(monitored == null ? 0D : monitored);
+                double power = data.getPower() == null ? 0D : data.getPower();
+                data.setBias(round2(data.getMonitoredPower() - power));
+                result.add(data);
+            }
+            result.sort(Comparator.comparing(UltraShortTermPowerForecastData::getTime));
+        }
+        catch (Exception e)
+        {
+            log.error("超短期功率预测数据查询失败: stationId={}, startTime={}, endTime={}",
+                    stationId, startTimeStr, endTimeStr, e);
+        }
+        return result;
+    }
+
+    private UltraShortTermPowerForecastData toData(UltraShortTermPowerForecastCsvRow row)
+    {
+        UltraShortTermPowerForecastData data = new UltraShortTermPowerForecastData();
+        data.setTime(ExcelForecastFileUtil.formatTime(row.getTime()));
+        data.setPower(row.getPower());
+        data.setPower15(row.getPower15());
+        data.setPower30(row.getPower30());
+        data.setPower45(row.getPower45());
+        data.setPower60(row.getPower60());
+        data.setPower75(row.getPower75());
+        data.setPower90(row.getPower90());
+        data.setPower105(row.getPower105());
+        data.setPower120(row.getPower120());
+        data.setPower135(row.getPower135());
+        data.setPower150(row.getPower150());
+        data.setPower165(row.getPower165());
+        data.setPower180(row.getPower180());
+        data.setPower195(row.getPower195());
+        data.setPower210(row.getPower210());
+        data.setPower225(row.getPower225());
+        data.setPower240(row.getPower240());
+        return data;
+    }
+
+    /**
+     * 判断格式化时间(yyyy-MM-dd HH:mm)是否在 [startTime, endTime] 区间内(字符串比较,无需时区换算)
+     */
+    private boolean inRange(String timeKey, Date startTime, Date endTime)
+    {
+        return timeKey.compareTo(formatDate(startTime, PATTERN_MINUTE)) >= 0
+                && timeKey.compareTo(formatDate(endTime, PATTERN_MINUTE)) <= 0;
+    }
+
+    private String formatDate(Date date, String pattern)
+    {
+        return new SimpleDateFormat(pattern).format(date);
+    }
+
+    /**
+     * 取日期当天 0 点
+     */
+    private Date startOfDay(Date date)
+    {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    /**
+     * 取日期当天 23:59:59
+     */
+    private Date endOfDay(Date date)
+    {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private double round2(double value)
+    {
+        return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    private String buildFileName(String startTime, String endTime)
+    {
+        try
+        {
+            SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_SECOND);
+            return "超短期预测-" + formatDate(sdf.parse(startTime), PATTERN_COMPACT)
+                    + "-" + formatDate(sdf.parse(endTime), PATTERN_COMPACT) + ".xlsx";
+        }
+        catch (Exception e)
+        {
+            return "超短期预测.xlsx";
+        }
+    }
+}

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

@@ -0,0 +1,116 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 短期功率预测CSV数据行
+ * 对应预测任务 result_path 指向的 csv 文件字段
+ *
+ * @author ufo
+ */
+public class ShortTermPowerForecastCsvRow
+{
+    @ExcelProperty("id")
+    private Long id;
+
+    @ExcelProperty("station_id")
+    private Long stationId;
+
+    @ExcelProperty("time")
+    private String time;
+
+    @ExcelProperty("power")
+    private Double power;
+
+    @ExcelProperty("theoretical_power")
+    private Double theoreticalPower;
+
+    @ExcelProperty("curtailment_power")
+    private Double curtailmentPower;
+
+    @ExcelProperty("grid_status")
+    private String gridStatus;
+
+    @ExcelProperty("device_status")
+    private String deviceStatus;
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getStationId()
+    {
+        return stationId;
+    }
+
+    public void setStationId(Long stationId)
+    {
+        this.stationId = stationId;
+    }
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public Double getPower()
+    {
+        return power;
+    }
+
+    public void setPower(Double power)
+    {
+        this.power = power;
+    }
+
+    public Double getTheoreticalPower()
+    {
+        return theoreticalPower;
+    }
+
+    public void setTheoreticalPower(Double theoreticalPower)
+    {
+        this.theoreticalPower = theoreticalPower;
+    }
+
+    public Double getCurtailmentPower()
+    {
+        return curtailmentPower;
+    }
+
+    public void setCurtailmentPower(Double curtailmentPower)
+    {
+        this.curtailmentPower = curtailmentPower;
+    }
+
+    public String getGridStatus()
+    {
+        return gridStatus;
+    }
+
+    public void setGridStatus(String gridStatus)
+    {
+        this.gridStatus = gridStatus;
+    }
+
+    public String getDeviceStatus()
+    {
+        return deviceStatus;
+    }
+
+    public void setDeviceStatus(String deviceStatus)
+    {
+        this.deviceStatus = deviceStatus;
+    }
+}

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

@@ -0,0 +1,70 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * 短期功率预测业务数据
+ *
+ * @author ufo
+ */
+public class ShortTermPowerForecastData
+{
+    /** 时间(yyyy-MM-dd HH:mm) */
+    @ExcelProperty("预测时间")
+    private String time;
+
+    /** 预报发电功率(kw) */
+    @ExcelProperty("预测功率")
+    private Double power;
+
+    /** 无故障、无限电下的理论功率(kw) */
+    @ExcelProperty("理论功率")
+    @JsonProperty("theoretical_power")
+    private Double theoreticalPower;
+
+    /** 因调度指令限制的功率(KW) */
+    @ExcelProperty("限制功率")
+    @JsonProperty("curtailment_power")
+    private Double curtailmentPower;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public Double getPower()
+    {
+        return power;
+    }
+
+    public void setPower(Double power)
+    {
+        this.power = power;
+    }
+
+    public Double getTheoreticalPower()
+    {
+        return theoreticalPower;
+    }
+
+    public void setTheoreticalPower(Double theoreticalPower)
+    {
+        this.theoreticalPower = theoreticalPower;
+    }
+
+    public Double getCurtailmentPower()
+    {
+        return curtailmentPower;
+    }
+
+    public void setCurtailmentPower(Double curtailmentPower)
+    {
+        this.curtailmentPower = curtailmentPower;
+    }
+}

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

@@ -0,0 +1,246 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 超短期功率预测CSV数据行
+ * 对应预测任务 result_path 指向的文件字段(日期/实测功率/15分钟...240分钟)
+ *
+ * @author ufo
+ */
+public class UltraShortTermPowerForecastCsvRow
+{
+    @ExcelProperty("日期")
+    private String time;
+
+    @ExcelProperty("实测功率")
+    private Double power;
+
+    @ExcelProperty("15分钟")
+    private Double power15;
+
+    @ExcelProperty("30分钟")
+    private Double power30;
+
+    @ExcelProperty("45分钟")
+    private Double power45;
+
+    @ExcelProperty("60分钟")
+    private Double power60;
+
+    @ExcelProperty("75分钟")
+    private Double power75;
+
+    @ExcelProperty("90分钟")
+    private Double power90;
+
+    @ExcelProperty("105分钟")
+    private Double power105;
+
+    @ExcelProperty("120分钟")
+    private Double power120;
+
+    @ExcelProperty("135分钟")
+    private Double power135;
+
+    @ExcelProperty("150分钟")
+    private Double power150;
+
+    @ExcelProperty("165分钟")
+    private Double power165;
+
+    @ExcelProperty("180分钟")
+    private Double power180;
+
+    @ExcelProperty("195分钟")
+    private Double power195;
+
+    @ExcelProperty("210分钟")
+    private Double power210;
+
+    @ExcelProperty("225分钟")
+    private Double power225;
+
+    @ExcelProperty("240分钟")
+    private Double power240;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public Double getPower()
+    {
+        return power;
+    }
+
+    public void setPower(Double power)
+    {
+        this.power = power;
+    }
+
+    public Double getPower15()
+    {
+        return power15;
+    }
+
+    public void setPower15(Double power15)
+    {
+        this.power15 = power15;
+    }
+
+    public Double getPower30()
+    {
+        return power30;
+    }
+
+    public void setPower30(Double power30)
+    {
+        this.power30 = power30;
+    }
+
+    public Double getPower45()
+    {
+        return power45;
+    }
+
+    public void setPower45(Double power45)
+    {
+        this.power45 = power45;
+    }
+
+    public Double getPower60()
+    {
+        return power60;
+    }
+
+    public void setPower60(Double power60)
+    {
+        this.power60 = power60;
+    }
+
+    public Double getPower75()
+    {
+        return power75;
+    }
+
+    public void setPower75(Double power75)
+    {
+        this.power75 = power75;
+    }
+
+    public Double getPower90()
+    {
+        return power90;
+    }
+
+    public void setPower90(Double power90)
+    {
+        this.power90 = power90;
+    }
+
+    public Double getPower105()
+    {
+        return power105;
+    }
+
+    public void setPower105(Double power105)
+    {
+        this.power105 = power105;
+    }
+
+    public Double getPower120()
+    {
+        return power120;
+    }
+
+    public void setPower120(Double power120)
+    {
+        this.power120 = power120;
+    }
+
+    public Double getPower135()
+    {
+        return power135;
+    }
+
+    public void setPower135(Double power135)
+    {
+        this.power135 = power135;
+    }
+
+    public Double getPower150()
+    {
+        return power150;
+    }
+
+    public void setPower150(Double power150)
+    {
+        this.power150 = power150;
+    }
+
+    public Double getPower165()
+    {
+        return power165;
+    }
+
+    public void setPower165(Double power165)
+    {
+        this.power165 = power165;
+    }
+
+    public Double getPower180()
+    {
+        return power180;
+    }
+
+    public void setPower180(Double power180)
+    {
+        this.power180 = power180;
+    }
+
+    public Double getPower195()
+    {
+        return power195;
+    }
+
+    public void setPower195(Double power195)
+    {
+        this.power195 = power195;
+    }
+
+    public Double getPower210()
+    {
+        return power210;
+    }
+
+    public void setPower210(Double power210)
+    {
+        this.power210 = power210;
+    }
+
+    public Double getPower225()
+    {
+        return power225;
+    }
+
+    public void setPower225(Double power225)
+    {
+        this.power225 = power225;
+    }
+
+    public Double getPower240()
+    {
+        return power240;
+    }
+
+    public void setPower240(Double power240)
+    {
+        this.power240 = power240;
+    }
+}

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

@@ -0,0 +1,276 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 超短期功率预测业务数据(单点预测)
+ *
+ * @author ufo
+ */
+public class UltraShortTermPowerForecastData
+{
+    /** 时间(yyyy-MM-dd HH:mm) */
+    @ExcelProperty("日期")
+    private String time;
+
+    /** 实测功率/预报发电功率(kw) */
+    @ExcelProperty("实测功率")
+    private Double power = 0D;
+
+    @ExcelProperty("15分钟")
+    private Double power15 = 0D;
+
+    @ExcelProperty("30分钟")
+    private Double power30 = 0D;
+
+    @ExcelProperty("45分钟")
+    private Double power45 = 0D;
+
+    @ExcelProperty("60分钟")
+    private Double power60 = 0D;
+
+    @ExcelProperty("75分钟")
+    private Double power75 = 0D;
+
+    @ExcelProperty("90分钟")
+    private Double power90 = 0D;
+
+    @ExcelProperty("105分钟")
+    private Double power105 = 0D;
+
+    @ExcelProperty("120分钟")
+    private Double power120 = 0D;
+
+    @ExcelProperty("135分钟")
+    private Double power135 = 0D;
+
+    @ExcelProperty("150分钟")
+    private Double power150 = 0D;
+
+    @ExcelProperty("165分钟")
+    private Double power165 = 0D;
+
+    @ExcelProperty("180分钟")
+    private Double power180 = 0D;
+
+    @ExcelProperty("195分钟")
+    private Double power195 = 0D;
+
+    @ExcelProperty("210分钟")
+    private Double power210 = 0D;
+
+    @ExcelProperty("225分钟")
+    private Double power225 = 0D;
+
+    @ExcelProperty("240分钟")
+    private Double power240 = 0D;
+
+    /** 实发功率(kw) */
+    @ExcelIgnore
+    private Double monitoredPower = 0D;
+
+    /** 偏差 = monitoredPower - power */
+    @ExcelIgnore
+    private Double bias = 0D;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public Double getPower()
+    {
+        return power;
+    }
+
+    public void setPower(Double power)
+    {
+        this.power = power;
+    }
+
+    public Double getPower15()
+    {
+        return power15;
+    }
+
+    public void setPower15(Double power15)
+    {
+        this.power15 = power15;
+    }
+
+    public Double getPower30()
+    {
+        return power30;
+    }
+
+    public void setPower30(Double power30)
+    {
+        this.power30 = power30;
+    }
+
+    public Double getPower45()
+    {
+        return power45;
+    }
+
+    public void setPower45(Double power45)
+    {
+        this.power45 = power45;
+    }
+
+    public Double getPower60()
+    {
+        return power60;
+    }
+
+    public void setPower60(Double power60)
+    {
+        this.power60 = power60;
+    }
+
+    public Double getPower75()
+    {
+        return power75;
+    }
+
+    public void setPower75(Double power75)
+    {
+        this.power75 = power75;
+    }
+
+    public Double getPower90()
+    {
+        return power90;
+    }
+
+    public void setPower90(Double power90)
+    {
+        this.power90 = power90;
+    }
+
+    public Double getPower105()
+    {
+        return power105;
+    }
+
+    public void setPower105(Double power105)
+    {
+        this.power105 = power105;
+    }
+
+    public Double getPower120()
+    {
+        return power120;
+    }
+
+    public void setPower120(Double power120)
+    {
+        this.power120 = power120;
+    }
+
+    public Double getPower135()
+    {
+        return power135;
+    }
+
+    public void setPower135(Double power135)
+    {
+        this.power135 = power135;
+    }
+
+    public Double getPower150()
+    {
+        return power150;
+    }
+
+    public void setPower150(Double power150)
+    {
+        this.power150 = power150;
+    }
+
+    public Double getPower165()
+    {
+        return power165;
+    }
+
+    public void setPower165(Double power165)
+    {
+        this.power165 = power165;
+    }
+
+    public Double getPower180()
+    {
+        return power180;
+    }
+
+    public void setPower180(Double power180)
+    {
+        this.power180 = power180;
+    }
+
+    public Double getPower195()
+    {
+        return power195;
+    }
+
+    public void setPower195(Double power195)
+    {
+        this.power195 = power195;
+    }
+
+    public Double getPower210()
+    {
+        return power210;
+    }
+
+    public void setPower210(Double power210)
+    {
+        this.power210 = power210;
+    }
+
+    public Double getPower225()
+    {
+        return power225;
+    }
+
+    public void setPower225(Double power225)
+    {
+        this.power225 = power225;
+    }
+
+    public Double getPower240()
+    {
+        return power240;
+    }
+
+    public void setPower240(Double power240)
+    {
+        this.power240 = power240;
+    }
+
+    public Double getMonitoredPower()
+    {
+        return monitoredPower;
+    }
+
+    public void setMonitoredPower(Double monitoredPower)
+    {
+        this.monitoredPower = monitoredPower;
+    }
+
+    public Double getBias()
+    {
+        return bias;
+    }
+
+    public void setBias(Double bias)
+    {
+        this.bias = bias;
+    }
+}

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

@@ -0,0 +1,67 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 超短期功率预测导出数据(单次预测 type=1)
+ *
+ * @author ufo
+ */
+public class UltraShortTermPowerSingleExport
+{
+    /** 时间(yyyy-MM-dd HH:mm) */
+    @ExcelProperty("日期")
+    private String time;
+
+    /** 实发功率(kw) */
+    @ExcelProperty("实发功率")
+    private Double monitoredPower = 0D;
+
+    /** 超短期预测功率(kw) */
+    @ExcelProperty("超短期预测")
+    private Double power = 0D;
+
+    /** 偏差 = monitoredPower - power */
+    @ExcelProperty("偏差")
+    private Double bias = 0D;
+
+    public String getTime()
+    {
+        return time;
+    }
+
+    public void setTime(String time)
+    {
+        this.time = time;
+    }
+
+    public Double getMonitoredPower()
+    {
+        return monitoredPower;
+    }
+
+    public void setMonitoredPower(Double monitoredPower)
+    {
+        this.monitoredPower = monitoredPower;
+    }
+
+    public Double getPower()
+    {
+        return power;
+    }
+
+    public void setPower(Double power)
+    {
+        this.power = power;
+    }
+
+    public Double getBias()
+    {
+        return bias;
+    }
+
+    public void setBias(Double bias)
+    {
+        this.bias = bias;
+    }
+}

+ 138 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/util/ExcelForecastFileUtil.java

@@ -0,0 +1,138 @@
+package com.ufo.project.forecast.shortterm.power.util;
+
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.read.builder.ExcelReaderBuilder;
+import com.alibaba.excel.support.ExcelTypeEnum;
+import com.ufo.base.common.utils.StringUtils;
+import com.ufo.base.framework.config.ProjectConfig;
+
+/**
+ * 预测结果文件(csv/xls/xlsx)读取工具
+ *
+ * @author ufo
+ */
+public class ExcelForecastFileUtil
+{
+    private static final Logger log = LoggerFactory.getLogger(ExcelForecastFileUtil.class);
+
+    /**
+     * 解析预测任务结果文件;文件缺失或解析失败返回 null
+     *
+     * @param resultPath 结果文件路径
+     * @param headClass 表头映射类
+     * @param <T> 行数据类型
+     * @return 解析结果
+     */
+    public static <T> List<T> readResultFile(String resultPath, Class<T> headClass)
+    {
+        if (StringUtils.isEmpty(resultPath))
+        {
+            return null;
+        }
+        File file = new File(resultPath);
+        if (!file.exists())
+        {
+            file = new File(ProjectConfig.getProfile(),
+                    resultPath.startsWith("/") ? resultPath.substring(1) : resultPath);
+        }
+        if (!file.exists())
+        {
+            log.warn("预测结果文件不存在: {}", resultPath);
+            return null;
+        }
+        try
+        {
+            ExcelReaderBuilder builder = EasyExcel.read(file).head(headClass);
+            ExcelTypeEnum excelType = resolveExcelType(resultPath);
+            if (excelType != null)
+            {
+                builder.excelType(excelType);
+            }
+            return builder.sheet().doReadSync();
+        }
+        catch (Exception e)
+        {
+            log.warn("预测结果文件解析失败: {}", resultPath, e);
+            return null;
+        }
+    }
+
+    private static final DateTimeFormatter TIME_OUT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+    // 单字母 M/d/H/m/s 同时接受补零与非补零(如 2026-8-20 4:30 / 2026-08-20 04:30:00)
+    private static final DateTimeFormatter[] TIME_IN = {
+        DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"),
+        DateTimeFormatter.ofPattern("yyyy-M-d H:m"),
+        DateTimeFormatter.ofPattern("yyyy/M/d H:m:s"),
+        DateTimeFormatter.ofPattern("yyyy/M/d H:m"),
+        DateTimeFormatter.ofPattern("yyyy-M-d'T'H:m:s"),
+        DateTimeFormatter.ofPattern("yyyy-M-d'T'H:m")
+    };
+
+    /**
+     * 时间统一格式化为固定格式 yyyy-MM-dd HH:mm
+     */
+    public static String formatTime(String time)
+    {
+        if (time == null || time.trim().isEmpty())
+        {
+            return "";
+        }
+        String t = time.trim();
+        // 去掉时区后缀(+00:00 / Z),保留本地时间
+        int plus = t.indexOf('+');
+        if (plus > 10)
+        {
+            t = t.substring(0, plus).trim();
+        }
+        int z = t.indexOf('Z');
+        if (z > 10)
+        {
+            t = t.substring(0, z).trim();
+        }
+        for (DateTimeFormatter formatter : TIME_IN)
+        {
+            try
+            {
+                return LocalDateTime.parse(t, formatter).format(TIME_OUT);
+            }
+            catch (DateTimeParseException ignore)
+            {
+                // 继续尝试下一种格式
+            }
+        }
+        // 无法解析时截断兜底
+        return t.length() >= 16 ? t.replace('T', ' ').substring(0, 16) : t;
+    }
+
+    /**
+     * 根据文件后缀确定 excel 类型;无法识别返回 null 交由 easyexcel 自动识别
+     */
+    private static ExcelTypeEnum resolveExcelType(String resultPath)
+    {
+        if (resultPath == null)
+        {
+            return null;
+        }
+        String name = resultPath.toLowerCase();
+        if (name.endsWith(".csv"))
+        {
+            return ExcelTypeEnum.CSV;
+        }
+        if (name.endsWith(".xlsx"))
+        {
+            return ExcelTypeEnum.XLSX;
+        }
+        if (name.endsWith(".xls"))
+        {
+            return ExcelTypeEnum.XLS;
+        }
+        return null;
+    }
+}

+ 138 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/domain/DataElectricityLocal.java

@@ -0,0 +1,138 @@
+package com.ufo.project.model.domain;
+
+import java.util.Date;
+
+import com.ufo.base.framework.web.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 功率采样数据对象 data_electricity_local
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public class DataElectricityLocal extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 时间戳(带时区) */
+    @Excel(name = "时间戳", readConverterExp = "带=时区")
+    private Date tm;
+
+    /** 光伏场站ID */
+    @Excel(name = "光伏场站ID")
+    private Long configStationId;
+
+    /** 实际发电功率(kW) */
+    @Excel(name = "实际发电功率")
+    private Double actualPower;
+
+    /** 理论发电功率(kW) */
+    @Excel(name = "理论发电功率")
+    private Double theoreticalPower;
+
+    /** 限电功率(kW) */
+    @Excel(name = "限电功率")
+    private Double curtailmentPower;
+
+    /** 可用率(%) */
+    @Excel(name = "可用率")
+    private Double availability;
+
+    /** 设备状态(0=正常,1=停机,2=限电,3=检修,4=故障,只要一个处于异常就设为某个异常) */
+    @Excel(name = "设备状态", readConverterExp = "0=正常,1=停机,2=限电,3=检修,4=故障,只要一个处于异常就设为某个异常")
+    private Integer deviceStatus;
+
+    /** 并网状态(1=并网,0=离网) */
+    @Excel(name = "并网状态", readConverterExp = "1=并网,0=离网")
+    private Integer gridStatus;
+
+    public void setTm(Date tm) 
+    {
+        this.tm = tm;
+    }
+
+    public Date getTm() 
+    {
+        return tm;
+    }
+    public void setConfigStationId(Long configStationId) 
+    {
+        this.configStationId = configStationId;
+    }
+
+    public Long getConfigStationId() 
+    {
+        return configStationId;
+    }
+    public void setActualPower(Double actualPower) 
+    {
+        this.actualPower = actualPower;
+    }
+
+    public Double getActualPower() 
+    {
+        return actualPower;
+    }
+    public void setTheoreticalPower(Double theoreticalPower) 
+    {
+        this.theoreticalPower = theoreticalPower;
+    }
+
+    public Double getTheoreticalPower() 
+    {
+        return theoreticalPower;
+    }
+    public void setCurtailmentPower(Double curtailmentPower) 
+    {
+        this.curtailmentPower = curtailmentPower;
+    }
+
+    public Double getCurtailmentPower() 
+    {
+        return curtailmentPower;
+    }
+    public void setAvailability(Double availability) 
+    {
+        this.availability = availability;
+    }
+
+    public Double getAvailability() 
+    {
+        return availability;
+    }
+    public void setDeviceStatus(Integer deviceStatus) 
+    {
+        this.deviceStatus = deviceStatus;
+    }
+
+    public Integer getDeviceStatus() 
+    {
+        return deviceStatus;
+    }
+    public void setGridStatus(Integer gridStatus) 
+    {
+        this.gridStatus = gridStatus;
+    }
+
+    public Integer getGridStatus() 
+    {
+        return gridStatus;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("tm", getTm())
+            .append("configStationId", getConfigStationId())
+            .append("actualPower", getActualPower())
+            .append("theoreticalPower", getTheoreticalPower())
+            .append("curtailmentPower", getCurtailmentPower())
+            .append("availability", getAvailability())
+            .append("deviceStatus", getDeviceStatus())
+            .append("gridStatus", getGridStatus())
+            .toString();
+    }
+}

+ 75 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/mapper/DataElectricityLocalMapper.java

@@ -0,0 +1,75 @@
+package com.ufo.project.model.mapper;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+import com.ufo.project.model.domain.DataElectricityLocal;
+
+/**
+ * 功率采样数据Mapper接口
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public interface DataElectricityLocalMapper 
+{
+    /**
+     * 查询功率采样数据
+     * 
+     * @param tm 功率采样数据主键
+     * @return 功率采样数据
+     */
+    public DataElectricityLocal selectDataElectricityLocalByTm(Date tm);
+
+    /**
+     * 查询功率采样数据列表
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 功率采样数据集合
+     */
+    public List<DataElectricityLocal> selectDataElectricityLocalList(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 查询场站时间段内的实发功率数据
+     *
+     * @param configStationId 场站id
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    public List<DataElectricityLocal> selectActualPowerList(@Param("configStationId") Long configStationId,
+            @Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+    /**
+     * 新增功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    public int insertDataElectricityLocal(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 修改功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    public int updateDataElectricityLocal(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 删除功率采样数据
+     * 
+     * @param tm 功率采样数据主键
+     * @return 结果
+     */
+    public int deleteDataElectricityLocalByTm(Date tm);
+
+    /**
+     * 批量删除功率采样数据
+     * 
+     * @param tms 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteDataElectricityLocalByTms(Date[] tms);
+}

+ 15 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/mapper/ModelPredictionTasksMapper.java

@@ -1,6 +1,8 @@
 package com.ufo.project.model.mapper;
 
+import java.util.Date;
 import java.util.List;
+import org.apache.ibatis.annotations.Param;
 import com.ufo.project.model.domain.ModelPredictionTasks;
 
 /**
@@ -27,6 +29,19 @@ public interface ModelPredictionTasksMapper
      */
     public List<ModelPredictionTasks> selectModelPredictionTasksList(ModelPredictionTasks modelPredictionTasks);
 
+    /**
+     * 查询预测任务(version_source=station、status=SUCCESS、target_time在时间区间内)
+     *
+     * @param stationId 场站id
+     * @param predictionType 预测类型 short_term/ultra_short
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 预测任务集合(按 target_time 升序)
+     */
+    public List<ModelPredictionTasks> selectPredictionTasks(@Param("stationId") Long stationId,
+            @Param("predictionType") String predictionType,
+            @Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
     /**
      * 新增预测任务
      * 

+ 73 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/service/IDataElectricityLocalService.java

@@ -0,0 +1,73 @@
+package com.ufo.project.model.service;
+
+import java.util.Date;
+import java.util.List;
+
+import com.ufo.project.model.domain.DataElectricityLocal;
+
+/**
+ * 功率采样数据Service接口
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public interface IDataElectricityLocalService 
+{
+    /**
+     * 查询功率采样数据
+     * 
+     * @param tm 功率采样数据主键
+     * @return 功率采样数据
+     */
+    public DataElectricityLocal selectDataElectricityLocalByTm(Date tm);
+
+    /**
+     * 查询功率采样数据列表
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 功率采样数据集合
+     */
+    public List<DataElectricityLocal> selectDataElectricityLocalList(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 查询场站时间段内的实发功率数据
+     *
+     * @param configStationId 场站id
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    public List<DataElectricityLocal> selectActualPowerList(Long configStationId, Date startTime, Date endTime);
+
+    /**
+     * 新增功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    public int insertDataElectricityLocal(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 修改功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    public int updateDataElectricityLocal(DataElectricityLocal dataElectricityLocal);
+
+    /**
+     * 批量删除功率采样数据
+     * 
+     * @param tms 需要删除的功率采样数据主键集合
+     * @return 结果
+     */
+    public int deleteDataElectricityLocalByTms(Date[] tms);
+
+    /**
+     * 删除功率采样数据信息
+     * 
+     * @param tm 功率采样数据主键
+     * @return 结果
+     */
+    public int deleteDataElectricityLocalByTm(Date tm);
+}

+ 12 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/service/IModelPredictionTasksService.java

@@ -1,5 +1,6 @@
 package com.ufo.project.model.service;
 
+import java.util.Date;
 import java.util.List;
 import com.ufo.project.model.domain.ModelPredictionTasks;
 
@@ -27,6 +28,17 @@ public interface IModelPredictionTasksService
      */
     public List<ModelPredictionTasks> selectModelPredictionTasksList(ModelPredictionTasks modelPredictionTasks);
 
+    /**
+     * 查询预测任务(version_source=station、status=SUCCESS、target_time在时间区间内)
+     *
+     * @param stationId 场站id
+     * @param predictionType 预测类型 short_term/ultra_short
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 预测任务集合(按 target_time 升序)
+     */
+    public List<ModelPredictionTasks> selectPredictionTasks(Long stationId, String predictionType, Date startTime, Date endTime);
+
     /**
      * 新增预测任务
      * 

+ 109 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/DataElectricityLocalServiceImpl.java

@@ -0,0 +1,109 @@
+package com.ufo.project.model.service.impl;
+
+import java.util.Date;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.mapper.DataElectricityLocalMapper;
+import com.ufo.project.model.service.IDataElectricityLocalService;
+
+/**
+ * 功率采样数据Service业务层处理
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+@Service
+public class DataElectricityLocalServiceImpl implements IDataElectricityLocalService 
+{
+    @Autowired
+    private DataElectricityLocalMapper dataElectricityLocalMapper;
+
+    /**
+     * 查询功率采样数据
+     * 
+     * @param tm 功率采样数据主键
+     * @return 功率采样数据
+     */
+    @Override
+    public DataElectricityLocal selectDataElectricityLocalByTm(Date tm)
+    {
+        return dataElectricityLocalMapper.selectDataElectricityLocalByTm(tm);
+    }
+
+    /**
+     * 查询功率采样数据列表
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 功率采样数据
+     */
+    @Override
+    public List<DataElectricityLocal> selectDataElectricityLocalList(DataElectricityLocal dataElectricityLocal)
+    {
+        return dataElectricityLocalMapper.selectDataElectricityLocalList(dataElectricityLocal);
+    }
+
+    /**
+     * 查询场站时间段内的实发功率数据
+     *
+     * @param configStationId 场站id
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    @Override
+    public List<DataElectricityLocal> selectActualPowerList(Long configStationId, Date startTime, Date endTime)
+    {
+        return dataElectricityLocalMapper.selectActualPowerList(configStationId, startTime, endTime);
+    }
+
+    /**
+     * 新增功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    @Override
+    public int insertDataElectricityLocal(DataElectricityLocal dataElectricityLocal)
+    {
+        return dataElectricityLocalMapper.insertDataElectricityLocal(dataElectricityLocal);
+    }
+
+    /**
+     * 修改功率采样数据
+     * 
+     * @param dataElectricityLocal 功率采样数据
+     * @return 结果
+     */
+    @Override
+    public int updateDataElectricityLocal(DataElectricityLocal dataElectricityLocal)
+    {
+        return dataElectricityLocalMapper.updateDataElectricityLocal(dataElectricityLocal);
+    }
+
+    /**
+     * 批量删除功率采样数据
+     * 
+     * @param tms 需要删除的功率采样数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDataElectricityLocalByTms(Date[] tms)
+    {
+        return dataElectricityLocalMapper.deleteDataElectricityLocalByTms(tms);
+    }
+
+    /**
+     * 删除功率采样数据信息
+     * 
+     * @param tm 功率采样数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDataElectricityLocalByTm(Date tm)
+    {
+        return dataElectricityLocalMapper.deleteDataElectricityLocalByTm(tm);
+    }
+}

+ 16 - 0
forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/ModelPredictionTasksServiceImpl.java

@@ -1,5 +1,6 @@
 package com.ufo.project.model.service.impl;
 
+import java.util.Date;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -43,6 +44,21 @@ public class ModelPredictionTasksServiceImpl implements IModelPredictionTasksSer
         return modelPredictionTasksMapper.selectModelPredictionTasksList(modelPredictionTasks);
     }
 
+    /**
+     * 查询预测任务(version_source=station、status=SUCCESS、target_time在时间区间内)
+     *
+     * @param stationId 场站id
+     * @param predictionType 预测类型 short_term/ultra_short
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 预测任务集合(按 target_time 升序)
+     */
+    @Override
+    public List<ModelPredictionTasks> selectPredictionTasks(Long stationId, String predictionType, Date startTime, Date endTime)
+    {
+        return modelPredictionTasksMapper.selectPredictionTasks(stationId, predictionType, startTime, endTime);
+    }
+
     /**
      * 新增预测任务
      * 

+ 97 - 0
forecast-backend-server/src/main/resources/mybatis/model/DataElectricityLocalMapper.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ufo.project.model.mapper.DataElectricityLocalMapper">
+    
+    <resultMap type="DataElectricityLocal" id="DataElectricityLocalResult">
+        <result property="tm"    column="tm"    />
+        <result property="configStationId"    column="config_station_id"    />
+        <result property="actualPower"    column="actual_power"    />
+        <result property="theoreticalPower"    column="theoretical_power"    />
+        <result property="curtailmentPower"    column="curtailment_power"    />
+        <result property="availability"    column="availability"    />
+        <result property="deviceStatus"    column="device_status"    />
+        <result property="gridStatus"    column="grid_status"    />
+    </resultMap>
+
+    <sql id="selectDataElectricityLocalVo">
+        select tm, config_station_id, actual_power, theoretical_power, curtailment_power, availability, device_status, grid_status from data_electricity_local
+    </sql>
+
+    <select id="selectDataElectricityLocalList" parameterType="DataElectricityLocal" resultMap="DataElectricityLocalResult">
+        <include refid="selectDataElectricityLocalVo"/>
+        <where>  
+            <if test="tm != null "> and tm = #{tm}</if>
+            <if test="configStationId != null "> and config_station_id = #{configStationId}</if>
+            <if test="actualPower != null "> and actual_power = #{actualPower}</if>
+            <if test="theoreticalPower != null "> and theoretical_power = #{theoreticalPower}</if>
+            <if test="curtailmentPower != null "> and curtailment_power = #{curtailmentPower}</if>
+            <if test="availability != null "> and availability = #{availability}</if>
+            <if test="deviceStatus != null "> and device_status = #{deviceStatus}</if>
+            <if test="gridStatus != null "> and grid_status = #{gridStatus}</if>
+        </where>
+    </select>
+    
+    <select id="selectDataElectricityLocalByTm" parameterType="Date" resultMap="DataElectricityLocalResult">
+        <include refid="selectDataElectricityLocalVo"/>
+        where tm = #{tm}
+    </select>
+
+    <select id="selectActualPowerList" resultMap="DataElectricityLocalResult">
+        <include refid="selectDataElectricityLocalVo"/>
+        where config_station_id = #{configStationId}
+          and tm &gt;= #{startTime}
+          and tm &lt;= #{endTime}
+        order by tm asc
+    </select>
+
+    <insert id="insertDataElectricityLocal" parameterType="DataElectricityLocal">
+        insert into data_electricity_local
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="tm != null">tm,</if>
+            <if test="configStationId != null">config_station_id,</if>
+            <if test="actualPower != null">actual_power,</if>
+            <if test="theoreticalPower != null">theoretical_power,</if>
+            <if test="curtailmentPower != null">curtailment_power,</if>
+            <if test="availability != null">availability,</if>
+            <if test="deviceStatus != null">device_status,</if>
+            <if test="gridStatus != null">grid_status,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="tm != null">#{tm},</if>
+            <if test="configStationId != null">#{configStationId},</if>
+            <if test="actualPower != null">#{actualPower},</if>
+            <if test="theoreticalPower != null">#{theoreticalPower},</if>
+            <if test="curtailmentPower != null">#{curtailmentPower},</if>
+            <if test="availability != null">#{availability},</if>
+            <if test="deviceStatus != null">#{deviceStatus},</if>
+            <if test="gridStatus != null">#{gridStatus},</if>
+         </trim>
+    </insert>
+
+    <update id="updateDataElectricityLocal" parameterType="DataElectricityLocal">
+        update data_electricity_local
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="configStationId != null">config_station_id = #{configStationId},</if>
+            <if test="actualPower != null">actual_power = #{actualPower},</if>
+            <if test="theoreticalPower != null">theoretical_power = #{theoreticalPower},</if>
+            <if test="curtailmentPower != null">curtailment_power = #{curtailmentPower},</if>
+            <if test="availability != null">availability = #{availability},</if>
+            <if test="deviceStatus != null">device_status = #{deviceStatus},</if>
+            <if test="gridStatus != null">grid_status = #{gridStatus},</if>
+        </trim>
+        where tm = #{tm}
+    </update>
+
+    <delete id="deleteDataElectricityLocalByTm" parameterType="Date">
+        delete from data_electricity_local where tm = #{tm}
+    </delete>
+
+    <delete id="deleteDataElectricityLocalByTms" parameterType="String">
+        delete from data_electricity_local where tm in 
+        <foreach item="tm" collection="array" open="(" separator="," close=")">
+            #{tm}
+        </foreach>
+    </delete>
+</mapper>

+ 12 - 1
forecast-backend-server/src/main/resources/mybatis/model/ModelPredictionTasksMapper.xml

@@ -42,7 +42,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectModelPredictionTasksVo"/>
         where id = #{id}
     </select>
-        
+
+    <select id="selectPredictionTasks" resultMap="ModelPredictionTasksResult">
+        <include refid="selectModelPredictionTasksVo"/>
+        where station_id = #{stationId}
+          and version_source = 'station'
+          and prediction_type = #{predictionType}
+          and status = 'SUCCESS'
+          and target_time &gt;= #{startTime}
+          and target_time &lt;= #{endTime}
+        order by target_time asc
+    </select>
+
     <insert id="insertModelPredictionTasks" parameterType="ModelPredictionTasks" useGeneratedKeys="true" keyProperty="id">
         insert into model_prediction_tasks
         <trim prefix="(" suffix=")" suffixOverrides=",">

+ 12 - 0
forecast-backend-server/src/main/resources/sql/20260819_short_term_power_forecasting.sql

@@ -0,0 +1,12 @@
+-- =============================================================================
+-- 任务编号 2026081901  增加"短期功率预测"功能
+-- 适用数据库:PostgreSQL(线上库 electricity)
+-- 执行方式:直接在本脚本所在库执行即可
+-- =============================================================================
+
+-- 1. sys_menu 增加菜单: 短期功率预测 (id=2077, parent_id=2007)
+INSERT INTO "sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark")
+VALUES (2077, '短期功率预测', 2007, 4, 'short-term-power', 'ufo/electricity/forecast/ShortTermPowerForecasting', '', 'ShortTermPowerForecasting', 1, 0, 'C', '0', '0', 'electricity:forecast:shorttermpower', 'area-chart', 'admin', now(), '', NULL, '短期功率预测菜单');
+
+-- 2. sys_role_menu 增加角色-菜单关联 (role_id=1, menu_id=2077)
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2077);

+ 12 - 0
forecast-backend-server/src/main/resources/sql/20260819_ultra_short_term_power_forecasting.sql

@@ -0,0 +1,12 @@
+-- =============================================================================
+-- 任务编号 2026081902  增加"超短期功率预测"功能
+-- 适用数据库:PostgreSQL(线上库 electricity)
+-- 执行方式:直接在本脚本所在库执行即可
+-- =============================================================================
+
+-- 1. sys_menu 增加菜单: 超短期功率预测 (id=2078, parent_id=2007)
+INSERT INTO "sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark")
+VALUES (2078, '超短期功率预测', 2007, 4, 'ultra-short-term-power', 'ufo/electricity/forecast/UltraShortTermPowerForecasting', '', 'UltraShortTermPowerForecasting', 1, 0, 'C', '0', '0', 'electricity:forecast:ultrashorttermpower', 'area-chart', 'admin', now(), '', NULL, '超短期功率预测菜单');
+
+-- 2. sys_role_menu 增加角色-菜单关联 (role_id=1, menu_id=2078)
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2078);

+ 21 - 0
forecast-backend-vue/src/api/power/shortTermPowerForecasting.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+// 查询短期功率预测数据
+export function reportShortTermPowerForecasting(query) {
+  return request({
+    url: '/power/shortTermPowerForecasting/report',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出短期功率预测数据
+export function exportShortTermPowerForecasting(query) {
+  return request({
+    url: '/power/shortTermPowerForecasting/export',
+    method: 'get',
+    params: query,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 21 - 0
forecast-backend-vue/src/api/power/ultraShortTermPowerForecasting.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+// 查询超短期功率预测数据
+export function reportUltraShortTermPowerForecasting(query) {
+  return request({
+    url: '/power/ultraShortTermPowerForecasting/report',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出超短期功率预测数据
+export function exportUltraShortTermPowerForecasting(query) {
+  return request({
+    url: '/power/ultraShortTermPowerForecasting/export',
+    method: 'get',
+    params: query,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 12 - 0
forecast-backend-vue/src/router/electricity.js

@@ -90,6 +90,18 @@ const electricityRoutes = [
         component: () => import('@/views/ufo/electricity/forecast/GenerationForecast.vue'),
         meta: {title: '发电量预测', icon: 'AreaChartOutlined'}
       },
+      {
+        path: 'forecast/short-term-power',
+        name: 'ShortTermPowerForecasting',
+        component: () => import('@/views/ufo/electricity/forecast/ShortTermPowerForecasting.vue'),
+        meta: {title: '短期功率预测', icon: 'AreaChartOutlined'}
+      },
+      {
+        path: 'forecast/ultra-short-term-power',
+        name: 'UltraShortTermPowerForecasting',
+        component: () => import('@/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue'),
+        meta: {title: '超短期功率预测', icon: 'AreaChartOutlined'}
+      },
       {
         path: 'analysis/power-stats',
         name: 'ElectricityPowerStats',

+ 4 - 1
forecast-backend-vue/src/views/ufo/electricity/components/BusinessDataTable.vue

@@ -2,9 +2,10 @@
   <a-table
     :columns="columns"
     :data-source="dataSource"
-    :pagination="{pageSize: 5}"
+    :pagination="pagination"
     :row-key="rowKey"
     :locale="{emptyText}"
+    :scroll="scrollX ? {x: scrollX} : undefined"
     size="middle"
     bordered
   />
@@ -16,6 +17,8 @@ import {demoTableRows} from './constants';
 defineProps({
   rowKey: {type: String, default: 'id'},
   emptyText: {type: String, default: '暂无数据'},
+  pagination: {type: Object, default: () => ({pageSize: 5})},
+  scrollX: {type: [String, Number], default: null},
   dataSource: {type: Array, default: () => demoTableRows},
   columns: {
     type: Array,

+ 39 - 20
forecast-backend-vue/src/views/ufo/electricity/components/PowerLineChart.vue

@@ -18,31 +18,50 @@ const props = defineProps({
     ]
   },
   series: {type: Array, default: () => demoSeries},
-  yAxisName: {type: String, default: 'MW'}
+  yAxisName: {type: String, default: 'MW'},
+  legendSelected: {type: Object, default: () => null},
+  legendWidth: {type: [String, Number], default: null}
 });
 
 const chartRef = ref();
 let chartInstance = null;
 
-const chartOption = computed(() => ({
-  color: props.series.map((item) => item.color).filter(Boolean),
-  tooltip: {trigger: 'axis'},
-  legend: {top: 0, left: 'center'},
-  grid: {left: 42, right: 24, top: 46, bottom: 52},
-  xAxis: {type: 'category', boundaryGap: false, data: props.xAxis},
-  yAxis: {type: 'value', name: props.yAxisName, splitLine: {lineStyle: {color: '#e6eef8'}}},
-  dataZoom: [
-    {type: 'inside', start: 0, end: 100},
-    {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
-  ],
-  series: props.series.map((item) => ({
-    name: item.name,
-    type: 'line',
-    smooth: true,
-    showSymbol: false,
-    data: item.data
-  }))
-}));
+const chartOption = computed(() => {
+  const legend = {
+    top: 0,
+    left: 'center'
+  };
+  if (props.legendWidth) {
+    // 图例项较多时约束宽度使其换行展示
+    legend.width = props.legendWidth;
+    legend.itemWidth = 18;
+    legend.itemHeight = 12;
+    legend.itemGap = 10;
+    legend.textStyle = {fontSize: 12};
+  }
+  if (props.legendSelected) {
+    legend.selected = props.legendSelected;
+  }
+  return {
+    color: props.series.map((item) => item.color).filter(Boolean),
+    tooltip: {trigger: 'axis'},
+    legend,
+    grid: {left: 42, right: 24, top: props.legendWidth ? 58 : 46, bottom: 52},
+    xAxis: {type: 'category', boundaryGap: false, data: props.xAxis},
+    yAxis: {type: 'value', name: props.yAxisName, splitLine: {lineStyle: {color: '#e6eef8'}}},
+    dataZoom: [
+      {type: 'inside', start: 0, end: 100},
+      {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
+    ],
+    series: props.series.map((item) => ({
+      name: item.name,
+      type: 'line',
+      smooth: true,
+      showSymbol: false,
+      data: item.data
+    }))
+  };
+});
 
 const renderChart = async () => {
   await nextTick();

+ 150 - 0
forecast-backend-vue/src/views/ufo/electricity/forecast/ShortTermPowerForecasting.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="short-term-power-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="8">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="6">
+          <a-select v-model:value="query.forecastHours" :options="forecastHoursOptions" class="full-control" />
+        </a-col>
+        <a-col :xs="24" :md="6">
+          <a-space>
+            <a-button type="primary" :loading="loading" @click="handleQuery">确认</a-button>
+            <a-button :loading="exporting" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-row :gutter="[16, 16]" class="content-row">
+      <a-col :span="24">
+        <a-card title="短期预测曲线图" :bordered="false">
+          <PowerLineChart :x-axis="chartAxis" :series="chartSeries" height="360px" y-axis-name="kW" />
+        </a-card>
+      </a-col>
+      <a-col :span="24">
+        <a-card title="数据详情" :bordered="false">
+          <BusinessDataTable :columns="tableColumns" :data-source="tableRows" :pagination="{pageSize: 10}" />
+        </a-card>
+      </a-col>
+    </a-row>
+  </div>
+</template>
+
+<script setup name="ShortTermPowerForecasting">
+import {onMounted, reactive, ref} from 'vue';
+import {message} from 'ant-design-vue';
+import {saveAs} from 'file-saver';
+import {blobValidate} from '@/utils/bearjia';
+import {listStation} from '@/api/config/station';
+import {exportShortTermPowerForecasting, reportShortTermPowerForecasting} from '@/api/power/shortTermPowerForecasting';
+import BusinessDataTable from '../components/BusinessDataTable.vue';
+import PowerLineChart from '../components/PowerLineChart.vue';
+import StationSelector from '../components/StationSelector.vue';
+import {buildLineFromRows, normalizeRows} from '../components/dataAdapter';
+import {toStationOptions} from '../components/stationAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const chartAxis = ref([]);
+const chartSeries = ref([]);
+const tableRows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  forecastHours: 24
+});
+
+const forecastHoursOptions = [
+  {label: '24小时', value: 24},
+  {label: '48小时', value: 48},
+  {label: '72小时', value: 72}
+];
+
+const tableColumns = [
+  {title: '预测时间', dataIndex: 'time'},
+  {title: '预测功率', dataIndex: 'power'},
+  {title: '理论功率', dataIndex: 'theoretical_power'},
+  {title: '限制功率', dataIndex: 'curtailment_power'}
+];
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  const rows = normalizeRows(res);
+  stationOptions.value = toStationOptions(rows);
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value;
+  }
+};
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await reportShortTermPowerForecasting({
+      stationId: query.stationId,
+      forecastHours: query.forecastHours
+    });
+    const rows = normalizeRows(res);
+    tableRows.value = rows;
+    const lineModel = buildLineFromRows(rows, [
+      {name: '预测功率', color: '#1677ff', keys: ['power']},
+      {name: '理论功率', color: '#52c41a', keys: ['theoretical_power']},
+      {name: '限制功率', color: '#f5222d', keys: ['curtailment_power']}
+    ]);
+    chartAxis.value = lineModel.xAxis;
+    chartSeries.value = lineModel.series;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  exporting.value = true;
+  try {
+    const res = await exportShortTermPowerForecasting({
+      stationId: query.stationId,
+      forecastHours: query.forecastHours
+    });
+    const isFile = await blobValidate(res);
+    if (isFile) {
+      const now = new Date();
+      const pad = (n) => String(n).padStart(2, '0');
+      const filename = `短期预测-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}.xlsx`;
+      saveAs(new Blob([res]), filename);
+      message.success('导出成功');
+    } else {
+      const text = await res.text();
+      let msg = '导出失败';
+      try {
+        msg = JSON.parse(text)?.msg || msg;
+      } catch (e) {
+        /* ignore */
+      }
+      message.error(msg);
+    }
+  } finally {
+    exporting.value = false;
+  }
+};
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.short-term-power-page { padding: 16px; }
+.content-row { margin-top: 16px; }
+.full-control { width: 100%; }
+</style>

+ 244 - 0
forecast-backend-vue/src/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="ultra-short-term-power-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.startTime"
+            show-time
+            format="YYYY-MM-DD HH:mm:ss"
+            placeholder="起始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.endTime"
+            show-time
+            format="YYYY-MM-DD HH:mm:ss"
+            placeholder="截止时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="8">
+          <a-space>
+            <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-tabs v-model:activeKey="query.type" class="content-tabs">
+      <a-tab-pane key="0" tab="单点预测">
+        <a-card :bordered="false">
+          <PowerLineChart :x-axis="pointChartAxis" :series="pointChartSeries" :legend-selected="legendSelected" legend-width="55%" height="360px" y-axis-name="kW" />
+        </a-card>
+        <a-card title="数据详情" :bordered="false" class="detail-card">
+          <BusinessDataTable :columns="pointColumns" :data-source="tableRows" :pagination="{pageSize: 10}" :scroll-x="'max-content'" />
+        </a-card>
+      </a-tab-pane>
+      <a-tab-pane key="1" tab="单次预测">
+        <a-card :bordered="false">
+          <div class="chart-toolbar">
+            <span class="toolbar-label">预测时间点:</span>
+            <a-select v-model:value="query.selectedTimePoint" :options="timePointOptions" placeholder="全部" allow-clear class="time-point-select" />
+          </div>
+          <PowerLineChart :x-axis="singleChartAxis" :series="singleChartSeries" height="360px" y-axis-name="kW" />
+        </a-card>
+        <a-card title="数据详情" :bordered="false" class="detail-card">
+          <BusinessDataTable :columns="singleColumns" :data-source="singleTableRows" :pagination="{pageSize: 10}" />
+        </a-card>
+      </a-tab-pane>
+    </a-tabs>
+  </div>
+</template>
+
+<script setup name="UltraShortTermPowerForecasting">
+import {computed, onMounted, reactive, ref} from 'vue';
+import {message} from 'ant-design-vue';
+import dayjs from 'dayjs';
+import {saveAs} from 'file-saver';
+import {blobValidate} from '@/utils/bearjia';
+import {listStation} from '@/api/config/station';
+import {exportUltraShortTermPowerForecasting, reportUltraShortTermPowerForecasting} from '@/api/power/ultraShortTermPowerForecasting';
+import BusinessDataTable from '../components/BusinessDataTable.vue';
+import PowerLineChart from '../components/PowerLineChart.vue';
+import StationSelector from '../components/StationSelector.vue';
+import {buildLineFromRows, normalizeRows} from '../components/dataAdapter';
+import {toStationOptions} from '../components/stationAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const tableRows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs().subtract(1, 'hour'),
+  endTime: dayjs(),
+  type: '0',
+  selectedTimePoint: undefined
+});
+
+const horizonMinutes = [15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240];
+const metricColors = [
+  '#1677ff', '#52c41a', '#f5222d', '#faad14', '#13c2c2', '#722ed1', '#eb2f96',
+  '#a0d911', '#fa8c16', '#2f54eb', '#fadb14', '#08979c', '#531dab', '#d4380d',
+  '#1d39c4', '#389e0d', '#c41d7f'
+];
+
+const metricOptions = [
+  {label: '实测功率', value: 'power'},
+  ...horizonMinutes.map((m) => ({label: `${m}分钟`, value: `power${m}`}))
+];
+
+const pointColumns = [
+  {title: '日期', dataIndex: 'time'},
+  {title: '实测功率', dataIndex: 'power'},
+  ...horizonMinutes.map((m) => ({title: `${m}分钟`, dataIndex: `power${m}`}))
+];
+
+const singleColumns = [
+  {title: '日期', dataIndex: 'time'},
+  {title: '实发功率', dataIndex: 'monitoredPower'},
+  {title: '超短期预测', dataIndex: 'power'},
+  {title: '偏差', dataIndex: 'bias'}
+];
+
+const queryValid = computed(() => {
+  if (!query.startTime || !query.endTime) {
+    return false;
+  }
+  const start = dayjs(query.startTime);
+  const end = dayjs(query.endTime);
+  if (!start.isValid() || !end.isValid()) {
+    return false;
+  }
+  return !end.isBefore(start);
+});
+
+const filteredRows = computed(() => {
+  if (!query.selectedTimePoint) {
+    return tableRows.value;
+  }
+  return tableRows.value.filter((row) => String(row.time) >= String(query.selectedTimePoint));
+});
+
+const defaultActiveMetrics = ['power', 'power240'];
+
+const pointChartAxis = computed(() => tableRows.value.map((item) => item.time));
+const pointChartSeries = computed(() => {
+  const defs = metricOptions.map((option, index) => ({
+    name: option.label,
+    color: metricColors[index % metricColors.length],
+    keys: [option.value]
+  }));
+  return buildLineFromRows(tableRows.value, defs).series;
+});
+
+const legendSelected = computed(() => {
+  const map = {};
+  metricOptions.forEach((option) => {
+    map[option.label] = defaultActiveMetrics.includes(option.value);
+  });
+  return map;
+});
+
+const singleChartAxis = computed(() => filteredRows.value.map((item) => item.time));
+const singleChartSeries = computed(() => buildLineFromRows(filteredRows.value, [
+  {name: '实发功率', color: '#1677ff', keys: ['monitoredPower']},
+  {name: '超短期预测', color: '#52c41a', keys: ['power']},
+  {name: '偏差', color: '#f5222d', keys: ['bias']}
+]).series);
+
+const singleTableRows = computed(() => filteredRows.value);
+
+const timePointOptions = computed(() => tableRows.value.map((item) => ({label: item.time, value: item.time})));
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  const rows = normalizeRows(res);
+  stationOptions.value = toStationOptions(rows);
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value;
+  }
+};
+
+const buildParams = () => ({
+  stationId: query.stationId,
+  startTime: dayjs(query.startTime).format('YYYY-MM-DD HH:mm:ss'),
+  endTime: dayjs(query.endTime).format('YYYY-MM-DD HH:mm:ss')
+});
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) {
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await reportUltraShortTermPowerForecasting(buildParams());
+    tableRows.value = normalizeRows(res);
+    query.selectedTimePoint = undefined;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) {
+    return;
+  }
+  exporting.value = true;
+  try {
+    const res = await exportUltraShortTermPowerForecasting({
+      ...buildParams(),
+      type: query.type
+    });
+    const isFile = await blobValidate(res);
+    if (isFile) {
+      const start = dayjs(query.startTime).format('YYYYMMDDHHmm');
+      const end = dayjs(query.endTime).format('YYYYMMDDHHmm');
+      saveAs(new Blob([res]), `超短期预测-${start}-${end}.xlsx`);
+      message.success('导出成功');
+    } else {
+      const text = await res.text();
+      let msg = '导出失败';
+      try {
+        msg = JSON.parse(text)?.msg || msg;
+      } catch (e) {
+        /* ignore */
+      }
+      message.error(msg);
+    }
+  } finally {
+    exporting.value = false;
+  }
+};
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.ultra-short-term-power-page { padding: 16px; }
+.content-tabs { margin-top: 16px; }
+.full-control { width: 100%; }
+.detail-card { margin-top: 16px; }
+.chart-toolbar { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 8px; }
+.toolbar-label { margin-right: 8px; color: rgba(0, 0, 0, 0.85); }
+.time-point-select { min-width: 200px; }
+</style>

+ 10 - 0
release_note/2026.08.20/20260820_dp.sql

@@ -0,0 +1,10 @@
+-- admin 增加 超短期功率预测 菜单
+INSERT INTO "sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark")
+VALUES (2078, '超短期功率预测', 2007, 4, 'ultra-short-term-power', 'ufo/electricity/forecast/UltraShortTermPowerForecasting', '', 'UltraShortTermPowerForecasting', 1, 0, 'C', '0', '0', 'electricity:forecast:ultrashorttermpower', 'area-chart', 'admin', now(), '', NULL, '超短期功率预测菜单');
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2078);
+
+
+-- admin 增加 短期功率预测 菜单
+INSERT INTO "sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark")
+VALUES (2077, '短期功率预测', 2007, 4, 'short-term-power', 'ufo/electricity/forecast/ShortTermPowerForecasting', '', 'ShortTermPowerForecasting', 1, 0, 'C', '0', '0', 'electricity:forecast:shorttermpower', 'area-chart', 'admin', now(), '', NULL, '短期功率预测菜单');
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2077);

+ 17 - 0
release_note/2026.08.20/20260820_release.pdf

@@ -0,0 +1,17 @@
+1. 外部引用的地方用引用对象的名称,是否列的地方用一个开关。[已修复]
+
+2. 页面风格同一用系统管理中的角色这种风格,原因在于需要导出,刷新表格,
+列部分可视化。[已修复]
+
+3. 设备配置用的是 config_station_device 吗?改为使用 config_device。欧力把
+config_station_device 设计考虑到的列迁移到 config_device 中去, 我们需更换表
+名和增加 config_device 中的列。[已修复]
+
+4. 通道配置是用 config_channel,不是用 config_station_channel。[已修复]
+
+5. 在”预报管理“模块中,增加:参考国能日新 PDF 所示的“超短期功率预测、
+短期功率预测、中期功率预测”。[已完成]
+[短期预测]
+[超短期预测-单次预测]
+[超短期预测-单点预测]
+

+ 28 - 0
release_note/2026.08.20/readme_tsj.md

@@ -0,0 +1,28 @@
+改动内容
+
+1. 外部引用的地方用引用对象的名称,是否列的地方用一个开关。
+2. 页面风格同一用系统管理中的角色这种风格,原因在于需要导出,刷新表格,列部分可视化。
+3. 设备配置用的是config_station_device吗?改为使用config_device。欧力把config_station_device设计考虑到的列迁移到config_device中去, 我们需更换表名和增加config_device中的列。
+4. 通道配置是用config_channel,不是用config_station_channel。
+5. 在”预报管理“模块中,增加:参考国能日新PDF所示的“超短期功率预测、短期功率预测、中期功率预测”。
+
+改动结果可以查看 [20260820_release.pdf]
+	
+
+[更新说明]
+1. 执行 [20260820_dp.sql]
+2. 更新服务端jar
+3. 更新前端dist
+
+[测试说明]
+短期功率预测、超短期功率预测依赖于实测数据、预测数据。进行测试前请检查数据准确性
+** 实测数据 ** 
+- 请确定表 `data_electricity_local` 中有测试时间区域内数据,无数据默认0。
+** 预测数据 **
+- 请确定表 `model_prediction_tasks` 中有测试时间区域内数据,无数据默认0。
+    - station_id        测试场站id
+    - versions_source   station
+    - prediction_type   short_term 短期、ultra_short 超短期
+    - target_time       预测时间
+    - status            SUCCESS
+    - result_path       预测给出的实际文件路径,并保证文件存在且服务端可读