فهرست منبع

[0920] 超短期功率预测使用预报值进行数据查询

taosj 1 ماه پیش
والد
کامیت
2db87ef7e0
13فایلهای تغییر یافته به همراه431 افزوده شده و 134 حذف شده
  1. 131 0
      doc/数据模拟脚本/实测/mock_data_electricity_local.sql
  2. 17 7
      doc/数据模拟脚本/超短期功率预测/mock_data_ultra_short_term_power_forecasting.sql
  3. 30 0
      forecast-backend-server/src/main/java/com/ufo/base/common/utils/TimeUtils.java
  4. 114 28
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/UltraShortTermPowerForecastingController.java
  5. 5 5
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastData.java
  6. 23 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastOnceData.java
  7. 0 67
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerSingleExport.java
  8. 11 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/mapper/DataUltraShortTermPowerForecastingMapper.java
  9. 9 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/service/IDataUltraShortTermPowerForecastingService.java
  10. 5 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/service/impl/DataUltraShortTermPowerForecastingServiceImpl.java
  11. 6 0
      forecast-backend-server/src/main/resources/mybatis/supershort/DataUltraShortTermPowerForecastingMapper.xml
  12. 9 0
      forecast-backend-vue/src/api/power/ultraShortTermPowerForecasting.js
  13. 71 27
      forecast-backend-vue/src/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue

+ 131 - 0
doc/数据模拟脚本/实测/mock_data_electricity_local.sql

@@ -0,0 +1,131 @@
+-- ============================================================
+-- 脚本功能:独立生成光伏电站实测数据 (data_electricity_local)
+-- 修正说明:
+--   1. 彻底修复所有 ROUND 函数的类型问题。
+--   2. 所有涉及浮点运算的结果均显式转换为 NUMERIC。
+-- ============================================================
+DO $$
+DECLARE
+    -- ================= 配置区域 =================
+    v_past_days        INTEGER := 30;              -- 从过去多少天开始
+    v_future_days      INTEGER := 10;              -- 到未来多少天结束
+    v_interval_minutes INTEGER := 15;              -- 数据间隔(分钟)
+    
+    -- 模拟场站配置
+    v_station_id       BIGINT := 2;                -- 光伏场站ID
+    v_max_power        NUMERIC := 100.0;           -- 理论最大发电功率(装机容量 kW)
+
+    -- 循环变量
+    v_start_timestamp  TIMESTAMP WITH TIME ZONE;
+    v_end_timestamp    TIMESTAMP WITH TIME ZONE;
+    v_current_time     TIMESTAMP WITH TIME ZONE;
+    
+    -- 计算变量
+    v_hour_of_day      NUMERIC;
+    v_solar_noon       NUMERIC := 12.5;            -- 正午时间(12:30左右)
+    v_sunshine_hours   NUMERIC := 11.0;            -- 日照时长(假设值)
+    
+    -- 最终插入的字段值
+    v_theoretical_power NUMERIC;
+    v_actual_power     NUMERIC;
+    v_curtailment_power NUMERIC;
+    v_device_status    SMALLINT;
+    v_availability     NUMERIC;
+    v_grid_status      SMALLINT;
+
+BEGIN
+    RAISE NOTICE '开始生成光伏实测数据。场站ID: %', v_station_id;
+
+    -- 1. 计算时间范围
+    v_start_timestamp := (CURRENT_DATE - INTERVAL '1 day' * v_past_days) AT TIME ZONE 'Asia/Shanghai';
+    v_end_timestamp := (CURRENT_DATE + INTERVAL '1 day' * v_future_days) AT TIME ZONE 'Asia/Shanghai';
+    v_current_time := v_start_timestamp;
+
+    -- 2. 清理旧数据
+    DELETE FROM public.data_electricity_local 
+    WHERE config_station_id = v_station_id
+      AND tm >= v_start_timestamp
+      AND tm <= v_end_timestamp;
+
+    -- 3. 循环生成数据
+    WHILE v_current_time <= v_end_timestamp LOOP
+        -- 获取小时数(带小数,例如 13.25 代表 13:15)
+        v_hour_of_day := EXTRACT(HOUR FROM v_current_time) + EXTRACT(MINUTE FROM v_current_time) / 60.0;
+        
+        -- ================= 核心算法:模拟光伏出力 =================
+        IF v_hour_of_day BETWEEN (v_solar_noon - v_sunshine_hours/2) AND (v_solar_noon + v_sunshine_hours/2) THEN
+            -- 计算正弦值
+            DECLARE
+                v_day_factor NUMERIC;
+            BEGIN
+                v_day_factor := (v_hour_of_day - (v_solar_noon - v_sunshine_hours/2)) / v_sunshine_hours;
+                -- 修正点:将 SIN 的结果转换为 NUMERIC
+                v_theoretical_power := v_max_power * SIN(PI() * v_day_factor)::NUMERIC;
+                -- 增加随机波动(修正点:将 RANDOM() 结果转换为 NUMERIC)
+                v_theoretical_power := v_theoretical_power * (0.95::NUMERIC + RANDOM()::NUMERIC * 0.1::NUMERIC);
+                v_theoretical_power := ROUND(v_theoretical_power, 2);
+            END;
+        ELSE
+            v_theoretical_power := 0;
+        END IF;
+
+        -- ================= 衍生字段逻辑 =================
+        
+        -- 1. 实际功率(修正点:显式转换)
+        v_actual_power := ROUND(v_theoretical_power * 0.98::NUMERIC, 2);
+        
+        -- 2. 限电功率与设备状态(修正点:显式转换)
+        IF RANDOM() > 0.95 AND v_actual_power > 10 THEN
+            v_curtailment_power := ROUND(v_actual_power * (0.1::NUMERIC + RANDOM()::NUMERIC * 0.2::NUMERIC), 2);
+            v_actual_power := v_actual_power - v_curtailment_power;
+            v_device_status := 2; -- 限电
+        ELSE
+            v_curtailment_power := 0;
+            v_device_status := 0; -- 正常
+        END IF;
+
+        -- 夜间停机
+        IF v_theoretical_power = 0 THEN
+            v_device_status := 1;
+        END IF;
+
+        -- 3. 并网状态
+        IF v_actual_power > 0 OR (v_hour_of_day BETWEEN 6 AND 19) THEN
+            v_grid_status := 1;
+        ELSE
+            v_grid_status := 0;
+        END IF;
+
+        -- 4. 可用率
+        v_availability := 100.0;
+        IF v_device_status = 1 THEN v_availability := 0; END IF;
+        IF v_device_status = 2 THEN v_availability := 90; END IF;
+
+        -- 5. 插入数据
+        INSERT INTO public.data_electricity_local (
+            tm,
+            config_station_id,
+            actual_power,
+            theoretical_power,
+            curtailment_power,
+            availability,
+            device_status,
+            grid_status
+        ) VALUES (
+            v_current_time,
+            v_station_id,
+            v_actual_power,
+            v_theoretical_power,
+            v_curtailment_power,
+            v_availability,
+            v_device_status,
+            v_grid_status
+        );
+
+        -- 移动到下一个时间点
+        v_current_time := v_current_time + INTERVAL '1 minute' * v_interval_minutes;
+    END LOOP;
+
+    RAISE NOTICE '光伏实测数据生成完成!范围: % 到 %', v_start_timestamp, v_end_timestamp;
+
+END $$;

+ 17 - 7
doc/数据模拟脚本/超短期功率预测/mock_data_ultra_short_term_power_forecasting.sql

@@ -1,9 +1,9 @@
 -- ============================================================
--- 脚本功能:独立生成模拟的超短期功率预测数据
+-- 脚本功能:独立生成模拟的超短期功率预测数据(带随机波动)
 -- 说明:
---   1. 不依赖任何源表,直接生成数据。
---   2. 使用正弦函数模拟风电出力曲线,数据更合理。
---   3. 可配置生成过去30天到未来10天的数据。
+--   1. 增加随机波动系数(±15%),模拟真实环境变化。
+--   2. 修复 ROUND 函数的类型转换问题。
+--   3. 保持正弦曲线的基础趋势。
 -- ============================================================
 DO $$
 DECLARE
@@ -13,6 +13,7 @@ DECLARE
     v_interval_minutes INTEGER := 15;              -- 数据间隔(分钟)
     v_forecast_hours   INTEGER := 4;               -- 预测时长(小时)
     v_max_power        NUMERIC := 100.0;           -- 模拟的最大功率(装机容量)
+    v_fluctuation      NUMERIC := 0.15;            -- 随机波动幅度 (0.15 = ±15%)
 
     -- 模拟数据配置
     v_station_id       BIGINT := 2;                -- 场站ID
@@ -72,17 +73,26 @@ BEGIN
             v_current_forecast := v_current_start + INTERVAL '1 minute' * (i * v_interval_minutes);
             v_forecast_time := EXTRACT(EPOCH FROM v_current_forecast)::INTEGER;
             
-            -- 使用正弦函数模拟风电出力曲线
-            -- 曲线从0开始,在2小时(120分钟)达到峰值,4小时(240分钟)回到0
+            -- 使用正弦函数模拟风电/光伏出力曲线
             DECLARE
                 v_diff_minutes INTEGER;
                 v_simulated_power NUMERIC;
+                v_random_factor NUMERIC;
             BEGIN
                 v_diff_minutes := i * v_interval_minutes;
                 
                 IF v_diff_minutes BETWEEN 0 AND 240 THEN
+                    -- 1. 基础正弦曲线计算 (0 -> 1 -> 0)
                     v_simulated_power := v_max_power * SIN(PI() * v_diff_minutes / 240.0);
-                    v_simulated_power := ROUND(v_simulated_power, 2);
+                    
+                    -- 2. 增加随机波动 (0.85 ~ 1.15)
+                    v_random_factor := (1.0 - v_fluctuation) + (RANDOM() * (v_fluctuation * 2));
+                    v_simulated_power := v_simulated_power * v_random_factor;
+                    
+                    -- 3. 确保功率不为负数,并保留两位小数
+                    -- 注意:这里显式转换为 NUMERIC 以兼容 ROUND 函数
+                    v_simulated_power := ROUND(v_simulated_power::NUMERIC, 2);
+                    IF v_simulated_power < 0 THEN v_simulated_power := 0; END IF;
                 ELSE
                     v_simulated_power := 0;
                 END IF;

+ 30 - 0
forecast-backend-server/src/main/java/com/ufo/base/common/utils/TimeUtils.java

@@ -3,8 +3,10 @@ package com.ufo.base.common.utils;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import lombok.extern.slf4j.Slf4j;
 
 /**
  * 时区转换工具类
@@ -12,6 +14,7 @@ import java.time.format.DateTimeFormatter;
  *
  * @author ufo
  */
+@Slf4j
 public class TimeUtils
 {
     /** GMT+8 zone */
@@ -49,4 +52,31 @@ public class TimeUtils
         LocalDateTime startLdt = LocalDateTime.parse(timeStr, FORMATTER);
         return startLdt.atZone(GMT8).toEpochSecond();
     }
+
+    /**
+     * 将 yyyy-MM-dd 转换为 ISO 8601 格式(北京时间)
+     */
+    /** 北京时间偏移 */
+    private static final ZoneOffset ZONE_OFFSET = ZoneOffset.ofHours(8);
+    public static String formatOffsetDateTime(String dateStr)
+    {
+        return formatOffsetDateTime(dateStr, null);
+    }
+    public static String formatOffsetDateTime(String dateStr, Integer plusHours)
+    {
+        try
+        {
+            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            LocalDateTime ldt = LocalDateTime.parse(dateStr, fmt);
+            if (null != plusHours) {
+                ldt = ldt.plusHours(plusHours);
+            }
+            LocalDateTime beijing = ldt.atZone(ZoneId.of("Asia/Shanghai")).withZoneSameInstant(ZONE_OFFSET).toLocalDateTime();
+            return beijing.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        }
+        catch (Exception e)
+        {
+            return dateStr;
+        }
+    }
 }

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

@@ -6,31 +6,38 @@ import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 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.util.CollectionUtils;
 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.github.pagehelper.PageHelper;
 import com.ufo.base.common.utils.TimeUtils;
 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.UltraShortTermPowerForecastData;
-import com.ufo.project.forecast.shortterm.power.domain.UltraShortTermPowerSingleExport;
+import com.ufo.project.forecast.shortterm.power.domain.UltraShortTermPowerForecastOnceData;
 import com.ufo.project.forecast.supershort.domain.DataUltraShortTermPowerForecasting;
 import com.ufo.project.forecast.supershort.service.IDataUltraShortTermPowerForecastingService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
 
 /**
  * 超短期功率预测Controller
@@ -48,6 +55,24 @@ public class UltraShortTermPowerForecastingController extends BaseController
 
     @Autowired
     private IDataUltraShortTermPowerForecastingService dataUltraShortTermPowerForecastingService;
+    @Autowired 
+    private IDataElectricityLocalService dataElectricityLocalService;
+
+    @PreAuthorize("@ss.hasPermi('electricity:forecast:ultrashorttermpower')")
+    @GetMapping("/report/scopes")
+    public AjaxResult scopes(@RequestParam("stationId") Long stationId,
+            @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime, @RequestParam(value = "type", defaultValue = "0") Integer type)
+    {
+        long startUnix = TimeUtils.gmt8Time2Utc(startTime);
+        long endUnix   = TimeUtils.gmt8Time2Utc(endTime);
+        List<DataUltraShortTermPowerForecasting> dataList = dataUltraShortTermPowerForecastingService
+                    .selectByStartTimeRange(stationId.toString(), startUnix, endUnix);
+        List<String> scopes = dataList.stream().map(r -> {
+            String scope = TimeUtils.utcToGmt8Format(r.getStartTime());
+            return scope;
+        }).collect(Collectors.toList());
+        return success(scopes);
+    }
 
     /**
      * 获取超短期功率预测数据(预测 + 实发功率合并)
@@ -55,13 +80,18 @@ public class UltraShortTermPowerForecastingController extends BaseController
      * @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')")
     @GetMapping("/report")
     public AjaxResult report(@RequestParam("stationId") Long stationId,
-            @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime)
+            @RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime, @RequestParam(value = "type", defaultValue = "0") Integer type)
     {
-        return success(queryForecastData(stationId, startTime, endTime));
+        if(type == 0){
+            return success(queryForecastData(stationId, startTime, endTime));
+        }else{
+            return success(queryForecastOnceData(stationId, startTime, endTime));
+        }
     }
 
     /**
@@ -80,33 +110,76 @@ public class UltraShortTermPowerForecastingController extends BaseController
             @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
-        {
+        if(type == 0){
+            List<UltraShortTermPowerForecastData> data = queryForecastData(stationId, startTime, endTime);
             EasyExcel.write(response.getOutputStream(), UltraShortTermPowerForecastData.class)
                     .sheet("超短期功率预测").doWrite(data);
+        }else{
+            List<UltraShortTermPowerForecastOnceData> data = queryForecastOnceData(stationId, startTime, endTime);
+            EasyExcel.write(response.getOutputStream(), UltraShortTermPowerForecastOnceData.class)
+                    .sheet("超短期功率预测").doWrite(data);
         }
     }
 
+    private List<UltraShortTermPowerForecastOnceData> queryForecastOnceData(Long stationId, String startTimeStr, String endTimeStr){
+        List<UltraShortTermPowerForecastOnceData> result = new ArrayList<>();
+        long startUnix = TimeUtils.gmt8Time2Utc(startTimeStr);
+        long endUnix   = TimeUtils.gmt8Time2Utc(endTimeStr);
+        PageHelper.startPage(1, 1);
+        List<DataUltraShortTermPowerForecasting> dataList = dataUltraShortTermPowerForecastingService
+                    .selectByStartTimeRange(stationId.toString(), startUnix, endUnix);
+        if(CollectionUtils.isEmpty(dataList)){
+            return result;
+        }
+        DataUltraShortTermPowerForecasting data = dataList.get(0);
+        String startTimeTimeZoneStr = TimeUtils.formatOffsetDateTime(startTimeStr);
+        String endTimeTimeZoneStr = TimeUtils.formatOffsetDateTime(endTimeStr, 4);
+        List<DataElectricityLocal> realDataList = dataElectricityLocalService.selectActualPowerList(stationId, startTimeTimeZoneStr, endTimeTimeZoneStr);
+
+        Map<Long, DataElectricityLocal> realDataMap = realDataList.stream()
+                    .collect(Collectors.toMap(
+                            (DataElectricityLocal r) -> r.getTm().getTime() / 1000,
+                            Function.identity()));
+        
+        List<Function<DataUltraShortTermPowerForecasting, String>> timePowerConsumers = Arrays.asList(
+            DataUltraShortTermPowerForecasting::getPower , DataUltraShortTermPowerForecasting::getPower15,
+            DataUltraShortTermPowerForecasting::getPower30, DataUltraShortTermPowerForecasting::getPower45,
+            DataUltraShortTermPowerForecasting::getPower60, DataUltraShortTermPowerForecasting::getPower75,
+            DataUltraShortTermPowerForecasting::getPower90, DataUltraShortTermPowerForecasting::getPower105,
+            DataUltraShortTermPowerForecasting::getPower120, DataUltraShortTermPowerForecasting::getPower135,
+            DataUltraShortTermPowerForecasting::getPower150, DataUltraShortTermPowerForecasting::getPower165,
+            DataUltraShortTermPowerForecasting::getPower180, DataUltraShortTermPowerForecasting::getPower195,
+            DataUltraShortTermPowerForecasting::getPower210, DataUltraShortTermPowerForecasting::getPower225,
+            DataUltraShortTermPowerForecasting::getPower240
+        );
+        int step = 15;
+        for(int i = 0; i < 16; i++){
+            UltraShortTermPowerForecastOnceData onceData = 
+                toOnceData(data, (long) i * step, realDataMap, timePowerConsumers.get(i));
+            result.add(onceData);
+        }
+        return result;
+    }
+
+    private UltraShortTermPowerForecastOnceData toOnceData(DataUltraShortTermPowerForecasting data, Long plusMinutes, Map<Long, DataElectricityLocal> realDataMap, 
+        Function<DataUltraShortTermPowerForecasting, String> timePowerConsumer)
+    {
+        Long time = data.getForecastTime() + plusMinutes * 60;
+        DataElectricityLocal realData = realDataMap.get(time);
+        String powerStr = timePowerConsumer.apply(data);
+        double actualPower = realData != null ? realData.getActualPower() : 0D;
+        UltraShortTermPowerForecastOnceData onceData = new UltraShortTermPowerForecastOnceData().setTime(TimeUtils.utcToGmt8Format(time))
+        .setPower(Double.parseDouble(powerStr))
+        .setMonitoredPower(actualPower)
+        .setBias(actualPower - Double.parseDouble(powerStr))
+        ;
+        return onceData;
+    }
+
     /**
      * 查询超短期功率预测数据,从 data_ultra_short_term_power_forecasting 表获取
      */
@@ -119,23 +192,36 @@ public class UltraShortTermPowerForecastingController extends BaseController
         long endUnix   = TimeUtils.gmt8Time2Utc(endTimeStr);
 
             List<DataUltraShortTermPowerForecasting> dataList = dataUltraShortTermPowerForecastingService
-                    .selectByTimeRange(stationId.toString(), startUnix, endUnix);
+                    .selectByStartTimeRange(stationId.toString(), startUnix, endUnix);
             if (dataList == null || dataList.isEmpty())
             {
                 return result;
             }
 
-            // 按 forecast_time 分组,同一时间点取平均值
+            String startTimeTimeZoneStr = TimeUtils.formatOffsetDateTime(startTimeStr);
+            String endTimeTimeZoneStr = TimeUtils.formatOffsetDateTime(endTimeStr);
+            List<DataElectricityLocal> realDataList = dataElectricityLocalService.selectActualPowerList(stationId, startTimeTimeZoneStr, endTimeTimeZoneStr);
+
+            // 按 start_time 分组,同一时间点取平均值
             Map<Long, List<DataUltraShortTermPowerForecasting>> groupedByTime = dataList.stream()
-                    .filter(data -> data.getForecastTime() != null)
+                    .filter(data -> data.getStartTime() != null)
                     .collect(Collectors.groupingBy(
-                            DataUltraShortTermPowerForecasting::getForecastTime,
+                            DataUltraShortTermPowerForecasting::getStartTime,
                             LinkedHashMap::new,
                             Collectors.toList()));
 
+            Map<Long, DataElectricityLocal> realDataMap = realDataList.stream()
+                    .collect(Collectors.toMap(
+                            (DataElectricityLocal r) -> r.getTm().getTime() / 1000,
+                            Function.identity()));
+
             for (Map.Entry<Long, List<DataUltraShortTermPowerForecasting>> entry : groupedByTime.entrySet())
             {
-                result.add(toData(entry.getKey(), entry.getValue()));
+                UltraShortTermPowerForecastData data = toData(entry.getKey(), entry.getValue());
+                if(realDataMap.containsKey(entry.getKey())){
+                    data.setMonitoredPower(realDataMap.get(entry.getKey()).getActualPower());
+                }
+                result.add(data);
             }
         }
         catch (Exception e)
@@ -146,11 +232,11 @@ public class UltraShortTermPowerForecastingController extends BaseController
         return result;
     }
 
-    private UltraShortTermPowerForecastData toData(Long forecastTime, List<DataUltraShortTermPowerForecasting> items)
+    private UltraShortTermPowerForecastData toData(Long time, List<DataUltraShortTermPowerForecasting> items)
     {
         UltraShortTermPowerForecastData result = new UltraShortTermPowerForecastData();
         // forecast_time 为 UTC unix 秒,展示时格式化为 GMT+8 时间
-        result.setTime(TimeUtils.utcToGmt8Format(forecastTime));
+        result.setTime(TimeUtils.utcToGmt8Format(time));
         result.setMonitoredPower(avgPower(items, DataUltraShortTermPowerForecasting::getPower));
         result.setPower15(avgPower(items, DataUltraShortTermPowerForecasting::getPower15));
         result.setPower30(avgPower(items, DataUltraShortTermPowerForecasting::getPower30));

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

@@ -11,12 +11,12 @@ import com.alibaba.excel.annotation.ExcelProperty;
 public class UltraShortTermPowerForecastData
 {
     /** 时间(yyyy-MM-dd HH:mm) */
-    @ExcelProperty("日期")
+    @ExcelProperty("时间")
     private String time;
 
-    /** 实测功率/预报发电功率(kw) */
+    /** 实发功率(kw) */
     @ExcelProperty("实测功率")
-    private Double power = 0D;
+    private Double monitoredPower = 0D;
 
     @ExcelProperty("15分钟")
     private Double power15 = 0D;
@@ -66,9 +66,9 @@ public class UltraShortTermPowerForecastData
     @ExcelProperty("240分钟")
     private Double power240 = 0D;
 
-    /** 实发功率(kw) */
+    /** 预报功率 */
     @ExcelIgnore
-    private Double monitoredPower = 0D;
+    private Double power = 0D;
 
     /** 偏差 = monitoredPower - power */
     @ExcelIgnore

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

@@ -0,0 +1,23 @@
+package com.ufo.project.forecast.shortterm.power.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors (chain = true)
+public class UltraShortTermPowerForecastOnceData {
+
+    @ExcelProperty("时间")
+    private String time;
+
+    @ExcelProperty("实测功率")
+    private Double monitoredPower = 0D;
+
+    @ExcelProperty("预测功率")
+    private Double power = 0D;
+
+    @ExcelProperty("偏差")
+    private Double bias = 0D;
+}

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

@@ -1,67 +0,0 @@
-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;
-    }
-}

+ 11 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/mapper/DataUltraShortTermPowerForecastingMapper.java

@@ -70,4 +70,15 @@ public interface DataUltraShortTermPowerForecastingMapper
      */
     public List<DataUltraShortTermPowerForecasting> selectByTimeRange(@Param("entityId") String entityId,
             @Param("startTime") Long startTime, @Param("endTime") Long endTime);
+
+    /**
+     * 查询场站指定起报时间范围内的预测数据
+     *
+     * @param entityId 实体ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 预测数据集合
+     */
+    public List<DataUltraShortTermPowerForecasting> selectByStartTimeRange(@Param("entityId") String entityId,
+            @Param("startTime") Long startTime, @Param("endTime") Long endTime);
 }

+ 9 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/service/IDataUltraShortTermPowerForecastingService.java

@@ -68,4 +68,13 @@ public interface IDataUltraShortTermPowerForecastingService
      * @return 预测数据集合
      */
     public List<DataUltraShortTermPowerForecasting> selectByTimeRange(String entityId, Long startTime, Long endTime);
+    /**
+     * 查询场站指定时间范围内的预测数据
+     *
+     * @param entityId 实体ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 预测数据集合
+     */
+    public List<DataUltraShortTermPowerForecasting> selectByStartTimeRange(String entityId, Long startTime, Long endTime);
 }

+ 5 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/service/impl/DataUltraShortTermPowerForecastingServiceImpl.java

@@ -106,4 +106,9 @@ public class DataUltraShortTermPowerForecastingServiceImpl implements IDataUltra
     {
         return dataUltraShortTermPowerForecastingMapper.selectByTimeRange(entityId, startTime, endTime);
     }
+
+    @Override
+    public List<DataUltraShortTermPowerForecasting> selectByStartTimeRange(String entityId, Long startTime, Long endTime){
+        return dataUltraShortTermPowerForecastingMapper.selectByStartTimeRange(entityId, startTime, endTime);
+    }
 }

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

@@ -78,6 +78,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by forecast_time asc
     </select>
 
+    <select id="selectByStartTimeRange" resultMap="DataUltraShortTermPowerForecastingResult">
+        <include refid="selectDataUltraShortTermPowerForecastingVo"/>
+        where entity_id = #{entityId} and start_time between #{startTime} and #{endTime}
+        order by start_time asc
+    </select>
+
     <insert id="insertDataUltraShortTermPowerForecasting" parameterType="DataUltraShortTermPowerForecasting" useGeneratedKeys="true" keyProperty="Id">
         insert into data_ultra_short_term_power_forecasting
         <trim prefix="(" suffix=")" suffixOverrides=",">

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

@@ -9,6 +9,15 @@ export function reportUltraShortTermPowerForecasting(query) {
   })
 }
 
+// 查询超短期功率预测时间点列表
+export function reportUltraShortTermPowerForecastingScopes(query) {
+  return request({
+    url: '/power/ultraShortTermPowerForecasting/report/scopes',
+    method: 'get',
+    params: query
+  })
+}
+
 // 导出超短期功率预测数据
 export function exportUltraShortTermPowerForecasting(data) {
   return request({

+ 71 - 27
forecast-backend-vue/src/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue

@@ -32,7 +32,7 @@
       </a-row>
     </a-card>
 
-    <a-tabs v-model:activeKey="query.type" class="content-tabs">
+    <a-tabs v-model:activeKey="query.type" class="content-tabs" @change="handleQuery">
       <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" />
@@ -45,12 +45,12 @@
         <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" />
+            <a-select v-model:value="query.selectedTimePoint" :options="timePointOptions" placeholder="请选择预测时间点" class="time-point-select" @change="fetchSingleData" />
           </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}" />
+          <BusinessDataTable :columns="singleColumns" :data-source="singleRows" :pagination="{pageSize: 10}" />
         </a-card>
       </a-tab-pane>
     </a-tabs>
@@ -64,7 +64,11 @@ 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 {
+  exportUltraShortTermPowerForecasting,
+  reportUltraShortTermPowerForecasting,
+  reportUltraShortTermPowerForecastingScopes
+} from '@/api/power/ultraShortTermPowerForecasting';
 import BusinessDataTable from '../components/BusinessDataTable.vue';
 import PowerLineChart from '../components/PowerLineChart.vue';
 import StationSelector from '../components/StationSelector.vue';
@@ -75,6 +79,8 @@ const loading = ref(false);
 const exporting = ref(false);
 const stationOptions = ref([]);
 const tableRows = ref([]);
+const singleRows = ref([]);
+const timeScopeList = ref([]);
 
 const query = reactive({
   stationId: undefined,
@@ -92,13 +98,13 @@ const metricColors = [
 ];
 
 const metricOptions = [
-  {label: '实测功率', value: 'power'},
+  {label: '实测功率', value: 'monitoredPower'},
   ...horizonMinutes.map((m) => ({label: `${m}分钟`, value: `power${m}`}))
 ];
 
 const pointColumns = [
   {title: '日期', dataIndex: 'time'},
-  {title: '实测功率', dataIndex: 'power'},
+  {title: '实测功率', dataIndex: 'monitoredPower'},
   ...horizonMinutes.map((m) => ({title: `${m}分钟`, dataIndex: `power${m}`}))
 ];
 
@@ -121,14 +127,7 @@ const queryValid = computed(() => {
   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 defaultActiveMetrics = ['monitoredPower', 'power240'];
 
 const pointChartAxis = computed(() => tableRows.value.map((item) => item.time));
 const pointChartSeries = computed(() => {
@@ -148,16 +147,14 @@ const legendSelected = computed(() => {
   return map;
 });
 
-const singleChartAxis = computed(() => filteredRows.value.map((item) => item.time));
-const singleChartSeries = computed(() => buildLineFromRows(filteredRows.value, [
+const singleChartAxis = computed(() => singleRows.value.map((item) => item.time));
+const singleChartSeries = computed(() => buildLineFromRows(singleRows.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 timePointOptions = computed(() => timeScopeList.value.map((time) => ({label: time, value: time})));
 
 const loadStationOptions = async () => {
   const res = await listStation({pageNum: 1, pageSize: 500});
@@ -171,7 +168,15 @@ const loadStationOptions = async () => {
 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')
+  endTime: dayjs(query.endTime).format('YYYY-MM-DD HH:mm:ss'),
+  type: query.type
+});
+
+const buildSingleParams = () => ({
+  stationId: query.stationId,
+  startTime: dayjs(query.selectedTimePoint).format('YYYY-MM-DD HH:mm:ss'),
+  endTime: dayjs(query.selectedTimePoint).add(1, 'second').format('YYYY-MM-DD HH:mm:ss'),
+  type: '1'
 });
 
 const handleQuery = async () => {
@@ -182,11 +187,47 @@ const handleQuery = async () => {
   if (!queryValid.value) {
     return;
   }
+  if (query.type === '1') {
+    await handleSingleQuery();
+    return;
+  }
   loading.value = true;
   try {
     const res = await reportUltraShortTermPowerForecasting(buildParams());
     tableRows.value = normalizeRows(res);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 单次预测查询: 先取预测时间点列表, 默认选中第一个并获取该时间点预测数据
+const handleSingleQuery = async () => {
+  loading.value = true;
+  try {
+    const res = await reportUltraShortTermPowerForecastingScopes(buildParams());
+    timeScopeList.value = Array.from(new Set(normalizeRows(res)));
+  } finally {
+    loading.value = false;
+  }
+  if (!timeScopeList.value.length) {
+    message.warning('所选时间范围内没有预测数据');
     query.selectedTimePoint = undefined;
+    singleRows.value = [];
+    return;
+  }
+  query.selectedTimePoint = timeScopeList.value[0];
+  await fetchSingleData();
+};
+
+const fetchSingleData = async () => {
+  if (!query.stationId || !query.selectedTimePoint) {
+    singleRows.value = [];
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await reportUltraShortTermPowerForecasting(buildSingleParams());
+    singleRows.value = normalizeRows(res);
   } finally {
     loading.value = false;
   }
@@ -197,19 +238,22 @@ const handleExport = async () => {
     message.warning('请先选择场站');
     return;
   }
-  if (!queryValid.value) {
+  const single = query.type === '1';
+  if (single && !query.selectedTimePoint) {
+    message.warning('请先选择预测时间点');
+    return;
+  }
+  if (!single && !queryValid.value) {
     return;
   }
+  const params = single ? buildSingleParams() : buildParams();
   exporting.value = true;
   try {
-    const res = await exportUltraShortTermPowerForecasting({
-      ...buildParams(),
-      type: query.type
-    });
+    const res = await exportUltraShortTermPowerForecasting(params);
     const isFile = await blobValidate(res);
     if (isFile) {
-      const start = dayjs(query.startTime).format('YYYYMMDDHHmm');
-      const end = dayjs(query.endTime).format('YYYYMMDDHHmm');
+      const start = dayjs(params.startTime).format('YYYYMMDDHHmm');
+      const end = dayjs(params.endTime).format('YYYYMMDDHHmm');
       saveAs(new Blob([res]), `超短期预测-${start}-${end}.xlsx`);
       message.success('导出成功');
     } else {