Răsfoiți Sursa

[1021] 功率预测修正功能完善:修正页3曲线(上报/修正/观测)、新增中期修正菜单/页面、系数调整与单条修正、修正表名笔误

taosj 2 săptămâni în urmă
părinte
comite
b319e03404
23 a modificat fișierele cu 1627 adăugiri și 139 ștergeri
  1. 222 23
      forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/controller/DataMediumTermPowerForecastCorrectController.java
  2. 23 1
      forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/mapper/DataMediumTermPowerForecastCorrectMapper.java
  3. 11 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/mapper/DataMediumTermPowerForecastReportMapper.java
  4. 222 23
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/DataShortTermPowerForecastCorrectController.java
  5. 10 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/mapper/DataShortTermPowerForecastCorrectMapper.java
  6. 11 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/mapper/DataShortTermPowerForecastReportMapper.java
  7. 221 23
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/controller/DataSuperShortTermForecastCorrectController.java
  8. 10 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/mapper/DataSuperShortTermForecastCorrectMapper.java
  9. 11 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/mapper/DataSuperShortTermForecastReportMapper.java
  10. 22 3
      forecast-backend-server/src/main/resources/mybatis/mediumpower/DataMediumTermPowerForecastCorrectMapper.xml
  11. 11 0
      forecast-backend-server/src/main/resources/mybatis/mediumpower/DataMediumTermPowerForecastReportMapper.xml
  12. 11 3
      forecast-backend-server/src/main/resources/mybatis/power/DataShortTermPowerForecastCorrectMapper.xml
  13. 11 0
      forecast-backend-server/src/main/resources/mybatis/power/DataShortTermPowerForecastReportMapper.xml
  14. 11 3
      forecast-backend-server/src/main/resources/mybatis/supershort/DataSuperShortTermForecastCorrectMapper.xml
  15. 11 0
      forecast-backend-server/src/main/resources/mybatis/supershort/DataSuperShortTermForecastReportMapper.xml
  16. 81 0
      forecast-backend-vue/src/api/power/dataMediumTermPowerForecastCorrect.js
  17. 19 1
      forecast-backend-vue/src/api/power/dataShortTermPowerForecastCorrect.js
  18. 19 1
      forecast-backend-vue/src/api/power/dataSuperShortTermForecastCorrect.js
  19. 321 0
      forecast-backend-vue/src/views/ufo/electricity/forecast/DataMediumTermPowerForecastCorrect.vue
  20. 172 29
      forecast-backend-vue/src/views/ufo/electricity/forecast/DataShortTermPowerForecastCorrect.vue
  21. 172 29
      forecast-backend-vue/src/views/ufo/electricity/forecast/DataSuperShortTermForecastCorrect.vue
  22. 13 0
      release_note/20261021/20261021_dp.sql
  23. 12 0
      release_note/20261021/release_note.md

+ 222 - 23
forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/controller/DataMediumTermPowerForecastCorrectController.java

@@ -1,10 +1,13 @@
 package com.ufo.project.forecast.mediumterm.power.controller;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
+import java.util.TreeSet;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -15,11 +18,18 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+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.project.forecast.mediumterm.power.domain.DataMediumTermPowerForecastCorrect;
+import com.ufo.project.forecast.mediumterm.power.domain.DataMediumTermPowerForecastReport;
+import com.ufo.project.forecast.mediumterm.power.mapper.DataMediumTermPowerForecastCorrectMapper;
+import com.ufo.project.forecast.mediumterm.power.mapper.DataMediumTermPowerForecastReportMapper;
 import com.ufo.project.forecast.mediumterm.power.service.IDataMediumTermPowerForecastCorrectService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.common.utils.poi.ExcelUtil;
@@ -27,7 +37,7 @@ import com.ufo.base.framework.web.page.TableDataInfo;
 
 /**
  * 中期功率修正数据Controller
- * 
+ *
  * @author ufo
  * @date 2026-05-05
  */
@@ -38,6 +48,15 @@ public class DataMediumTermPowerForecastCorrectController extends BaseController
     @Autowired
     private IDataMediumTermPowerForecastCorrectService dataMediumTermPowerForecastCorrectService;
 
+    @Autowired
+    private DataMediumTermPowerForecastReportMapper dataMediumTermPowerForecastReportMapper;
+
+    @Autowired
+    private DataMediumTermPowerForecastCorrectMapper dataMediumTermPowerForecastCorrectMapper;
+
+    @Autowired
+    private IDataElectricityLocalService dataElectricityLocalService;
+
     /**
      * 查询中期功率修正数据列表
      */
@@ -50,12 +69,67 @@ public class DataMediumTermPowerForecastCorrectController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询中期功率预测图表数据(上报/修正/观测三条曲线, 时间点取三者并集, 缺失补0)
+     */
     @PreAuthorize("@ss.hasPermi('mediumterm:powerForecastCorrect:list')")
     @GetMapping("/chart")
-    public TableDataInfo chart(DataMediumTermPowerForecastCorrect dataMediumTermPowerForecastCorrect)
+    public AjaxResult chart(@RequestParam("stationId") Long stationId,
+                            @RequestParam("forecastStartTime") Long forecastStartTime,
+                            @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataMediumTermPowerForecastCorrect> list = this.queryList(dataMediumTermPowerForecastCorrect);
-        return getDataTable(list);
+        String entityId = String.valueOf(stationId);
+
+        List<DataMediumTermPowerForecastReport> reportList = dataMediumTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> reportPowerMap = new HashMap<>();
+        if (reportList != null)
+        {
+            for (DataMediumTermPowerForecastReport item : reportList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    reportPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        List<DataMediumTermPowerForecastCorrect> correctList = dataMediumTermPowerForecastCorrectMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> correctPowerMap = new HashMap<>();
+        if (correctList != null)
+        {
+            for (DataMediumTermPowerForecastCorrect item : correctList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    correctPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        Map<Long, Double> monitorPowerMap = new HashMap<>();
+        List<DataElectricityLocal> localList = dataElectricityLocalService.selectActualPowerList(
+                stationId, TimeUtils.utcToGmt8Format(forecastStartTime), TimeUtils.utcToGmt8Format(forecastEndTime));
+        if (localList != null)
+        {
+            for (DataElectricityLocal item : localList)
+            {
+                if (item.getTm() != null)
+                {
+                    monitorPowerMap.put(item.getTm().getTime() / 1000, item.getActualPower() == null ? 0.0 : item.getActualPower());
+                }
+            }
+        }
+
+        TreeSet<Long> times = new TreeSet<>();
+        times.addAll(reportPowerMap.keySet());
+        times.addAll(correctPowerMap.keySet());
+        times.addAll(monitorPowerMap.keySet());
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("report", buildSeries(times, reportPowerMap));
+        body.put("correct", buildSeries(times, correctPowerMap));
+        body.put("monitor", buildSeries(times, monitorPowerMap));
+        return AjaxResult.success(body);
     }
 
     /**
@@ -64,36 +138,161 @@ public class DataMediumTermPowerForecastCorrectController extends BaseController
     @PreAuthorize("@ss.hasPermi('mediumterm:powerForecastCorrect:export')")
     @Log(title = "中期功率修正数据", businessType = BusinessType.EXPORT)
     @PostMapping("/export/excel")
-    public void export(HttpServletResponse response, DataMediumTermPowerForecastCorrect dataMediumTermPowerForecastCorrect)
+    public void export(HttpServletResponse response, @RequestParam("stationId") Long stationId,
+                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                       @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataMediumTermPowerForecastCorrect> list = this.queryList(dataMediumTermPowerForecastCorrect);
+        List<DataMediumTermPowerForecastCorrect> list = dataMediumTermPowerForecastCorrectMapper.selectLatestByWindow(
+                String.valueOf(stationId), forecastStartTime, forecastEndTime);
         ExcelUtil<DataMediumTermPowerForecastCorrect> util = new ExcelUtil<DataMediumTermPowerForecastCorrect>(DataMediumTermPowerForecastCorrect.class);
         util.exportExcel(response, list, "中期功率修正数据数据");
     }
 
-    private List<DataMediumTermPowerForecastCorrect> queryList(DataMediumTermPowerForecastCorrect dataMediumTermPowerForecastCorrect)
+    /**
+     * 系数调整: 以窗口内上报数据为基准, 按系数生成/更新修正数据
+     */
+    @PreAuthorize("@ss.hasPermi('mediumterm:powerForecastCorrect:edit')")
+    @Log(title = "中期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/multiplierUpdate")
+    public AjaxResult multiplierUpdate(@RequestParam("stationId") Long stationId,
+                                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                                       @RequestParam("forecastEndTime") Long forecastEndTime,
+                                       @RequestParam("multiplier") Double multiplier)
     {
-        List<DataMediumTermPowerForecastCorrect> list = dataMediumTermPowerForecastCorrectService.selectDataMediumTermPowerForecastCorrectList(dataMediumTermPowerForecastCorrect);
-        if (list == null || list.isEmpty())
+        if (multiplier == null || multiplier < 0.01 || multiplier > 100.00)
         {
-            return list;
+            return AjaxResult.error("调整系数必须在0.01-100.00之间");
         }
-
-        // 按 forecast_time 分组,每组按 start_time desc, id desc 取第一条(即同一时间点最新起报的数据)
-        Comparator<DataMediumTermPowerForecastCorrect> latestFirst = Comparator
-                .comparing(DataMediumTermPowerForecastCorrect::getStartTime, Comparator.nullsFirst(Comparator.<Long>naturalOrder()))
-                .thenComparing(DataMediumTermPowerForecastCorrect::getId, Comparator.nullsFirst(Comparator.<Long>naturalOrder()));
-        Map<Long, DataMediumTermPowerForecastCorrect> latestByForecastTime = new TreeMap<>();
-        for (DataMediumTermPowerForecastCorrect item : list)
+        String entityId = String.valueOf(stationId);
+        List<DataMediumTermPowerForecastReport> reportList = dataMediumTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        if (reportList == null || reportList.isEmpty())
         {
-            if (item.getForecastTime() == null)
+            return AjaxResult.error("窗口内无上报数据,无法调整");
+        }
+        BigDecimal factor = BigDecimal.valueOf(multiplier);
+        for (DataMediumTermPowerForecastReport report : reportList)
+        {
+            if (report.getForecastTime() == null)
             {
                 continue;
             }
-            latestByForecastTime.merge(item.getForecastTime(), item,
-                    (existing, candidate) -> latestFirst.compare(candidate, existing) > 0 ? candidate : existing);
+            String newPower = multiplyPower(report.getPower(), factor);
+            DataMediumTermPowerForecastCorrect existing = dataMediumTermPowerForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, report.getForecastTime());
+            if (existing != null)
+            {
+                long newStartTime = report.getStartTime() != null
+                        && (existing.getStartTime() == null || existing.getStartTime() < report.getStartTime())
+                        ? report.getStartTime() : existing.getStartTime();
+                existing.setPower(newPower);
+                existing.setStartTime(newStartTime);
+                dataMediumTermPowerForecastCorrectMapper.updateDataMediumTermPowerForecastCorrect(existing);
+            }
+            else
+            {
+                DataMediumTermPowerForecastCorrect row = new DataMediumTermPowerForecastCorrect();
+                row.setDataSourcegrabId(report.getDataSourcegrabId());
+                row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+                row.setEntityId(report.getEntityId());
+                row.setStationIds(report.getStationIds());
+                row.setEntityType(report.getEntityType());
+                row.setEntityName(report.getEntityName());
+                row.setPredays(report.getPredays());
+                row.setStartTime(report.getStartTime());
+                row.setForecastTime(report.getForecastTime());
+                row.setPower(newPower);
+                dataMediumTermPowerForecastCorrectMapper.insertDataMediumTermPowerForecastCorrect(row);
+            }
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * 单点修正: 更新指定预测时刻的修正功率(无修正数据时以上报数据为模板新增)
+     */
+    @PreAuthorize("@ss.hasPermi('mediumterm:powerForecastCorrect:edit')")
+    @Log(title = "中期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/update")
+    public AjaxResult update(@RequestBody DataMediumTermPowerForecastCorrect dataMediumTermPowerForecastCorrect)
+    {
+        String entityId = dataMediumTermPowerForecastCorrect.getEntityId();
+        Long forecastTime = dataMediumTermPowerForecastCorrect.getForecastTime();
+        String power = dataMediumTermPowerForecastCorrect.getPower();
+        if (entityId == null || entityId.isEmpty() || forecastTime == null || power == null || power.trim().isEmpty())
+        {
+            return AjaxResult.error("参数不完整");
+        }
+        try
+        {
+            Double.parseDouble(power.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return AjaxResult.error("修正功率必须为数字");
+        }
+
+        DataMediumTermPowerForecastCorrect existing = dataMediumTermPowerForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, forecastTime);
+        if (existing != null)
+        {
+            existing.setPower(power.trim());
+            dataMediumTermPowerForecastCorrectMapper.updateDataMediumTermPowerForecastCorrect(existing);
+            return AjaxResult.success();
+        }
+
+        // 无修正数据时, 取该时刻最新一条上报数据作为新增模板
+        List<DataMediumTermPowerForecastReport> reportList = dataMediumTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastTime - 1, forecastTime);
+        DataMediumTermPowerForecastReport report = (reportList == null || reportList.isEmpty()) ? null : reportList.get(0);
+        if (report == null)
+        {
+            return AjaxResult.error("未找到对应时刻的上报数据,无法新增修正数据");
+        }
+        DataMediumTermPowerForecastCorrect row = new DataMediumTermPowerForecastCorrect();
+        row.setDataSourcegrabId(report.getDataSourcegrabId());
+        row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+        row.setEntityId(report.getEntityId());
+        row.setStationIds(report.getStationIds());
+        row.setEntityType(report.getEntityType());
+        row.setEntityName(report.getEntityName());
+        row.setPredays(report.getPredays());
+        row.setStartTime(System.currentTimeMillis() / 1000);
+        row.setForecastTime(forecastTime);
+        row.setPower(power.trim());
+        dataMediumTermPowerForecastCorrectMapper.insertDataMediumTermPowerForecastCorrect(row);
+        return AjaxResult.success();
+    }
+
+    private double parsePower(String rawPower)
+    {
+        if (rawPower == null || rawPower.trim().isEmpty())
+        {
+            return 0.0;
+        }
+        try
+        {
+            return Double.parseDouble(rawPower.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return 0.0;
+        }
+    }
+
+    private String multiplyPower(String rawPower, BigDecimal factor)
+    {
+        return BigDecimal.valueOf(parsePower(rawPower)).multiply(factor)
+                .setScale(2, RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private List<Map<String, Object>> buildSeries(TreeSet<Long> times, Map<Long, Double> powerMap)
+    {
+        List<Map<String, Object>> series = new ArrayList<>();
+        for (Long time : times)
+        {
+            Map<String, Object> point = new LinkedHashMap<>();
+            point.put("time", TimeUtils.utcToGmt8Format(time));
+            point.put("power", powerMap.getOrDefault(time, 0.0));
+            series.add(point);
         }
-        return new ArrayList<>(latestByForecastTime.values());
+        return series;
     }
 
     /**

+ 23 - 1
forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/mapper/DataMediumTermPowerForecastCorrectMapper.java

@@ -1,6 +1,7 @@
 package com.ufo.project.forecast.mediumterm.power.mapper;
 
 import java.util.List;
+import org.apache.ibatis.annotations.Param;
 import com.ufo.project.forecast.mediumterm.power.domain.DataMediumTermPowerForecastCorrect;
 
 /**
@@ -53,9 +54,30 @@ public interface DataMediumTermPowerForecastCorrectMapper
 
     /**
      * 批量删除中期功率修正数据
-     * 
+     *
      * @param Ids 需要删除的数据主键集合
      * @return 结果
      */
     public int deleteDataMediumTermPowerForecastCorrectByIds(Long[] Ids);
+
+    /**
+     * 查询场站指定窗口内每个预测时刻的最新一条修正数据(窗口起点不含、终点含)
+     *
+     * @param entityId 实体ID
+     * @param windowStart 窗口起点(unix秒,不含)
+     * @param windowEnd 窗口终点(unix秒,含)
+     * @return 修正数据集合(按 forecast_time 升序)
+     */
+    public List<DataMediumTermPowerForecastCorrect> selectLatestByWindow(@Param("entityId") String entityId,
+            @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
+    /**
+     * 查询场站指定预测时刻start_time最大的最新一条修正数据(供upsert定位)
+     *
+     * @param entityId 实体ID
+     * @param forecastTime 预测时刻(unix秒)
+     * @return 修正数据(不存在返回null)
+     */
+    public DataMediumTermPowerForecastCorrect selectLatestByEntityAndForecastTime(@Param("entityId") String entityId,
+            @Param("forecastTime") Long forecastTime);
 }

+ 11 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/mediumterm/power/mapper/DataMediumTermPowerForecastReportMapper.java

@@ -71,6 +71,17 @@ public interface DataMediumTermPowerForecastReportMapper
      */
     public List<DataMediumTermPowerForecastReport> selectLatestForecastList(@Param("entityId") String entityId, @Param("startTime") Long startTime, @Param("endTime") Long endTime);
 
+    /**
+     * 查询场站指定窗口内每个预测时刻的最新一条上报数据(窗口起点不含、终点含,不限lead time)
+     *
+     * @param entityId 实体ID
+     * @param windowStart 窗口起点(unix秒,不含)
+     * @param windowEnd 窗口终点(unix秒,含)
+     * @return 上报数据集合(按 forecast_time 升序)
+     */
+    public List<DataMediumTermPowerForecastReport> selectLatestByWindow(@Param("entityId") String entityId,
+            @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
     /**
      * 按entity_id与start_time精确匹配查询场站上报数据(供upsert查重)
      *

+ 222 - 23
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/DataShortTermPowerForecastCorrectController.java

@@ -1,10 +1,13 @@
 package com.ufo.project.forecast.shortterm.power.controller;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
+import java.util.TreeSet;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -15,11 +18,18 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+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.project.forecast.shortterm.power.domain.DataShortTermPowerForecastCorrect;
+import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastReport;
+import com.ufo.project.forecast.shortterm.power.mapper.DataShortTermPowerForecastCorrectMapper;
+import com.ufo.project.forecast.shortterm.power.mapper.DataShortTermPowerForecastReportMapper;
 import com.ufo.project.forecast.shortterm.power.service.IDataShortTermPowerForecastCorrectService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.common.utils.poi.ExcelUtil;
@@ -27,7 +37,7 @@ import com.ufo.base.framework.web.page.TableDataInfo;
 
 /**
  * 短期功率修正数据Controller
- * 
+ *
  * @author ufo
  * @date 2026-05-05
  */
@@ -38,6 +48,15 @@ public class DataShortTermPowerForecastCorrectController extends BaseController
     @Autowired
     private IDataShortTermPowerForecastCorrectService dataShortTermPowerForecastCorrectService;
 
+    @Autowired
+    private DataShortTermPowerForecastReportMapper dataShortTermPowerForecastReportMapper;
+
+    @Autowired
+    private DataShortTermPowerForecastCorrectMapper dataShortTermPowerForecastCorrectMapper;
+
+    @Autowired
+    private IDataElectricityLocalService dataElectricityLocalService;
+
     /**
      * 查询短期功率修正数据列表
      */
@@ -50,12 +69,67 @@ public class DataShortTermPowerForecastCorrectController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询短期功率预测图表数据(上报/修正/观测三条曲线, 时间点取三者并集, 缺失补0)
+     */
     @PreAuthorize("@ss.hasPermi('power:powerForecastCorrect:list')")
     @GetMapping("/chart")
-    public TableDataInfo chart(DataShortTermPowerForecastCorrect dataShortTermPowerForecastCorrect)
+    public AjaxResult chart(@RequestParam("stationId") Long stationId,
+                            @RequestParam("forecastStartTime") Long forecastStartTime,
+                            @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataShortTermPowerForecastCorrect> list = this.queryList(dataShortTermPowerForecastCorrect);
-        return getDataTable(list);
+        String entityId = String.valueOf(stationId);
+
+        List<DataShortTermPowerForecastReport> reportList = dataShortTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> reportPowerMap = new HashMap<>();
+        if (reportList != null)
+        {
+            for (DataShortTermPowerForecastReport item : reportList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    reportPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        List<DataShortTermPowerForecastCorrect> correctList = dataShortTermPowerForecastCorrectMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> correctPowerMap = new HashMap<>();
+        if (correctList != null)
+        {
+            for (DataShortTermPowerForecastCorrect item : correctList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    correctPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        Map<Long, Double> monitorPowerMap = new HashMap<>();
+        List<DataElectricityLocal> localList = dataElectricityLocalService.selectActualPowerList(
+                stationId, TimeUtils.utcToGmt8Format(forecastStartTime), TimeUtils.utcToGmt8Format(forecastEndTime));
+        if (localList != null)
+        {
+            for (DataElectricityLocal item : localList)
+            {
+                if (item.getTm() != null)
+                {
+                    monitorPowerMap.put(item.getTm().getTime() / 1000, item.getActualPower() == null ? 0.0 : item.getActualPower());
+                }
+            }
+        }
+
+        TreeSet<Long> times = new TreeSet<>();
+        times.addAll(reportPowerMap.keySet());
+        times.addAll(correctPowerMap.keySet());
+        times.addAll(monitorPowerMap.keySet());
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("report", buildSeries(times, reportPowerMap));
+        body.put("correct", buildSeries(times, correctPowerMap));
+        body.put("monitor", buildSeries(times, monitorPowerMap));
+        return AjaxResult.success(body);
     }
 
     /**
@@ -64,36 +138,161 @@ public class DataShortTermPowerForecastCorrectController extends BaseController
     @PreAuthorize("@ss.hasPermi('power:powerForecastCorrect:export')")
     @Log(title = "短期功率修正数据", businessType = BusinessType.EXPORT)
     @PostMapping("/export/excel")
-    public void export(HttpServletResponse response, DataShortTermPowerForecastCorrect dataShortTermPowerForecastCorrect)
+    public void export(HttpServletResponse response, @RequestParam("stationId") Long stationId,
+                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                       @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataShortTermPowerForecastCorrect> list = this.queryList(dataShortTermPowerForecastCorrect);
+        List<DataShortTermPowerForecastCorrect> list = dataShortTermPowerForecastCorrectMapper.selectLatestByWindow(
+                String.valueOf(stationId), forecastStartTime, forecastEndTime);
         ExcelUtil<DataShortTermPowerForecastCorrect> util = new ExcelUtil<DataShortTermPowerForecastCorrect>(DataShortTermPowerForecastCorrect.class);
         util.exportExcel(response, list, "短期功率修正数据数据");
     }
 
-    private List<DataShortTermPowerForecastCorrect> queryList(DataShortTermPowerForecastCorrect dataShortTermPowerForecastCorrect)
+    /**
+     * 系数调整: 以窗口内上报数据为基准, 按系数生成/更新修正数据
+     */
+    @PreAuthorize("@ss.hasPermi('power:powerForecastCorrect:edit')")
+    @Log(title = "短期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/multiplierUpdate")
+    public AjaxResult multiplierUpdate(@RequestParam("stationId") Long stationId,
+                                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                                       @RequestParam("forecastEndTime") Long forecastEndTime,
+                                       @RequestParam("multiplier") Double multiplier)
     {
-        List<DataShortTermPowerForecastCorrect> list = dataShortTermPowerForecastCorrectService.selectDataShortTermPowerForecastCorrectList(dataShortTermPowerForecastCorrect);
-        if (list == null || list.isEmpty())
+        if (multiplier == null || multiplier < 0.01 || multiplier > 100.00)
         {
-            return list;
+            return AjaxResult.error("调整系数必须在0.01-100.00之间");
         }
-
-        // 按 forecast_time 分组,每组按 start_time desc, id desc 取第一条(即同一时间点最新起报的数据)
-        Comparator<DataShortTermPowerForecastCorrect> latestFirst = Comparator
-                .comparing(DataShortTermPowerForecastCorrect::getStartTime, Comparator.nullsFirst(Comparator.<Long>naturalOrder()))
-                .thenComparing(DataShortTermPowerForecastCorrect::getId, Comparator.nullsFirst(Comparator.<Long>naturalOrder()));
-        Map<Long, DataShortTermPowerForecastCorrect> latestByForecastTime = new TreeMap<>();
-        for (DataShortTermPowerForecastCorrect item : list)
+        String entityId = String.valueOf(stationId);
+        List<DataShortTermPowerForecastReport> reportList = dataShortTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        if (reportList == null || reportList.isEmpty())
         {
-            if (item.getForecastTime() == null)
+            return AjaxResult.error("窗口内无上报数据,无法调整");
+        }
+        BigDecimal factor = BigDecimal.valueOf(multiplier);
+        for (DataShortTermPowerForecastReport report : reportList)
+        {
+            if (report.getForecastTime() == null)
             {
                 continue;
             }
-            latestByForecastTime.merge(item.getForecastTime(), item,
-                    (existing, candidate) -> latestFirst.compare(candidate, existing) > 0 ? candidate : existing);
+            String newPower = multiplyPower(report.getPower(), factor);
+            DataShortTermPowerForecastCorrect existing = dataShortTermPowerForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, report.getForecastTime());
+            if (existing != null)
+            {
+                long newStartTime = report.getStartTime() != null
+                        && (existing.getStartTime() == null || existing.getStartTime() < report.getStartTime())
+                        ? report.getStartTime() : existing.getStartTime();
+                existing.setPower(newPower);
+                existing.setStartTime(newStartTime);
+                dataShortTermPowerForecastCorrectMapper.updateDataShortTermPowerForecastCorrect(existing);
+            }
+            else
+            {
+                DataShortTermPowerForecastCorrect row = new DataShortTermPowerForecastCorrect();
+                row.setDataSourcegrabId(report.getDataSourcegrabId());
+                row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+                row.setEntityId(report.getEntityId());
+                row.setStationIds(report.getStationIds());
+                row.setEntityType(report.getEntityType());
+                row.setEntityName(report.getEntityName());
+                row.setPredays(report.getPredays());
+                row.setStartTime(report.getStartTime());
+                row.setForecastTime(report.getForecastTime());
+                row.setPower(newPower);
+                dataShortTermPowerForecastCorrectMapper.insertDataShortTermPowerForecastCorrect(row);
+            }
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * 单点修正: 更新指定预测时刻的修正功率(无修正数据时以上报数据为模板新增)
+     */
+    @PreAuthorize("@ss.hasPermi('power:powerForecastCorrect:edit')")
+    @Log(title = "短期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/update")
+    public AjaxResult update(@RequestBody DataShortTermPowerForecastCorrect dataShortTermPowerForecastCorrect)
+    {
+        String entityId = dataShortTermPowerForecastCorrect.getEntityId();
+        Long forecastTime = dataShortTermPowerForecastCorrect.getForecastTime();
+        String power = dataShortTermPowerForecastCorrect.getPower();
+        if (entityId == null || entityId.isEmpty() || forecastTime == null || power == null || power.trim().isEmpty())
+        {
+            return AjaxResult.error("参数不完整");
+        }
+        try
+        {
+            Double.parseDouble(power.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return AjaxResult.error("修正功率必须为数字");
+        }
+
+        DataShortTermPowerForecastCorrect existing = dataShortTermPowerForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, forecastTime);
+        if (existing != null)
+        {
+            existing.setPower(power.trim());
+            dataShortTermPowerForecastCorrectMapper.updateDataShortTermPowerForecastCorrect(existing);
+            return AjaxResult.success();
+        }
+
+        // 无修正数据时, 取该时刻最新一条上报数据作为新增模板
+        List<DataShortTermPowerForecastReport> reportList = dataShortTermPowerForecastReportMapper.selectLatestByWindow(entityId, forecastTime - 1, forecastTime);
+        DataShortTermPowerForecastReport report = (reportList == null || reportList.isEmpty()) ? null : reportList.get(0);
+        if (report == null)
+        {
+            return AjaxResult.error("未找到对应时刻的上报数据,无法新增修正数据");
+        }
+        DataShortTermPowerForecastCorrect row = new DataShortTermPowerForecastCorrect();
+        row.setDataSourcegrabId(report.getDataSourcegrabId());
+        row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+        row.setEntityId(report.getEntityId());
+        row.setStationIds(report.getStationIds());
+        row.setEntityType(report.getEntityType());
+        row.setEntityName(report.getEntityName());
+        row.setPredays(report.getPredays());
+        row.setStartTime(System.currentTimeMillis() / 1000);
+        row.setForecastTime(forecastTime);
+        row.setPower(power.trim());
+        dataShortTermPowerForecastCorrectMapper.insertDataShortTermPowerForecastCorrect(row);
+        return AjaxResult.success();
+    }
+
+    private double parsePower(String rawPower)
+    {
+        if (rawPower == null || rawPower.trim().isEmpty())
+        {
+            return 0.0;
+        }
+        try
+        {
+            return Double.parseDouble(rawPower.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return 0.0;
+        }
+    }
+
+    private String multiplyPower(String rawPower, BigDecimal factor)
+    {
+        return BigDecimal.valueOf(parsePower(rawPower)).multiply(factor)
+                .setScale(2, RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private List<Map<String, Object>> buildSeries(TreeSet<Long> times, Map<Long, Double> powerMap)
+    {
+        List<Map<String, Object>> series = new ArrayList<>();
+        for (Long time : times)
+        {
+            Map<String, Object> point = new LinkedHashMap<>();
+            point.put("time", TimeUtils.utcToGmt8Format(time));
+            point.put("power", powerMap.getOrDefault(time, 0.0));
+            series.add(point);
         }
-        return new ArrayList<>(latestByForecastTime.values());
+        return series;
     }
 
     /**

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

@@ -70,4 +70,14 @@ public interface DataShortTermPowerForecastCorrectMapper
      */
     public List<DataShortTermPowerForecastCorrect> selectLatestByWindow(@Param("entityId") String entityId,
             @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
+    /**
+     * 查询场站指定预测时刻start_time最大的最新一条修正数据(供upsert定位)
+     *
+     * @param entityId 实体ID
+     * @param forecastTime 预测时刻(unix秒)
+     * @return 修正数据(不存在返回null)
+     */
+    public DataShortTermPowerForecastCorrect selectLatestByEntityAndForecastTime(@Param("entityId") String entityId,
+            @Param("forecastTime") Long forecastTime);
 }

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

@@ -84,6 +84,17 @@ public interface DataShortTermPowerForecastReportMapper
      */
     public List<DataShortTermPowerForecastReport> selectLatestForecastList(@Param("entityId") String entityId, @Param("startTime") Long startTime, @Param("endTime") Long endTime);
 
+    /**
+     * 查询场站指定窗口内每个预测时刻的最新一条上报数据(窗口起点不含、终点含,不限lead time)
+     *
+     * @param entityId 实体ID
+     * @param windowStart 窗口起点(unix秒,不含)
+     * @param windowEnd 窗口终点(unix秒,含)
+     * @return 上报数据集合(按 forecast_time 升序)
+     */
+    public List<DataShortTermPowerForecastReport> selectLatestByWindow(@Param("entityId") String entityId,
+            @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
     /**
      * 按上报批次号统计条数(用于批次幂等判断)
      *

+ 221 - 23
forecast-backend-server/src/main/java/com/ufo/project/forecast/supershort/controller/DataSuperShortTermForecastCorrectController.java

@@ -1,10 +1,13 @@
 package com.ufo.project.forecast.supershort.controller;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
+import java.util.TreeSet;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -15,11 +18,18 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+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.project.forecast.supershort.domain.DataSuperShortTermForecastCorrect;
+import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastReport;
+import com.ufo.project.forecast.supershort.mapper.DataSuperShortTermForecastCorrectMapper;
+import com.ufo.project.forecast.supershort.mapper.DataSuperShortTermForecastReportMapper;
 import com.ufo.project.forecast.supershort.service.IDataSuperShortTermForecastCorrectService;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.service.IDataElectricityLocalService;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.common.utils.poi.ExcelUtil;
@@ -27,7 +37,7 @@ import com.ufo.base.framework.web.page.TableDataInfo;
 
 /**
  * 超短期功率修正数据Controller
- * 
+ *
  * @author ufo
  * @date 2026-05-05
  */
@@ -38,6 +48,15 @@ public class DataSuperShortTermForecastCorrectController extends BaseController
     @Autowired
     private IDataSuperShortTermForecastCorrectService dataSuperShortTermForecastCorrectService;
 
+    @Autowired
+    private DataSuperShortTermForecastReportMapper dataSuperShortTermForecastReportMapper;
+
+    @Autowired
+    private DataSuperShortTermForecastCorrectMapper dataSuperShortTermForecastCorrectMapper;
+
+    @Autowired
+    private IDataElectricityLocalService dataElectricityLocalService;
+
     /**
      * 查询超短期功率修正数据列表
      */
@@ -49,12 +68,68 @@ public class DataSuperShortTermForecastCorrectController extends BaseController
         List<DataSuperShortTermForecastCorrect> list = dataSuperShortTermForecastCorrectService.selectDataSuperShortTermForecastCorrectList(dataSuperShortTermForecastCorrect);
         return getDataTable(list);
     }
+
+    /**
+     * 查询超短期功率预测图表数据(上报/修正/观测三条曲线, 时间点取三者并集, 缺失补0)
+     */
     @PreAuthorize("@ss.hasPermi('supershort:termForecastCorrect:list')")
     @GetMapping("/chart")
-    public TableDataInfo chart(DataSuperShortTermForecastCorrect dataSuperShortTermForecastCorrect)
+    public AjaxResult chart(@RequestParam("stationId") Long stationId,
+                            @RequestParam("forecastStartTime") Long forecastStartTime,
+                            @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataSuperShortTermForecastCorrect> list = this.queryList(dataSuperShortTermForecastCorrect);
-        return getDataTable(list);
+        String entityId = String.valueOf(stationId);
+
+        List<DataSuperShortTermForecastReport> reportList = dataSuperShortTermForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> reportPowerMap = new HashMap<>();
+        if (reportList != null)
+        {
+            for (DataSuperShortTermForecastReport item : reportList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    reportPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        List<DataSuperShortTermForecastCorrect> correctList = dataSuperShortTermForecastCorrectMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        Map<Long, Double> correctPowerMap = new HashMap<>();
+        if (correctList != null)
+        {
+            for (DataSuperShortTermForecastCorrect item : correctList)
+            {
+                if (item.getForecastTime() != null)
+                {
+                    correctPowerMap.put(item.getForecastTime(), parsePower(item.getPower()));
+                }
+            }
+        }
+
+        Map<Long, Double> monitorPowerMap = new HashMap<>();
+        List<DataElectricityLocal> localList = dataElectricityLocalService.selectActualPowerList(
+                stationId, TimeUtils.utcToGmt8Format(forecastStartTime), TimeUtils.utcToGmt8Format(forecastEndTime));
+        if (localList != null)
+        {
+            for (DataElectricityLocal item : localList)
+            {
+                if (item.getTm() != null)
+                {
+                    monitorPowerMap.put(item.getTm().getTime() / 1000, item.getActualPower() == null ? 0.0 : item.getActualPower());
+                }
+            }
+        }
+
+        TreeSet<Long> times = new TreeSet<>();
+        times.addAll(reportPowerMap.keySet());
+        times.addAll(correctPowerMap.keySet());
+        times.addAll(monitorPowerMap.keySet());
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("report", buildSeries(times, reportPowerMap));
+        body.put("correct", buildSeries(times, correctPowerMap));
+        body.put("monitor", buildSeries(times, monitorPowerMap));
+        return AjaxResult.success(body);
     }
 
     /**
@@ -63,36 +138,159 @@ public class DataSuperShortTermForecastCorrectController extends BaseController
     @PreAuthorize("@ss.hasPermi('supershort:termForecastCorrect:export')")
     @Log(title = "超短期功率修正数据", businessType = BusinessType.EXPORT)
     @PostMapping("/export/excel")
-    public void export(HttpServletResponse response, DataSuperShortTermForecastCorrect dataSuperShortTermForecastCorrect)
+    public void export(HttpServletResponse response, @RequestParam("stationId") Long stationId,
+                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                       @RequestParam("forecastEndTime") Long forecastEndTime)
     {
-        List<DataSuperShortTermForecastCorrect> list = this.queryList(dataSuperShortTermForecastCorrect);
+        List<DataSuperShortTermForecastCorrect> list = dataSuperShortTermForecastCorrectMapper.selectLatestByWindow(
+                String.valueOf(stationId), forecastStartTime, forecastEndTime);
         ExcelUtil<DataSuperShortTermForecastCorrect> util = new ExcelUtil<DataSuperShortTermForecastCorrect>(DataSuperShortTermForecastCorrect.class);
         util.exportExcel(response, list, "超短期功率修正数据数据");
     }
 
-    private List<DataSuperShortTermForecastCorrect> queryList(DataSuperShortTermForecastCorrect dataSuperShortTermForecastCorrect)
+    /**
+     * 系数调整: 以窗口内上报数据为基准, 按系数生成/更新修正数据
+     */
+    @PreAuthorize("@ss.hasPermi('supershort:termForecastCorrect:edit')")
+    @Log(title = "超短期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/multiplierUpdate")
+    public AjaxResult multiplierUpdate(@RequestParam("stationId") Long stationId,
+                                       @RequestParam("forecastStartTime") Long forecastStartTime,
+                                       @RequestParam("forecastEndTime") Long forecastEndTime,
+                                       @RequestParam("multiplier") Double multiplier)
     {
-        List<DataSuperShortTermForecastCorrect> list = dataSuperShortTermForecastCorrectService.selectDataSuperShortTermForecastCorrectList(dataSuperShortTermForecastCorrect);
-        if (list == null || list.isEmpty())
+        if (multiplier == null || multiplier < 0.01 || multiplier > 100.00)
         {
-            return list;
+            return AjaxResult.error("调整系数必须在0.01-100.00之间");
         }
-
-        // 按 forecast_time 分组,每组按 start_time desc, id desc 取第一条(即同一时间点最新起报的数据)
-        Comparator<DataSuperShortTermForecastCorrect> latestFirst = Comparator
-                .comparing(DataSuperShortTermForecastCorrect::getStartTime, Comparator.nullsFirst(Comparator.<Long>naturalOrder()))
-                .thenComparing(DataSuperShortTermForecastCorrect::getId, Comparator.nullsFirst(Comparator.<Long>naturalOrder()));
-        Map<Long, DataSuperShortTermForecastCorrect> latestByForecastTime = new TreeMap<>();
-        for (DataSuperShortTermForecastCorrect item : list)
+        String entityId = String.valueOf(stationId);
+        List<DataSuperShortTermForecastReport> reportList = dataSuperShortTermForecastReportMapper.selectLatestByWindow(entityId, forecastStartTime, forecastEndTime);
+        if (reportList == null || reportList.isEmpty())
         {
-            if (item.getForecastTime() == null)
+            return AjaxResult.error("窗口内无上报数据,无法调整");
+        }
+        BigDecimal factor = BigDecimal.valueOf(multiplier);
+        for (DataSuperShortTermForecastReport report : reportList)
+        {
+            if (report.getForecastTime() == null)
             {
                 continue;
             }
-            latestByForecastTime.merge(item.getForecastTime(), item,
-                    (existing, candidate) -> latestFirst.compare(candidate, existing) > 0 ? candidate : existing);
+            String newPower = multiplyPower(report.getPower(), factor);
+            DataSuperShortTermForecastCorrect existing = dataSuperShortTermForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, report.getForecastTime());
+            if (existing != null)
+            {
+                long newStartTime = report.getStartTime() != null
+                        && (existing.getStartTime() == null || existing.getStartTime() < report.getStartTime())
+                        ? report.getStartTime() : existing.getStartTime();
+                existing.setPower(newPower);
+                existing.setStartTime(newStartTime);
+                dataSuperShortTermForecastCorrectMapper.updateDataSuperShortTermForecastCorrect(existing);
+            }
+            else
+            {
+                DataSuperShortTermForecastCorrect row = new DataSuperShortTermForecastCorrect();
+                row.setDataSourcegrabId(report.getDataSourcegrabId());
+                row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+                row.setEntityId(report.getEntityId());
+                row.setEntityType(report.getEntityType());
+                row.setEntityName(report.getEntityName());
+                row.setPreQrts(report.getPreQrts());
+                row.setStartTime(report.getStartTime());
+                row.setForecastTime(report.getForecastTime());
+                row.setPower(newPower);
+                dataSuperShortTermForecastCorrectMapper.insertDataSuperShortTermForecastCorrect(row);
+            }
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * 单点修正: 更新指定预测时刻的修正功率(无修正数据时以上报数据为模板新增)
+     */
+    @PreAuthorize("@ss.hasPermi('supershort:termForecastCorrect:edit')")
+    @Log(title = "超短期功率修正数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/update")
+    public AjaxResult update(@RequestBody DataSuperShortTermForecastCorrect dataSuperShortTermForecastCorrect)
+    {
+        String entityId = dataSuperShortTermForecastCorrect.getEntityId();
+        Long forecastTime = dataSuperShortTermForecastCorrect.getForecastTime();
+        String power = dataSuperShortTermForecastCorrect.getPower();
+        if (entityId == null || entityId.isEmpty() || forecastTime == null || power == null || power.trim().isEmpty())
+        {
+            return AjaxResult.error("参数不完整");
+        }
+        try
+        {
+            Double.parseDouble(power.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return AjaxResult.error("修正功率必须为数字");
+        }
+
+        DataSuperShortTermForecastCorrect existing = dataSuperShortTermForecastCorrectMapper.selectLatestByEntityAndForecastTime(entityId, forecastTime);
+        if (existing != null)
+        {
+            existing.setPower(power.trim());
+            dataSuperShortTermForecastCorrectMapper.updateDataSuperShortTermForecastCorrect(existing);
+            return AjaxResult.success();
+        }
+
+        // 无修正数据时, 取该时刻最新一条上报数据作为新增模板
+        List<DataSuperShortTermForecastReport> reportList = dataSuperShortTermForecastReportMapper.selectLatestByWindow(entityId, forecastTime - 1, forecastTime);
+        DataSuperShortTermForecastReport report = (reportList == null || reportList.isEmpty()) ? null : reportList.get(0);
+        if (report == null)
+        {
+            return AjaxResult.error("未找到对应时刻的上报数据,无法新增修正数据");
+        }
+        DataSuperShortTermForecastCorrect row = new DataSuperShortTermForecastCorrect();
+        row.setDataSourcegrabId(report.getDataSourcegrabId());
+        row.setDataSourcegrabPatternName(report.getDataSourcegrabPatternName());
+        row.setEntityId(report.getEntityId());
+        row.setEntityType(report.getEntityType());
+        row.setEntityName(report.getEntityName());
+        row.setPreQrts(report.getPreQrts());
+        row.setStartTime(System.currentTimeMillis() / 1000);
+        row.setForecastTime(forecastTime);
+        row.setPower(power.trim());
+        dataSuperShortTermForecastCorrectMapper.insertDataSuperShortTermForecastCorrect(row);
+        return AjaxResult.success();
+    }
+
+    private double parsePower(String rawPower)
+    {
+        if (rawPower == null || rawPower.trim().isEmpty())
+        {
+            return 0.0;
+        }
+        try
+        {
+            return Double.parseDouble(rawPower.trim());
+        }
+        catch (NumberFormatException e)
+        {
+            return 0.0;
+        }
+    }
+
+    private String multiplyPower(String rawPower, BigDecimal factor)
+    {
+        return BigDecimal.valueOf(parsePower(rawPower)).multiply(factor)
+                .setScale(2, RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private List<Map<String, Object>> buildSeries(TreeSet<Long> times, Map<Long, Double> powerMap)
+    {
+        List<Map<String, Object>> series = new ArrayList<>();
+        for (Long time : times)
+        {
+            Map<String, Object> point = new LinkedHashMap<>();
+            point.put("time", TimeUtils.utcToGmt8Format(time));
+            point.put("power", powerMap.getOrDefault(time, 0.0));
+            series.add(point);
         }
-        return new ArrayList<>(latestByForecastTime.values());
+        return series;
     }
 
     /**

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

@@ -70,4 +70,14 @@ public interface DataSuperShortTermForecastCorrectMapper
      */
     public List<DataSuperShortTermForecastCorrect> selectLatestByWindow(@Param("entityId") String entityId,
             @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
+    /**
+     * 查询场站指定预测时刻start_time最大的最新一条修正数据(供upsert定位)
+     *
+     * @param entityId 实体ID
+     * @param forecastTime 预测时刻(unix秒)
+     * @return 修正数据(不存在返回null)
+     */
+    public DataSuperShortTermForecastCorrect selectLatestByEntityAndForecastTime(@Param("entityId") String entityId,
+            @Param("forecastTime") Long forecastTime);
 }

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

@@ -82,6 +82,17 @@ public interface DataSuperShortTermForecastReportMapper
      */
     public List<DataSuperShortTermForecastReport> selectLatestForecastList(@Param("entityId") String entityId, @Param("startTime") Long startTime, @Param("endTime") Long endTime);
 
+    /**
+     * 查询场站指定窗口内每个预测时刻的最新一条上报数据(窗口起点不含、终点含,不限lead time)
+     *
+     * @param entityId 实体ID
+     * @param windowStart 窗口起点(unix秒,不含)
+     * @param windowEnd 窗口终点(unix秒,含)
+     * @return 上报数据集合(按 forecast_time 升序)
+     */
+    public List<DataSuperShortTermForecastReport> selectLatestByWindow(@Param("entityId") String entityId,
+            @Param("windowStart") Long windowStart, @Param("windowEnd") Long windowEnd);
+
     /**
      * 按上报批次号统计条数(用于批次幂等判断)
      *

+ 22 - 3
forecast-backend-server/src/main/resources/mybatis/mediumpower/DataMediumTermPowerForecastCorrectMapper.xml

@@ -50,6 +50,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectDataMediumTermPowerForecastCorrectVo"/>
         where _id = #{Id}
     </select>
+
+    <!-- 按预测时刻取窗口内最新一条修正数据,起点不含、终点含 -->
+    <select id="selectLatestByWindow" resultMap="DataMediumTermPowerForecastCorrectResult">
+        select distinct on (forecast_time)
+        _id, data_sourcegrab_id, data_sourcegrab_pattern_name, entity_id, entity_type, entity_name, to_timestamp(create_time) AS create_time, start_time, forecast_time, predays, power
+        from data_medium_term_power_forecast_correct
+        where entity_id = #{entityId}
+          and forecast_time &gt; #{windowStart}
+          and forecast_time &lt;= #{windowEnd}
+        order by forecast_time asc, start_time desc, _id desc
+    </select>
+
+    <!-- 按预测时刻取start_time最大的一条修正数据(供upsert定位) -->
+    <select id="selectLatestByEntityAndForecastTime" resultMap="DataMediumTermPowerForecastCorrectResult">
+        <include refid="selectDataMediumTermPowerForecastCorrectVo"/>
+        where entity_id = #{entityId}
+          and forecast_time = #{forecastTime}
+        order by start_time desc, _id desc
+        limit 1
+    </select>
         
     <insert id="insertDataMediumTermPowerForecastCorrect" parameterType="DataMediumTermPowerForecastCorrect" useGeneratedKeys="true" keyProperty="Id">
         insert into data_medium_term_power_forecast_correct
@@ -59,7 +79,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id,</if>
             <if test="entityType != null">entity_type,</if>
             <if test="entityName != null">entity_name,</if>
-            <if test="createTime != null">create_time,</if>
+            create_time,
             <if test="startTime != null">start_time,</if>
             <if test="forecastTime != null">forecast_time,</if>
             <if test="predays != null">predays,</if>
@@ -71,7 +91,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">#{entityId},</if>
             <if test="entityType != null">#{entityType},</if>
             <if test="entityName != null">#{entityName},</if>
-            <if test="createTime != null">#{createTime},</if>
+            EXTRACT(EPOCH FROM now())::INTEGER,
             <if test="startTime != null">#{startTime},</if>
             <if test="forecastTime != null">#{forecastTime},</if>
             <if test="predays != null">#{predays},</if>
@@ -87,7 +107,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id = #{entityId},</if>
             <if test="entityType != null">entity_type = #{entityType},</if>
             <if test="entityName != null">entity_name = #{entityName},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
             <if test="startTime != null">start_time = #{startTime},</if>
             <if test="forecastTime != null">forecast_time = #{forecastTime},</if>
             <if test="predays != null">predays = #{predays},</if>

+ 11 - 0
forecast-backend-server/src/main/resources/mybatis/mediumpower/DataMediumTermPowerForecastReportMapper.xml

@@ -118,6 +118,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY forecast_time asc, start_time desc, _id DESC
     </select>
 
+    <!-- 按预测时刻取窗口内最新一条上报数据,起点不含、终点含,不限lead time -->
+    <select id="selectLatestByWindow" resultMap="DataMediumTermPowerForecastReportResult">
+        select distinct on (forecast_time)
+        _id, data_sourcegrab_id, data_sourcegrab_pattern_name, entity_id, entity_type, entity_name, to_timestamp(create_time) AS create_time, start_time, forecast_time, predays, power
+        from data_medium_term_power_forecast_report
+        where entity_id = #{entityId}
+          and forecast_time &gt; #{windowStart}
+          and forecast_time &lt;= #{windowEnd}
+        order by forecast_time asc, start_time desc, _id desc
+    </select>
+
     <!-- 按entity_id与start_time精确匹配查询场站上报数据(供upsert查重) -->
     <select id="selectByEntityAndStartTime" parameterType="DataMediumTermPowerForecastReport" resultMap="DataMediumTermPowerForecastReportResult">
         <include refid="selectDataMediumTermPowerForecastReportVo"/>

+ 11 - 3
forecast-backend-server/src/main/resources/mybatis/power/DataShortTermPowerForecastCorrectMapper.xml

@@ -61,6 +61,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           and forecast_time &lt;= #{windowEnd}
         order by forecast_time asc, start_time desc, _id desc
     </select>
+
+    <!-- 按预测时刻取start_time最大的一条修正数据(供upsert定位) -->
+    <select id="selectLatestByEntityAndForecastTime" resultMap="DataShortTermPowerForecastCorrectResult">
+        <include refid="selectDataShortTermPowerForecastCorrectVo"/>
+        where entity_id = #{entityId}
+          and forecast_time = #{forecastTime}
+        order by start_time desc, _id desc
+        limit 1
+    </select>
         
     <insert id="insertDataShortTermPowerForecastCorrect" parameterType="DataShortTermPowerForecastCorrect" useGeneratedKeys="true" keyProperty="Id">
         insert into data_short_term_power_forecast_correct
@@ -70,7 +79,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id,</if>
             <if test="entityType != null">entity_type,</if>
             <if test="entityName != null">entity_name,</if>
-            <if test="createTime != null">create_time,</if>
+            create_time,
             <if test="startTime != null">start_time,</if>
             <if test="forecastTime != null">forecast_time,</if>
             <if test="predays != null">predays,</if>
@@ -82,7 +91,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">#{entityId},</if>
             <if test="entityType != null">#{entityType},</if>
             <if test="entityName != null">#{entityName},</if>
-            <if test="createTime != null">#{createTime},</if>
+            EXTRACT(EPOCH FROM now())::INTEGER,
             <if test="startTime != null">#{startTime},</if>
             <if test="forecastTime != null">#{forecastTime},</if>
             <if test="predays != null">#{predays},</if>
@@ -98,7 +107,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id = #{entityId},</if>
             <if test="entityType != null">entity_type = #{entityType},</if>
             <if test="entityName != null">entity_name = #{entityName},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
             <if test="startTime != null">start_time = #{startTime},</if>
             <if test="forecastTime != null">forecast_time = #{forecastTime},</if>
             <if test="predays != null">predays = #{predays},</if>

+ 11 - 0
forecast-backend-server/src/main/resources/mybatis/power/DataShortTermPowerForecastReportMapper.xml

@@ -139,6 +139,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY forecast_time asc, start_time desc, _id DESC
     </select>
 
+    <!-- 按预测时刻取窗口内最新一条上报数据,起点不含、终点含,不限lead time -->
+    <select id="selectLatestByWindow" resultMap="DataShortTermPowerForecastReportResult">
+        select distinct on (forecast_time)
+        _id, data_sourcegrab_id, data_sourcegrab_pattern_name, entity_id, entity_type, entity_name, to_timestamp(create_time) AS create_time, start_time, forecast_time, predays, power
+        from data_short_term_power_forecast_report
+        where entity_id = #{entityId}
+          and forecast_time &gt; #{windowStart}
+          and forecast_time &lt;= #{windowEnd}
+        order by forecast_time asc, start_time desc, _id desc
+    </select>
+
     <select id="countByBatchNo" resultType="int">
         select count(1) from data_short_term_power_forecast_report where batch_no = #{batchNo}
     </select>

+ 11 - 3
forecast-backend-server/src/main/resources/mybatis/supershort/DataSuperShortTermForecastCorrectMapper.xml

@@ -56,6 +56,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           and forecast_time &lt;= #{windowEnd}
         order by forecast_time asc, start_time desc, _id desc
     </select>
+
+    <!-- 按预测时刻取start_time最大的一条修正数据(供upsert定位) -->
+    <select id="selectLatestByEntityAndForecastTime" resultMap="DataSuperShortTermForecastCorrectResult">
+        <include refid="selectDataSuperShortTermForecastCorrectVo"/>
+        where entity_id = #{entityId}
+          and forecast_time = #{forecastTime}
+        order by start_time desc, _id desc
+        limit 1
+    </select>
         
     <insert id="insertDataSuperShortTermForecastCorrect" parameterType="DataSuperShortTermForecastCorrect" useGeneratedKeys="true" keyProperty="Id">
         insert into data_super_short_term_forecast_correct
@@ -65,7 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id,</if>
             <if test="entityType != null">entity_type,</if>
             <if test="entityName != null">entity_name,</if>
-            <if test="createTime != null">create_time,</if>
+            create_time,
             <if test="startTime != null">start_time,</if>
             <if test="forecastTime != null">forecast_time,</if>
             <if test="preQrts != null">pre_qrts,</if>
@@ -77,7 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">#{entityId},</if>
             <if test="entityType != null">#{entityType},</if>
             <if test="entityName != null">#{entityName},</if>
-            <if test="createTime != null">#{createTime},</if>
+            EXTRACT(EPOCH FROM now())::INTEGER,
             <if test="startTime != null">#{startTime},</if>
             <if test="forecastTime != null">#{forecastTime},</if>
             <if test="preQrts != null">#{preQrts},</if>
@@ -93,7 +102,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="entityId != null and entityId != ''">entity_id = #{entityId},</if>
             <if test="entityType != null">entity_type = #{entityType},</if>
             <if test="entityName != null">entity_name = #{entityName},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
             <if test="startTime != null">start_time = #{startTime},</if>
             <if test="forecastTime != null">forecast_time = #{forecastTime},</if>
             <if test="preQrts != null">pre_qrts = #{preQrts},</if>

+ 11 - 0
forecast-backend-server/src/main/resources/mybatis/supershort/DataSuperShortTermForecastReportMapper.xml

@@ -130,6 +130,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY forecast_time asc, start_time desc, _id DESC
     </select>
 
+    <!-- 按预测时刻取窗口内最新一条上报数据,起点不含、终点含,不限lead time -->
+    <select id="selectLatestByWindow" resultMap="DataSuperShortTermForecastReportResult">
+        select distinct on (forecast_time)
+        _id, data_sourcegrab_id, data_sourcegrab_pattern_name, entity_id, entity_type, entity_name, to_timestamp(create_time) AS create_time, start_time, forecast_time, pre_qrts, power
+        from data_super_short_term_forecast_report
+        where entity_id = #{entityId}
+          and forecast_time &gt; #{windowStart}
+          and forecast_time &lt;= #{windowEnd}
+        order by forecast_time asc, start_time desc, _id desc
+    </select>
+
     <select id="countByBatchNo" resultType="int">
         select count(1) from data_super_short_term_forecast_report where batch_no = #{batchNo}
     </select>

+ 81 - 0
forecast-backend-vue/src/api/power/dataMediumTermPowerForecastCorrect.js

@@ -0,0 +1,81 @@
+import request from '@/utils/request'
+
+// 查询中期功率修正数据列表
+export function listDataMediumTermPowerForecastCorrect(query) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询中期功率修正图表数据(上报/修正/观测)
+export function chartDataMediumTermPowerForecastCorrect(query) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/chart',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询中期功率修正数据详细
+export function getDataMediumTermPowerForecastCorrect(Id) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/' + Id,
+    method: 'get'
+  })
+}
+
+// 新增中期功率修正数据
+export function addDataMediumTermPowerForecastCorrect(data) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改中期功率修正数据
+export function updateDataMediumTermPowerForecastCorrect(data) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除中期功率修正数据
+export function delDataMediumTermPowerForecastCorrect(Id) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/' + Id,
+    method: 'delete'
+  })
+}
+
+// 导出中期功率修正数据
+export function exportDataMediumTermPowerForecastCorrect(data) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/export/excel',
+    method: 'post',
+    params: data,
+    responseType: 'blob'
+  })
+}
+
+// 系数调整: 以窗口内上报数据为基准批量修正
+export function multiplierUpdateDataMediumTermPowerForecastCorrect(data) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/multiplierUpdate',
+    method: 'post',
+    params: data
+  })
+}
+
+// 功率修正单条修改
+export function singlePointUpdateDataMediumTermPowerForecastCorrect(data) {
+  return request({
+    url: '/mediumterm/powerForecastCorrect/update',
+    method: 'post',
+    data: data
+  })
+}

+ 19 - 1
forecast-backend-vue/src/api/power/dataShortTermPowerForecastCorrect.js

@@ -55,7 +55,25 @@ export function exportDataShortTermPowerForecastCorrect(data) {
   return request({
     url: '/power/powerForecastCorrect/export/excel',
     method: 'post',
-    data: data,
+    params: data,
     responseType: 'blob'
   })
 }
+
+// 系数调整: 以窗口内上报数据为基准批量修正
+export function multiplierUpdateDataShortTermPowerForecastCorrect(data) {
+  return request({
+    url: '/power/powerForecastCorrect/multiplierUpdate',
+    method: 'post',
+    params: data
+  })
+}
+
+// 功率修正单条修改
+export function singlePointUpdateDataShortTermPowerForecastCorrect(data) {
+  return request({
+    url: '/power/powerForecastCorrect/update',
+    method: 'post',
+    data: data
+  })
+}

+ 19 - 1
forecast-backend-vue/src/api/power/dataSuperShortTermForecastCorrect.js

@@ -55,7 +55,25 @@ export function exportDataSuperShortTermForecastCorrect(data) {
   return request({
     url: '/supershort/termForecastCorrect/export/excel',
     method: 'post',
-    data: data,
+    params: data,
     responseType: 'blob'
   })
 }
+
+// 系数调整: 以窗口内上报数据为基准批量修正
+export function multiplierUpdateDataSuperShortTermForecastCorrect(data) {
+  return request({
+    url: '/supershort/termForecastCorrect/multiplierUpdate',
+    method: 'post',
+    params: data
+  })
+}
+
+// 功率修正单条修改
+export function singlePointUpdateDataSuperShortTermForecastCorrect(data) {
+  return request({
+    url: '/supershort/termForecastCorrect/update',
+    method: 'post',
+    data: data
+  })
+}

+ 321 - 0
forecast-backend-vue/src/views/ufo/electricity/forecast/DataMediumTermPowerForecastCorrect.vue

@@ -0,0 +1,321 @@
+<template>
+  <div class="data-medium-term-power-forecast-correct-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="5">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="4">
+          <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="4">
+          <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="11">
+          <a-space>
+            <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :disabled="!queryValid" @click="openMultiplierModal">系数调整</a-button>
+            <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-card title="功率修正曲线图" :bordered="false" class="content-card">
+      <PowerLineChart :x-axis="chartAxis" :series="chartSeries" height="360px" y-axis-name="kW" />
+    </a-card>
+
+    <a-card title="数据详情" :bordered="false" class="detail-card">
+      <BusinessDataTable
+        :columns="columns"
+        :data-source="tableRows"
+        row-key="time"
+        :pagination="{pageSize: 10}"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+
+    <a-modal
+      v-model:open="multiplierVisible"
+      title="系数调整"
+      :confirm-loading="multiplierSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleMultiplierConfirm"
+    >
+      <a-form layout="vertical">
+        <a-form-item label="调整系数">
+          <a-input-number
+            v-model:value="multiplierForm.multiplier"
+            :min="0.01"
+            :max="100"
+            :step="0.01"
+            :precision="2"
+            placeholder="0.01 - 100.00"
+            class="full-control"
+          />
+        </a-form-item>
+      </a-form>
+      <div class="modal-tip">以当前窗口内上报数据为基准,按所选系数批量生成/更新修正数据(修正功率 = 上报功率 × 系数)。</div>
+    </a-modal>
+
+    <a-modal
+      v-model:open="correctVisible"
+      title="功率修正"
+      :confirm-loading="correctSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleCorrectConfirm"
+    >
+      <a-descriptions :column="1" size="small" bordered>
+        <a-descriptions-item label="时间">{{ correctForm.row ? correctForm.row.time : '' }}</a-descriptions-item>
+        <a-descriptions-item label="上报功率 (kW)">{{ correctForm.row ? correctForm.row.reportPower : '' }}</a-descriptions-item>
+        <a-descriptions-item label="观测功率 (kW)">{{ correctForm.row ? correctForm.row.monitorPower : '' }}</a-descriptions-item>
+      </a-descriptions>
+      <a-form layout="vertical" class="correct-power-form">
+        <a-form-item label="修正功率 (kW)">
+          <a-input-number v-model:value="correctForm.power" :min="0" :step="0.1" :precision="2" class="full-control" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+
+<script setup name="DataMediumTermPowerForecastCorrect">
+import {computed, h, onMounted, reactive, ref} from 'vue';
+import {Button as AButton, 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 {
+  chartDataMediumTermPowerForecastCorrect,
+  exportDataMediumTermPowerForecastCorrect,
+  multiplierUpdateDataMediumTermPowerForecastCorrect,
+  singlePointUpdateDataMediumTermPowerForecastCorrect
+} from '@/api/power/dataMediumTermPowerForecastCorrect';
+import BusinessDataTable from '@/views/ufo/electricity/components/BusinessDataTable.vue';
+import PowerLineChart from '@/views/ufo/electricity/components/PowerLineChart.vue';
+import StationSelector from '@/views/ufo/electricity/components/StationSelector.vue';
+import {normalizeRows} from '@/views/ufo/electricity/components/dataAdapter';
+import {toStationOptions} from '@/views/ufo/electricity/components/stationAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+
+const seriesData = reactive({report: [], correct: [], monitor: []});
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs().subtract(1, 'day'),
+  endTime: dayjs()
+});
+
+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 powerMapOf = (list) => {
+  const map = new Map();
+  (list || []).forEach((item) => map.set(item.time, Number(item.power)));
+  return map;
+};
+
+const tableRows = computed(() => {
+  const times = new Set();
+  [seriesData.report, seriesData.correct, seriesData.monitor].forEach((list) => {
+    list.forEach((item) => times.add(item.time));
+  });
+  const reportMap = powerMapOf(seriesData.report);
+  const correctMap = powerMapOf(seriesData.correct);
+  const monitorMap = powerMapOf(seriesData.monitor);
+  return Array.from(times).sort().map((time) => ({
+    time,
+    reportPower: reportMap.has(time) ? reportMap.get(time) : 0,
+    correctPower: correctMap.has(time) ? correctMap.get(time) : 0,
+    monitorPower: monitorMap.has(time) ? monitorMap.get(time) : 0
+  }));
+});
+
+const chartAxis = computed(() => tableRows.value.map((item) => item.time));
+const chartSeries = computed(() => [
+  {name: '上报功率', color: '#1677ff', data: tableRows.value.map((item) => item.reportPower)},
+  {name: '修正功率', color: '#faad14', data: tableRows.value.map((item) => item.correctPower)},
+  {name: '观测功率', color: '#52c41a', data: tableRows.value.map((item) => item.monitorPower)}
+]);
+
+const columns = [
+  {title: '时间', dataIndex: 'time'},
+  {title: '上报功率', dataIndex: 'reportPower'},
+  {title: '修正功率', dataIndex: 'correctPower'},
+  {title: '观测功率', dataIndex: 'monitorPower'},
+  {
+    title: '操作',
+    key: 'action',
+    width: 90,
+    customRender: ({record}) => h(AButton, {type: 'link', size: 'small', onClick: () => openCorrectModal(record)}, '修正')
+  }
+];
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  stationOptions.value = toStationOptions(normalizeRows(res));
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value;
+  }
+};
+
+const buildParams = () => ({
+  stationId: query.stationId,
+  forecastStartTime: dayjs(query.startTime).unix(),
+  forecastEndTime: dayjs(query.endTime).unix()
+});
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) {
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await chartDataMediumTermPowerForecastCorrect(buildParams());
+    const body = (res && res.data) || {};
+    seriesData.report = body.report || [];
+    seriesData.correct = body.correct || [];
+    seriesData.monitor = body.monitor || [];
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) {
+    return;
+  }
+  exporting.value = true;
+  try {
+    const res = await exportDataMediumTermPowerForecastCorrect(buildParams());
+    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;
+  }
+};
+
+// 事件1: 系数调整
+const multiplierVisible = ref(false);
+const multiplierSaving = ref(false);
+const multiplierForm = reactive({multiplier: null});
+
+const openMultiplierModal = () => {
+  multiplierForm.multiplier = null;
+  multiplierVisible.value = true;
+};
+
+const handleMultiplierConfirm = async () => {
+  if (multiplierForm.multiplier === null || multiplierForm.multiplier === undefined) {
+    message.warning('请输入调整系数');
+    return;
+  }
+  multiplierSaving.value = true;
+  try {
+    await multiplierUpdateDataMediumTermPowerForecastCorrect({...buildParams(), multiplier: multiplierForm.multiplier});
+    message.success('系数调整成功');
+    multiplierVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    multiplierSaving.value = false;
+  }
+};
+
+// 事件2: 单条修正
+const correctVisible = ref(false);
+const correctSaving = ref(false);
+const correctForm = reactive({row: null, power: null});
+
+const openCorrectModal = (record) => {
+  correctForm.row = record;
+  correctForm.power = record.correctPower;
+  correctVisible.value = true;
+};
+
+const handleCorrectConfirm = async () => {
+  if (correctForm.power === null || correctForm.power === undefined) {
+    message.warning('请输入修正功率');
+    return;
+  }
+  correctSaving.value = true;
+  try {
+    await singlePointUpdateDataMediumTermPowerForecastCorrect({
+      entityId: String(query.stationId),
+      forecastTime: dayjs(correctForm.row.time).unix(),
+      power: correctForm.power
+    });
+    message.success('功率修正成功');
+    correctVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    correctSaving.value = false;
+  }
+};
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.data-medium-term-power-forecast-correct-page { padding: 16px; }
+.full-control { width: 100%; }
+.content-card { margin-top: 16px; }
+.detail-card { margin-top: 16px; }
+.modal-tip { margin-top: 8px; color: #8c8c8c; font-size: 12px; }
+.correct-power-form { margin-top: 16px; }
+</style>

+ 172 - 29
forecast-backend-vue/src/views/ufo/electricity/forecast/DataShortTermPowerForecastCorrect.vue

@@ -2,10 +2,10 @@
   <div class="data-short-term-power-forecast-correct-page">
     <a-card :bordered="false">
       <a-row :gutter="[12, 12]" align="middle">
-        <a-col :xs="24" :md="6">
+        <a-col :xs="24" :md="5">
           <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
         </a-col>
-        <a-col :xs="24" :md="5">
+        <a-col :xs="24" :md="4">
           <a-date-picker
             v-model:value="query.startTime"
             show-time
@@ -14,7 +14,7 @@
             class="full-control"
           />
         </a-col>
-        <a-col :xs="24" :md="5">
+        <a-col :xs="24" :md="4">
           <a-date-picker
             v-model:value="query.endTime"
             show-time
@@ -23,33 +23,89 @@
             class="full-control"
           />
         </a-col>
-        <a-col :xs="24" :md="8">
+        <a-col :xs="24" :md="11">
           <a-space>
             <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :disabled="!queryValid" @click="openMultiplierModal">系数调整</a-button>
             <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
           </a-space>
         </a-col>
       </a-row>
     </a-card>
 
-    <a-card :bordered="false" class="content-card">
+    <a-card title="功率修正曲线图" :bordered="false" class="content-card">
       <PowerLineChart :x-axis="chartAxis" :series="chartSeries" height="360px" y-axis-name="kW" />
     </a-card>
 
     <a-card title="数据详情" :bordered="false" class="detail-card">
-      <BusinessDataTable :columns="columns" :data-source="tableRows" :pagination="{pageSize: 10}" :scroll-x="'max-content'" />
+      <BusinessDataTable
+        :columns="columns"
+        :data-source="tableRows"
+        row-key="time"
+        :pagination="{pageSize: 10}"
+        :scroll-x="'max-content'"
+      />
     </a-card>
+
+    <a-modal
+      v-model:open="multiplierVisible"
+      title="系数调整"
+      :confirm-loading="multiplierSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleMultiplierConfirm"
+    >
+      <a-form layout="vertical">
+        <a-form-item label="调整系数">
+          <a-input-number
+            v-model:value="multiplierForm.multiplier"
+            :min="0.01"
+            :max="100"
+            :step="0.01"
+            :precision="2"
+            placeholder="0.01 - 100.00"
+            class="full-control"
+          />
+        </a-form-item>
+      </a-form>
+      <div class="modal-tip">以当前窗口内上报数据为基准,按所选系数批量生成/更新修正数据(修正功率 = 上报功率 × 系数)。</div>
+    </a-modal>
+
+    <a-modal
+      v-model:open="correctVisible"
+      title="功率修正"
+      :confirm-loading="correctSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleCorrectConfirm"
+    >
+      <a-descriptions :column="1" size="small" bordered>
+        <a-descriptions-item label="时间">{{ correctForm.row ? correctForm.row.time : '' }}</a-descriptions-item>
+        <a-descriptions-item label="上报功率 (kW)">{{ correctForm.row ? correctForm.row.reportPower : '' }}</a-descriptions-item>
+        <a-descriptions-item label="观测功率 (kW)">{{ correctForm.row ? correctForm.row.monitorPower : '' }}</a-descriptions-item>
+      </a-descriptions>
+      <a-form layout="vertical" class="correct-power-form">
+        <a-form-item label="修正功率 (kW)">
+          <a-input-number v-model:value="correctForm.power" :min="0" :step="0.1" :precision="2" class="full-control" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
   </div>
 </template>
 
 <script setup name="DataShortTermPowerForecastCorrect">
-import {computed, onMounted, reactive, ref} from 'vue';
-import {message} from 'ant-design-vue';
+import {computed, h, onMounted, reactive, ref} from 'vue';
+import {Button as AButton, 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 {chartDataShortTermPowerForecastCorrect, exportDataShortTermPowerForecastCorrect} from '@/api/power/dataShortTermPowerForecastCorrect';
+import {
+  chartDataShortTermPowerForecastCorrect,
+  exportDataShortTermPowerForecastCorrect,
+  multiplierUpdateDataShortTermPowerForecastCorrect,
+  singlePointUpdateDataShortTermPowerForecastCorrect
+} from '@/api/power/dataShortTermPowerForecastCorrect';
 import BusinessDataTable from '@/views/ufo/electricity/components/BusinessDataTable.vue';
 import PowerLineChart from '@/views/ufo/electricity/components/PowerLineChart.vue';
 import StationSelector from '@/views/ufo/electricity/components/StationSelector.vue';
@@ -59,19 +115,15 @@ import {toStationOptions} from '@/views/ufo/electricity/components/stationAdapte
 const loading = ref(false);
 const exporting = ref(false);
 const stationOptions = ref([]);
-const tableRows = ref([]);
+
+const seriesData = reactive({report: [], correct: [], monitor: []});
 
 const query = reactive({
   stationId: undefined,
   startTime: dayjs().subtract(1, 'hour'),
-  endTime: dayjs(),
+  endTime: dayjs()
 });
 
-const columns = [
-  {title: '日期', dataIndex: 'time'},
-  {title: '预测功率', dataIndex: 'power'}
-];
-
 const queryValid = computed(() => {
   if (!query.startTime || !query.endTime) {
     return false;
@@ -84,22 +136,47 @@ const queryValid = computed(() => {
   return !end.isBefore(start);
 });
 
+const powerMapOf = (list) => {
+  const map = new Map();
+  (list || []).forEach((item) => map.set(item.time, Number(item.power)));
+  return map;
+};
+
+const tableRows = computed(() => {
+  const times = new Set();
+  [seriesData.report, seriesData.correct, seriesData.monitor].forEach((list) => {
+    list.forEach((item) => times.add(item.time));
+  });
+  const reportMap = powerMapOf(seriesData.report);
+  const correctMap = powerMapOf(seriesData.correct);
+  const monitorMap = powerMapOf(seriesData.monitor);
+  return Array.from(times).sort().map((time) => ({
+    time,
+    reportPower: reportMap.has(time) ? reportMap.get(time) : 0,
+    correctPower: correctMap.has(time) ? correctMap.get(time) : 0,
+    monitorPower: monitorMap.has(time) ? monitorMap.get(time) : 0
+  }));
+});
+
 const chartAxis = computed(() => tableRows.value.map((item) => item.time));
 const chartSeries = computed(() => [
-  {
-    name: '预测功率',
-    color: '#1677ff',
-    data: tableRows.value.map((item) => Number(item.power))
-  }
+  {name: '上报功率', color: '#1677ff', data: tableRows.value.map((item) => item.reportPower)},
+  {name: '修正功率', color: '#faad14', data: tableRows.value.map((item) => item.correctPower)},
+  {name: '观测功率', color: '#52c41a', data: tableRows.value.map((item) => item.monitorPower)}
 ]);
 
-const formatRows = (rows) => {
-  return rows.map((item) => ({
-    ...item,
-    time: dayjs.unix(Number(item.forecastTime)).format('YYYY-MM-DD HH:mm:ss'),
-    power: Number(item.power)
-  }));
-};
+const columns = [
+  {title: '时间', dataIndex: 'time'},
+  {title: '上报功率', dataIndex: 'reportPower'},
+  {title: '修正功率', dataIndex: 'correctPower'},
+  {title: '观测功率', dataIndex: 'monitorPower'},
+  {
+    title: '操作',
+    key: 'action',
+    width: 90,
+    customRender: ({record}) => h(AButton, {type: 'link', size: 'small', onClick: () => openCorrectModal(record)}, '修正')
+  }
+];
 
 const loadStationOptions = async () => {
   const res = await listStation({pageNum: 1, pageSize: 500});
@@ -126,7 +203,10 @@ const handleQuery = async () => {
   loading.value = true;
   try {
     const res = await chartDataShortTermPowerForecastCorrect(buildParams());
-    tableRows.value = formatRows(res.rows || []);
+    const body = (res && res.data) || {};
+    seriesData.report = body.report || [];
+    seriesData.correct = body.correct || [];
+    seriesData.monitor = body.monitor || [];
   } finally {
     loading.value = false;
   }
@@ -164,6 +244,67 @@ const handleExport = async () => {
   }
 };
 
+// 事件1: 系数调整
+const multiplierVisible = ref(false);
+const multiplierSaving = ref(false);
+const multiplierForm = reactive({multiplier: null});
+
+const openMultiplierModal = () => {
+  multiplierForm.multiplier = null;
+  multiplierVisible.value = true;
+};
+
+const handleMultiplierConfirm = async () => {
+  if (multiplierForm.multiplier === null || multiplierForm.multiplier === undefined) {
+    message.warning('请输入调整系数');
+    return;
+  }
+  multiplierSaving.value = true;
+  try {
+    await multiplierUpdateDataShortTermPowerForecastCorrect({...buildParams(), multiplier: multiplierForm.multiplier});
+    message.success('系数调整成功');
+    multiplierVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    multiplierSaving.value = false;
+  }
+};
+
+// 事件2: 单条修正
+const correctVisible = ref(false);
+const correctSaving = ref(false);
+const correctForm = reactive({row: null, power: null});
+
+const openCorrectModal = (record) => {
+  correctForm.row = record;
+  correctForm.power = record.correctPower;
+  correctVisible.value = true;
+};
+
+const handleCorrectConfirm = async () => {
+  if (correctForm.power === null || correctForm.power === undefined) {
+    message.warning('请输入修正功率');
+    return;
+  }
+  correctSaving.value = true;
+  try {
+    await singlePointUpdateDataShortTermPowerForecastCorrect({
+      entityId: String(query.stationId),
+      forecastTime: dayjs(correctForm.row.time).unix(),
+      power: correctForm.power
+    });
+    message.success('功率修正成功');
+    correctVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    correctSaving.value = false;
+  }
+};
+
 onMounted(async () => {
   await loadStationOptions();
   await handleQuery();
@@ -175,4 +316,6 @@ onMounted(async () => {
 .full-control { width: 100%; }
 .content-card { margin-top: 16px; }
 .detail-card { margin-top: 16px; }
+.modal-tip { margin-top: 8px; color: #8c8c8c; font-size: 12px; }
+.correct-power-form { margin-top: 16px; }
 </style>

+ 172 - 29
forecast-backend-vue/src/views/ufo/electricity/forecast/DataSuperShortTermForecastCorrect.vue

@@ -2,10 +2,10 @@
   <div class="data-super-short-term-forecast-correct-page">
     <a-card :bordered="false">
       <a-row :gutter="[12, 12]" align="middle">
-        <a-col :xs="24" :md="6">
+        <a-col :xs="24" :md="5">
           <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
         </a-col>
-        <a-col :xs="24" :md="5">
+        <a-col :xs="24" :md="4">
           <a-date-picker
             v-model:value="query.startTime"
             show-time
@@ -14,7 +14,7 @@
             class="full-control"
           />
         </a-col>
-        <a-col :xs="24" :md="5">
+        <a-col :xs="24" :md="4">
           <a-date-picker
             v-model:value="query.endTime"
             show-time
@@ -23,33 +23,89 @@
             class="full-control"
           />
         </a-col>
-        <a-col :xs="24" :md="8">
+        <a-col :xs="24" :md="11">
           <a-space>
             <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :disabled="!queryValid" @click="openMultiplierModal">系数调整</a-button>
             <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
           </a-space>
         </a-col>
       </a-row>
     </a-card>
 
-    <a-card :bordered="false" class="content-card">
+    <a-card title="功率修正曲线图" :bordered="false" class="content-card">
       <PowerLineChart :x-axis="chartAxis" :series="chartSeries" height="360px" y-axis-name="kW" />
     </a-card>
 
     <a-card title="数据详情" :bordered="false" class="detail-card">
-      <BusinessDataTable :columns="columns" :data-source="tableRows" :pagination="{pageSize: 10}" :scroll-x="'max-content'" />
+      <BusinessDataTable
+        :columns="columns"
+        :data-source="tableRows"
+        row-key="time"
+        :pagination="{pageSize: 10}"
+        :scroll-x="'max-content'"
+      />
     </a-card>
+
+    <a-modal
+      v-model:open="multiplierVisible"
+      title="系数调整"
+      :confirm-loading="multiplierSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleMultiplierConfirm"
+    >
+      <a-form layout="vertical">
+        <a-form-item label="调整系数">
+          <a-input-number
+            v-model:value="multiplierForm.multiplier"
+            :min="0.01"
+            :max="100"
+            :step="0.01"
+            :precision="2"
+            placeholder="0.01 - 100.00"
+            class="full-control"
+          />
+        </a-form-item>
+      </a-form>
+      <div class="modal-tip">以当前窗口内上报数据为基准,按所选系数批量生成/更新修正数据(修正功率 = 上报功率 × 系数)。</div>
+    </a-modal>
+
+    <a-modal
+      v-model:open="correctVisible"
+      title="功率修正"
+      :confirm-loading="correctSaving"
+      ok-text="确定"
+      cancel-text="取消"
+      @ok="handleCorrectConfirm"
+    >
+      <a-descriptions :column="1" size="small" bordered>
+        <a-descriptions-item label="时间">{{ correctForm.row ? correctForm.row.time : '' }}</a-descriptions-item>
+        <a-descriptions-item label="上报功率 (kW)">{{ correctForm.row ? correctForm.row.reportPower : '' }}</a-descriptions-item>
+        <a-descriptions-item label="观测功率 (kW)">{{ correctForm.row ? correctForm.row.monitorPower : '' }}</a-descriptions-item>
+      </a-descriptions>
+      <a-form layout="vertical" class="correct-power-form">
+        <a-form-item label="修正功率 (kW)">
+          <a-input-number v-model:value="correctForm.power" :min="0" :step="0.1" :precision="2" class="full-control" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
   </div>
 </template>
 
 <script setup name="DataSuperShortTermForecastCorrect">
-import {computed, onMounted, reactive, ref} from 'vue';
-import {message} from 'ant-design-vue';
+import {computed, h, onMounted, reactive, ref} from 'vue';
+import {Button as AButton, 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 {chartDataSuperShortTermForecastCorrect, exportDataSuperShortTermForecastCorrect} from '@/api/power/dataSuperShortTermForecastCorrect';
+import {
+  chartDataSuperShortTermForecastCorrect,
+  exportDataSuperShortTermForecastCorrect,
+  multiplierUpdateDataSuperShortTermForecastCorrect,
+  singlePointUpdateDataSuperShortTermForecastCorrect
+} from '@/api/power/dataSuperShortTermForecastCorrect';
 import BusinessDataTable from '@/views/ufo/electricity/components/BusinessDataTable.vue';
 import PowerLineChart from '@/views/ufo/electricity/components/PowerLineChart.vue';
 import StationSelector from '@/views/ufo/electricity/components/StationSelector.vue';
@@ -59,19 +115,15 @@ import {toStationOptions} from '@/views/ufo/electricity/components/stationAdapte
 const loading = ref(false);
 const exporting = ref(false);
 const stationOptions = ref([]);
-const tableRows = ref([]);
+
+const seriesData = reactive({report: [], correct: [], monitor: []});
 
 const query = reactive({
   stationId: undefined,
   startTime: dayjs().subtract(1, 'hour'),
-  endTime: dayjs(),
+  endTime: dayjs()
 });
 
-const columns = [
-  {title: '日期', dataIndex: 'time'},
-  {title: '预测功率', dataIndex: 'power'}
-];
-
 const queryValid = computed(() => {
   if (!query.startTime || !query.endTime) {
     return false;
@@ -84,22 +136,47 @@ const queryValid = computed(() => {
   return !end.isBefore(start);
 });
 
+const powerMapOf = (list) => {
+  const map = new Map();
+  (list || []).forEach((item) => map.set(item.time, Number(item.power)));
+  return map;
+};
+
+const tableRows = computed(() => {
+  const times = new Set();
+  [seriesData.report, seriesData.correct, seriesData.monitor].forEach((list) => {
+    list.forEach((item) => times.add(item.time));
+  });
+  const reportMap = powerMapOf(seriesData.report);
+  const correctMap = powerMapOf(seriesData.correct);
+  const monitorMap = powerMapOf(seriesData.monitor);
+  return Array.from(times).sort().map((time) => ({
+    time,
+    reportPower: reportMap.has(time) ? reportMap.get(time) : 0,
+    correctPower: correctMap.has(time) ? correctMap.get(time) : 0,
+    monitorPower: monitorMap.has(time) ? monitorMap.get(time) : 0
+  }));
+});
+
 const chartAxis = computed(() => tableRows.value.map((item) => item.time));
 const chartSeries = computed(() => [
-  {
-    name: '预测功率',
-    color: '#1677ff',
-    data: tableRows.value.map((item) => Number(item.power))
-  }
+  {name: '上报功率', color: '#1677ff', data: tableRows.value.map((item) => item.reportPower)},
+  {name: '修正功率', color: '#faad14', data: tableRows.value.map((item) => item.correctPower)},
+  {name: '观测功率', color: '#52c41a', data: tableRows.value.map((item) => item.monitorPower)}
 ]);
 
-const formatRows = (rows) => {
-  return rows.map((item) => ({
-    ...item,
-    time: dayjs.unix(Number(item.forecastTime)).format('YYYY-MM-DD HH:mm:ss'),
-    power: Number(item.power)
-  }));
-};
+const columns = [
+  {title: '时间', dataIndex: 'time'},
+  {title: '上报功率', dataIndex: 'reportPower'},
+  {title: '修正功率', dataIndex: 'correctPower'},
+  {title: '观测功率', dataIndex: 'monitorPower'},
+  {
+    title: '操作',
+    key: 'action',
+    width: 90,
+    customRender: ({record}) => h(AButton, {type: 'link', size: 'small', onClick: () => openCorrectModal(record)}, '修正')
+  }
+];
 
 const loadStationOptions = async () => {
   const res = await listStation({pageNum: 1, pageSize: 500});
@@ -126,7 +203,10 @@ const handleQuery = async () => {
   loading.value = true;
   try {
     const res = await chartDataSuperShortTermForecastCorrect(buildParams());
-    tableRows.value = formatRows(res.rows || []);
+    const body = (res && res.data) || {};
+    seriesData.report = body.report || [];
+    seriesData.correct = body.correct || [];
+    seriesData.monitor = body.monitor || [];
   } finally {
     loading.value = false;
   }
@@ -164,6 +244,67 @@ const handleExport = async () => {
   }
 };
 
+// 事件1: 系数调整
+const multiplierVisible = ref(false);
+const multiplierSaving = ref(false);
+const multiplierForm = reactive({multiplier: null});
+
+const openMultiplierModal = () => {
+  multiplierForm.multiplier = null;
+  multiplierVisible.value = true;
+};
+
+const handleMultiplierConfirm = async () => {
+  if (multiplierForm.multiplier === null || multiplierForm.multiplier === undefined) {
+    message.warning('请输入调整系数');
+    return;
+  }
+  multiplierSaving.value = true;
+  try {
+    await multiplierUpdateDataSuperShortTermForecastCorrect({...buildParams(), multiplier: multiplierForm.multiplier});
+    message.success('系数调整成功');
+    multiplierVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    multiplierSaving.value = false;
+  }
+};
+
+// 事件2: 单条修正
+const correctVisible = ref(false);
+const correctSaving = ref(false);
+const correctForm = reactive({row: null, power: null});
+
+const openCorrectModal = (record) => {
+  correctForm.row = record;
+  correctForm.power = record.correctPower;
+  correctVisible.value = true;
+};
+
+const handleCorrectConfirm = async () => {
+  if (correctForm.power === null || correctForm.power === undefined) {
+    message.warning('请输入修正功率');
+    return;
+  }
+  correctSaving.value = true;
+  try {
+    await singlePointUpdateDataSuperShortTermForecastCorrect({
+      entityId: String(query.stationId),
+      forecastTime: dayjs(correctForm.row.time).unix(),
+      power: correctForm.power
+    });
+    message.success('功率修正成功');
+    correctVisible.value = false;
+    await handleQuery();
+  } catch (e) {
+    // 请求拦截器已提示错误
+  } finally {
+    correctSaving.value = false;
+  }
+};
+
 onMounted(async () => {
   await loadStationOptions();
   await handleQuery();
@@ -175,4 +316,6 @@ onMounted(async () => {
 .full-control { width: 100%; }
 .content-card { margin-top: 16px; }
 .detail-card { margin-top: 16px; }
+.modal-tip { margin-top: 8px; color: #8c8c8c; font-size: 12px; }
+.correct-power-form { margin-top: 16px; }
 </style>

+ 13 - 0
release_note/20261021/20261021_dp.sql

@@ -0,0 +1,13 @@
+-- 中期功率预测修正菜单
+INSERT INTO "public"."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"
+)
+SELECT
+    2087, '中期功率预测修正', 2007, 70, 'mediumTermPowerForecastCorrect', 'ufo/electricity/forecast/DataMediumTermPowerForecastCorrect', NULL,
+    'DataMediumTermPowerForecastCorrect', 1, 0, 'C', '0', '0', 'mediumterm:powerForecastCorrect:list',
+    'chart', 'admin', '2026-10-21 00:00:00', '', NULL, ''
+WHERE NOT EXISTS (
+    SELECT 1 FROM sys_menu WHERE menu_name = '中期功率预测修正'
+);

+ 12 - 0
release_note/20261021/release_note.md

@@ -0,0 +1,12 @@
+20261021版本更新说明
+
+1. 功率预测修正功能完善:超短期/短期/中期功率修正页面图表展示上报、修正、观测3条曲线
+2. 增加菜单“中期功率预测修正”及页面 DataMediumTermPowerForecastCorrect
+3. 修正页面增加“系数调整”功能:以窗口内上报数据为基准,按系数(0.01-100.00)批量生成/更新修正数据
+4. 修正页面数据详情增加“操作-修正”列,支持单条修正(上报/观测只读展示,修正功率可编辑)
+5. 修正任务书表名笔误:短期使用短期表(data_short_term_*)、中期使用中期表(data_medium_term_*),按明显本意实现
+6. 单条修正接口 /update 请求体参数采用驼峰 forecastTime
+7. 修正表插入时 create_time 改为数据库端按当前时间生成
+
+DB补丁
+release_note/20261021/20261021_dp.sql