Просмотр исходного кода

Merge branch 'master' of http://36.141.74.62:4888/yeguanglin/forecast

xiaoguohua 2 месяцев назад
Родитель
Сommit
c8c54b5635
19 измененных файлов с 1200 добавлено и 170 удалено
  1. 91 25
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/controller/DataShortTermWeatherForecastRawController.java
  2. 46 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/ChartDataPair.java
  3. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/PsfcChartStat.java
  4. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/RhsfcChartStat.java
  5. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/TirrChartStat.java
  6. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/TsfcChartStat.java
  7. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/WsChartStat.java
  8. 18 5
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/service/impl/DataShortTermWeatherForecastRawServiceImpl.java
  9. 153 0
      forecast-backend-server/src/main/java/com/ufo/project/weather/domain/DataWeatherLocal.java
  10. 78 0
      forecast-backend-server/src/main/java/com/ufo/project/weather/mapper/DataWeatherLocalMapper.java
  11. 118 0
      forecast-backend-server/src/main/java/com/ufo/project/weather/service/IDataWeatherLocalService.java
  12. 297 0
      forecast-backend-server/src/main/java/com/ufo/project/weather/service/impl/DataWeatherLocalServiceImpl.java
  13. 1 0
      forecast-backend-server/src/main/resources/mybatis/weather/DataShortTermWeatherForecastRawMapper.xml
  14. 108 0
      forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml
  15. 44 28
      forecast-backend-vue/src/views/ufo/electricity/weather/HumidityCurve.vue
  16. 44 28
      forecast-backend-vue/src/views/ufo/electricity/weather/IrradianceCurve.vue
  17. 44 28
      forecast-backend-vue/src/views/ufo/electricity/weather/PressureCurve.vue
  18. 44 28
      forecast-backend-vue/src/views/ufo/electricity/weather/TemperatureCurve.vue
  19. 44 28
      forecast-backend-vue/src/views/ufo/electricity/weather/WindSpeedCurve.vue

+ 91 - 25
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/controller/DataShortTermWeatherForecastRawController.java

@@ -1,5 +1,6 @@
 package com.ufo.project.forecast.shortterm.weather.controller;
 
+import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
 import com.ufo.base.framework.aspectj.lang.annotation.Log;
 import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
 import com.ufo.project.forecast.shortterm.weather.domain.DataShortTermWeatherForecastRaw;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.ChartDataPair;
 import com.ufo.project.forecast.shortterm.weather.domain.vo.PsfcChartResult;
 import com.ufo.project.forecast.shortterm.weather.domain.vo.PsfcChartStat;
 import com.ufo.project.forecast.shortterm.weather.domain.vo.RhsfcChartResult;
@@ -29,6 +31,7 @@ import com.ufo.project.forecast.shortterm.weather.domain.vo.TsfcChartStat;
 import com.ufo.project.forecast.shortterm.weather.domain.vo.WsChartResult;
 import com.ufo.project.forecast.shortterm.weather.domain.vo.WsChartStat;
 import com.ufo.project.forecast.shortterm.weather.service.IDataShortTermWeatherForecastRawService;
+import com.ufo.project.weather.service.IDataWeatherLocalService;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.common.utils.poi.ExcelUtil;
@@ -47,6 +50,9 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     @Autowired
     private IDataShortTermWeatherForecastRawService dataShortTermWeatherForecastRawService;
 
+    @Autowired
+    private IDataWeatherLocalService dataWeatherLocalService;
+
     /**
      * 查询短期气象原始数据列表
      */
@@ -85,18 +91,20 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     }
 
     /**
-     * 统计风速曲线数据
+     * 统计风速曲线数据(短期预报 + 本地实测)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:wschart')")
     @GetMapping("/wschart")
     public AjaxResult wschart(@RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        return success(dataShortTermWeatherForecastRawService.getWsChart(stationId, startTime, endTime));
+        WsChartResult forecast = dataShortTermWeatherForecastRawService.getWsChart(stationId, startTime, endTime);
+        WsChartResult local = dataWeatherLocalService.getWsChart(stationId, startTime, endTime);
+        return success(new ChartDataPair<WsChartResult>(forecast, local));
     }
 
     /**
-     * 导出风速曲线数据
+     * 导出风速曲线数据(短期预报 + 本地实测,合并导出)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:wschart')")
     @Log(title = "风速曲线", businessType = BusinessType.EXPORT)
@@ -104,24 +112,36 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     public void wschartExport(HttpServletResponse response, @RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        WsChartResult result = dataShortTermWeatherForecastRawService.getWsChart(stationId, startTime, endTime);
+        WsChartResult forecast = dataShortTermWeatherForecastRawService.getWsChart(stationId, startTime, endTime);
+        WsChartResult local = dataWeatherLocalService.getWsChart(stationId, startTime, endTime);
+        List<WsChartStat> stats = new ArrayList<WsChartStat>();
+        if (forecast.getStats() != null)
+        {
+            stats.addAll(forecast.getStats());
+        }
+        if (local.getStats() != null)
+        {
+            stats.addAll(local.getStats());
+        }
         ExcelUtil<WsChartStat> util = new ExcelUtil<WsChartStat>(WsChartStat.class);
-        util.exportExcel(response, result.getStats(), "风速曲线数据");
+        util.exportExcel(response, stats, "风速曲线数据");
     }
 
     /**
-     * 统计温度曲线数据
+     * 统计温度曲线数据(短期预报 + 本地实测)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:tsfcchart')")
     @GetMapping("/tsfcchart")
     public AjaxResult tsfcchart(@RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        return success(dataShortTermWeatherForecastRawService.getTsfcChart(stationId, startTime, endTime));
+        TsfcChartResult forecast = dataShortTermWeatherForecastRawService.getTsfcChart(stationId, startTime, endTime);
+        TsfcChartResult local = dataWeatherLocalService.getTsfcChart(stationId, startTime, endTime);
+        return success(new ChartDataPair<TsfcChartResult>(forecast, local));
     }
 
     /**
-     * 导出温度曲线数据
+     * 导出温度曲线数据(短期预报 + 本地实测,合并导出)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:tsfcchart')")
     @Log(title = "温度曲线", businessType = BusinessType.EXPORT)
@@ -129,24 +149,36 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     public void tsfcchartExport(HttpServletResponse response, @RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        TsfcChartResult result = dataShortTermWeatherForecastRawService.getTsfcChart(stationId, startTime, endTime);
+        TsfcChartResult forecast = dataShortTermWeatherForecastRawService.getTsfcChart(stationId, startTime, endTime);
+        TsfcChartResult local = dataWeatherLocalService.getTsfcChart(stationId, startTime, endTime);
+        List<TsfcChartStat> stats = new ArrayList<TsfcChartStat>();
+        if (forecast.getStats() != null)
+        {
+            stats.addAll(forecast.getStats());
+        }
+        if (local.getStats() != null)
+        {
+            stats.addAll(local.getStats());
+        }
         ExcelUtil<TsfcChartStat> util = new ExcelUtil<TsfcChartStat>(TsfcChartStat.class);
-        util.exportExcel(response, result.getStats(), "温度曲线数据");
+        util.exportExcel(response, stats, "温度曲线数据");
     }
 
     /**
-     * 统计湿度曲线数据
+     * 统计湿度曲线数据(短期预报 + 本地实测)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:rhsfcchart')")
     @GetMapping("/rhsfcchart")
     public AjaxResult rhsfcchart(@RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        return success(dataShortTermWeatherForecastRawService.getRhsfcChart(stationId, startTime, endTime));
+        RhsfcChartResult forecast = dataShortTermWeatherForecastRawService.getRhsfcChart(stationId, startTime, endTime);
+        RhsfcChartResult local = dataWeatherLocalService.getRhsfcChart(stationId, startTime, endTime);
+        return success(new ChartDataPair<RhsfcChartResult>(forecast, local));
     }
 
     /**
-     * 导出湿度曲线数据
+     * 导出湿度曲线数据(短期预报 + 本地实测,合并导出)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:rhsfcchart')")
     @Log(title = "湿度曲线", businessType = BusinessType.EXPORT)
@@ -154,24 +186,36 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     public void rhsfcchartExport(HttpServletResponse response, @RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        RhsfcChartResult result = dataShortTermWeatherForecastRawService.getRhsfcChart(stationId, startTime, endTime);
+        RhsfcChartResult forecast = dataShortTermWeatherForecastRawService.getRhsfcChart(stationId, startTime, endTime);
+        RhsfcChartResult local = dataWeatherLocalService.getRhsfcChart(stationId, startTime, endTime);
+        List<RhsfcChartStat> stats = new ArrayList<RhsfcChartStat>();
+        if (forecast.getStats() != null)
+        {
+            stats.addAll(forecast.getStats());
+        }
+        if (local.getStats() != null)
+        {
+            stats.addAll(local.getStats());
+        }
         ExcelUtil<RhsfcChartStat> util = new ExcelUtil<RhsfcChartStat>(RhsfcChartStat.class);
-        util.exportExcel(response, result.getStats(), "湿度曲线数据");
+        util.exportExcel(response, stats, "湿度曲线数据");
     }
 
     /**
-     * 统计气压曲线数据
+     * 统计气压曲线数据(短期预报 + 本地实测)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:psfcchart')")
     @GetMapping("/psfcchart")
     public AjaxResult psfcchart(@RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        return success(dataShortTermWeatherForecastRawService.getPsfcChart(stationId, startTime, endTime));
+        PsfcChartResult forecast = dataShortTermWeatherForecastRawService.getPsfcChart(stationId, startTime, endTime);
+        PsfcChartResult local = dataWeatherLocalService.getPsfcChart(stationId, startTime, endTime);
+        return success(new ChartDataPair<PsfcChartResult>(forecast, local));
     }
 
     /**
-     * 导出气压曲线数据
+     * 导出气压曲线数据(短期预报 + 本地实测,合并导出)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:psfcchart')")
     @Log(title = "气压曲线", businessType = BusinessType.EXPORT)
@@ -179,24 +223,36 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     public void psfcchartExport(HttpServletResponse response, @RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        PsfcChartResult result = dataShortTermWeatherForecastRawService.getPsfcChart(stationId, startTime, endTime);
+        PsfcChartResult forecast = dataShortTermWeatherForecastRawService.getPsfcChart(stationId, startTime, endTime);
+        PsfcChartResult local = dataWeatherLocalService.getPsfcChart(stationId, startTime, endTime);
+        List<PsfcChartStat> stats = new ArrayList<PsfcChartStat>();
+        if (forecast.getStats() != null)
+        {
+            stats.addAll(forecast.getStats());
+        }
+        if (local.getStats() != null)
+        {
+            stats.addAll(local.getStats());
+        }
         ExcelUtil<PsfcChartStat> util = new ExcelUtil<PsfcChartStat>(PsfcChartStat.class);
-        util.exportExcel(response, result.getStats(), "气压曲线数据");
+        util.exportExcel(response, stats, "气压曲线数据");
     }
 
     /**
-     * 统计辐照曲线数据
+     * 统计辐照曲线数据(短期预报 + 本地实测)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:tirrchart')")
     @GetMapping("/tirrchart")
     public AjaxResult tirrchart(@RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        return success(dataShortTermWeatherForecastRawService.getTirrChart(stationId, startTime, endTime));
+        TirrChartResult forecast = dataShortTermWeatherForecastRawService.getTirrChart(stationId, startTime, endTime);
+        TirrChartResult local = dataWeatherLocalService.getTirrChart(stationId, startTime, endTime);
+        return success(new ChartDataPair<TirrChartResult>(forecast, local));
     }
 
     /**
-     * 导出辐照曲线数据
+     * 导出辐照曲线数据(短期预报 + 本地实测,合并导出)
      */
     @PreAuthorize("@ss.hasPermi('weather:weatherRaw:tirrchart')")
     @Log(title = "辐照曲线", businessType = BusinessType.EXPORT)
@@ -204,9 +260,19 @@ public class DataShortTermWeatherForecastRawController extends BaseController
     public void tirrchartExport(HttpServletResponse response, @RequestParam("stationId") Long stationId,
             @RequestParam("startTime") Long startTime, @RequestParam("endTime") Long endTime)
     {
-        TirrChartResult result = dataShortTermWeatherForecastRawService.getTirrChart(stationId, startTime, endTime);
+        TirrChartResult forecast = dataShortTermWeatherForecastRawService.getTirrChart(stationId, startTime, endTime);
+        TirrChartResult local = dataWeatherLocalService.getTirrChart(stationId, startTime, endTime);
+        List<TirrChartStat> stats = new ArrayList<TirrChartStat>();
+        if (forecast.getStats() != null)
+        {
+            stats.addAll(forecast.getStats());
+        }
+        if (local.getStats() != null)
+        {
+            stats.addAll(local.getStats());
+        }
         ExcelUtil<TirrChartStat> util = new ExcelUtil<TirrChartStat>(TirrChartStat.class);
-        util.exportExcel(response, result.getStats(), "辐照曲线数据");
+        util.exportExcel(response, stats, "辐照曲线数据");
     }
 
     /**

+ 46 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/ChartDataPair.java

@@ -0,0 +1,46 @@
+package com.ufo.project.forecast.shortterm.weather.domain.vo;
+
+/**
+ * 短期预报数据与本地实测数据组合返回体
+ *
+ * @author ufo
+ * @date 2026-08-05
+ */
+public class ChartDataPair<T>
+{
+    /** 短期预报数据(data_short_term_weather_forecast_raw) */
+    private T forecast;
+
+    /** 本地实测数据(data_weather_local) */
+    private T local;
+
+    public ChartDataPair()
+    {
+    }
+
+    public ChartDataPair(T forecast, T local)
+    {
+        this.forecast = forecast;
+        this.local = local;
+    }
+
+    public T getForecast()
+    {
+        return forecast;
+    }
+
+    public void setForecast(T forecast)
+    {
+        this.forecast = forecast;
+    }
+
+    public T getLocal()
+    {
+        return local;
+    }
+
+    public void setLocal(T local)
+    {
+        this.local = local;
+    }
+}

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/PsfcChartStat.java

@@ -18,6 +18,10 @@ public class PsfcChartStat
     @Excel(name = "气压(hPa)")
     private Double psfc;
 
+    /** 数据来源(预测/本地实测) */
+    @Excel(name = "来源")
+    private String source;
+
     public PsfcChartStat()
     {
     }
@@ -47,4 +51,14 @@ public class PsfcChartStat
     {
         this.psfc = psfc;
     }
+
+    public String getSource()
+    {
+        return source;
+    }
+
+    public void setSource(String source)
+    {
+        this.source = source;
+    }
 }

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/RhsfcChartStat.java

@@ -18,6 +18,10 @@ public class RhsfcChartStat
     @Excel(name = "湿度(%)")
     private Double rhsfc;
 
+    /** 数据来源(预测/本地实测) */
+    @Excel(name = "来源")
+    private String source;
+
     public RhsfcChartStat()
     {
     }
@@ -47,4 +51,14 @@ public class RhsfcChartStat
     {
         this.rhsfc = rhsfc;
     }
+
+    public String getSource()
+    {
+        return source;
+    }
+
+    public void setSource(String source)
+    {
+        this.source = source;
+    }
 }

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/TirrChartStat.java

@@ -18,6 +18,10 @@ public class TirrChartStat
     @Excel(name = "辐照(W/m²)")
     private Double tirr;
 
+    /** 数据来源(预测/本地实测) */
+    @Excel(name = "来源")
+    private String source;
+
     public TirrChartStat()
     {
     }
@@ -47,4 +51,14 @@ public class TirrChartStat
     {
         this.tirr = tirr;
     }
+
+    public String getSource()
+    {
+        return source;
+    }
+
+    public void setSource(String source)
+    {
+        this.source = source;
+    }
 }

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/TsfcChartStat.java

@@ -18,6 +18,10 @@ public class TsfcChartStat
     @Excel(name = "温度(°C)")
     private Double tsfc;
 
+    /** 数据来源(预测/本地实测) */
+    @Excel(name = "来源")
+    private String source;
+
     public TsfcChartStat()
     {
     }
@@ -47,4 +51,14 @@ public class TsfcChartStat
     {
         this.tsfc = tsfc;
     }
+
+    public String getSource()
+    {
+        return source;
+    }
+
+    public void setSource(String source)
+    {
+        this.source = source;
+    }
 }

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/domain/vo/WsChartStat.java

@@ -18,6 +18,10 @@ public class WsChartStat
     @Excel(name = "风速(m/s)")
     private Double ws;
 
+    /** 数据来源(预测/本地实测) */
+    @Excel(name = "来源")
+    private String source;
+
     public WsChartStat()
     {
     }
@@ -47,4 +51,14 @@ public class WsChartStat
     {
         this.ws = ws;
     }
+
+    public String getSource()
+    {
+        return source;
+    }
+
+    public void setSource(String source)
+    {
+        this.source = source;
+    }
 }

+ 18 - 5
forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/weather/service/impl/DataShortTermWeatherForecastRawServiceImpl.java

@@ -37,6 +37,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
     @Autowired
     private DataShortTermWeatherForecastRawMapper dataShortTermWeatherForecastRawMapper;
 
+    /** 短期预报数据来源标识(用于曲线图导出区分) */
+    private static final String SOURCE_FORECAST = "预测";
+
     /** 16个风向方位名称 */
     private static final String[] DIRECTIONS = { "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
             "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" };
@@ -175,7 +178,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
             validRecords++;
             String time = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS,
                     new Date(record.getForecastTime() * 1000L));
-            stats.add(new WsChartStat(time, round2(ws)));
+            WsChartStat stat = new WsChartStat(time, round2(ws));
+            stat.setSource(SOURCE_FORECAST);
+            stats.add(stat);
         }
 
         WsChartResult result = new WsChartResult();
@@ -201,7 +206,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
         List<TsfcChartStat> stats = new ArrayList<TsfcChartStat>();
         for (Object[] point : cp.points)
         {
-            stats.add(new TsfcChartStat((String) point[0], (Double) point[1]));
+            TsfcChartStat stat = new TsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_FORECAST);
+            stats.add(stat);
         }
         TsfcChartResult result = new TsfcChartResult();
         result.setStationId(stationId);
@@ -226,7 +233,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
         List<RhsfcChartStat> stats = new ArrayList<RhsfcChartStat>();
         for (Object[] point : cp.points)
         {
-            stats.add(new RhsfcChartStat((String) point[0], (Double) point[1]));
+            RhsfcChartStat stat = new RhsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_FORECAST);
+            stats.add(stat);
         }
         RhsfcChartResult result = new RhsfcChartResult();
         result.setStationId(stationId);
@@ -251,7 +260,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
         List<PsfcChartStat> stats = new ArrayList<PsfcChartStat>();
         for (Object[] point : cp.points)
         {
-            stats.add(new PsfcChartStat((String) point[0], (Double) point[1]));
+            PsfcChartStat stat = new PsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_FORECAST);
+            stats.add(stat);
         }
         PsfcChartResult result = new PsfcChartResult();
         result.setStationId(stationId);
@@ -276,7 +287,9 @@ public class DataShortTermWeatherForecastRawServiceImpl implements IDataShortTer
         List<TirrChartStat> stats = new ArrayList<TirrChartStat>();
         for (Object[] point : cp.points)
         {
-            stats.add(new TirrChartStat((String) point[0], (Double) point[1]));
+            TirrChartStat stat = new TirrChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_FORECAST);
+            stats.add(stat);
         }
         TirrChartResult result = new TirrChartResult();
         result.setStationId(stationId);

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

@@ -0,0 +1,153 @@
+package com.ufo.project.weather.domain;
+
+import java.util.Date;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+import com.ufo.base.framework.web.domain.BaseEntity;
+
+/**
+ * 本地气象采样数据对象 data_weather_local
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public class DataWeatherLocal extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 时间戳(带时区) */
+    @Excel(name = "时间戳", readConverterExp = "带=时区")
+    private Date tm;
+
+    /** 设备ID */
+    @Excel(name = "场站id")
+    private Long configStationId;
+
+    /** 水平面总辐照度,单位 W/m² */
+    @Excel(name = "水平面总辐照度,单位 W/m²")
+    private Double horizontalIrradiance;
+
+    /** 斜面辐照度,单位 W/m² */
+    @Excel(name = "斜面辐照度,单位 W/m²")
+    private Double inclinedIrradiance;
+
+    /** 温度,单位 ℃ */
+    @Excel(name = "温度,单位 ℃")
+    private Double temperature;
+
+    /** 气压,单位 hPa */
+    @Excel(name = "气压,单位 hPa")
+    private Double pressure;
+
+    /** 湿度,单位 % */
+    @Excel(name = "湿度,单位 %")
+    private Double humidity;
+
+    /** 风速,单位 m/s */
+    @Excel(name = "风速,单位 m/s")
+    private Double windSpeed;
+
+    /** 风向,北向为0,顺时针,单位 °(0~360) */
+    @Excel(name = "风向,北向为0,顺时针,单位 °", readConverterExp = "0=~360")
+    private Double windDirection;
+
+    public void setTm(Date tm) 
+    {
+        this.tm = tm;
+    }
+
+    public Date getTm() 
+    {
+        return tm;
+    }
+    public void setConfigStationId(Long configStationId)
+    {
+        this.configStationId = configStationId;
+    }
+
+    public Long getConfigStationId()
+    {
+        return configStationId;
+    }
+    public void setHorizontalIrradiance(Double horizontalIrradiance) 
+    {
+        this.horizontalIrradiance = horizontalIrradiance;
+    }
+
+    public Double getHorizontalIrradiance() 
+    {
+        return horizontalIrradiance;
+    }
+    public void setInclinedIrradiance(Double inclinedIrradiance) 
+    {
+        this.inclinedIrradiance = inclinedIrradiance;
+    }
+
+    public Double getInclinedIrradiance() 
+    {
+        return inclinedIrradiance;
+    }
+    public void setTemperature(Double temperature) 
+    {
+        this.temperature = temperature;
+    }
+
+    public Double getTemperature() 
+    {
+        return temperature;
+    }
+    public void setPressure(Double pressure) 
+    {
+        this.pressure = pressure;
+    }
+
+    public Double getPressure() 
+    {
+        return pressure;
+    }
+    public void setHumidity(Double humidity) 
+    {
+        this.humidity = humidity;
+    }
+
+    public Double getHumidity() 
+    {
+        return humidity;
+    }
+    public void setWindSpeed(Double windSpeed) 
+    {
+        this.windSpeed = windSpeed;
+    }
+
+    public Double getWindSpeed() 
+    {
+        return windSpeed;
+    }
+    public void setWindDirection(Double windDirection) 
+    {
+        this.windDirection = windDirection;
+    }
+
+    public Double getWindDirection() 
+    {
+        return windDirection;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("tm", getTm())
+            .append("deviceId", getConfigStationId())
+            .append("horizontalIrradiance", getHorizontalIrradiance())
+            .append("inclinedIrradiance", getInclinedIrradiance())
+            .append("temperature", getTemperature())
+            .append("pressure", getPressure())
+            .append("humidity", getHumidity())
+            .append("windSpeed", getWindSpeed())
+            .append("windDirection", getWindDirection())
+            .toString();
+    }
+}

+ 78 - 0
forecast-backend-server/src/main/java/com/ufo/project/weather/mapper/DataWeatherLocalMapper.java

@@ -0,0 +1,78 @@
+package com.ufo.project.weather.mapper;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+
+import com.ufo.project.weather.domain.DataWeatherLocal;
+
+/**
+ * 本地气象采样数据Mapper接口
+ * 
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public interface DataWeatherLocalMapper 
+{
+    /**
+     * 查询本地气象采样数据
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 本地气象采样数据
+     */
+    public DataWeatherLocal selectDataWeatherLocalByTm(Date tm);
+
+    /**
+     * 查询本地气象采样数据列表
+     *
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 本地气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectDataWeatherLocalList(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 查询本地气象采样数据列表(按时间范围,北京时间墙钟过滤)
+     * tm 与 startTime/endTime 均转换为 Asia/Shanghai 墙钟时间后比较,
+     * 即按北京日历日期过滤。
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 本地气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectDataWeatherLocalListByTimeRange(@Param("stationId") Long stationId,
+            @Param("startTime") Long startTime, @Param("endTime") Long endTime);
+
+    /**
+     * 新增本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    public int insertDataWeatherLocal(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 修改本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    public int updateDataWeatherLocal(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 删除本地气象采样数据
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 结果
+     */
+    public int deleteDataWeatherLocalByTm(Date tm);
+
+    /**
+     * 批量删除本地气象采样数据
+     * 
+     * @param tms 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteDataWeatherLocalByTms(Date[] tms);
+}

+ 118 - 0
forecast-backend-server/src/main/java/com/ufo/project/weather/service/IDataWeatherLocalService.java

@@ -0,0 +1,118 @@
+package com.ufo.project.weather.service;
+
+import java.util.Date;
+import java.util.List;
+
+import com.ufo.project.forecast.shortterm.weather.domain.vo.PsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.RhsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TirrChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.WsChartResult;
+import com.ufo.project.weather.domain.DataWeatherLocal;
+
+/**
+ * 本地气象采样数据Service接口
+ *
+ * @author oleauto
+ * @date 2026-06-24
+ */
+public interface IDataWeatherLocalService
+{
+    /**
+     * 查询本地气象采样数据
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 本地气象采样数据
+     */
+    public DataWeatherLocal selectDataWeatherLocalByTm(Date tm);
+
+    /**
+     * 查询本地气象采样数据列表
+     *
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 本地气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectDataWeatherLocalList(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 统计场站指定时间范围内的风速曲线数据(数据来源 data_weather_local.wind_speed)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 风速曲线统计结果
+     */
+    public WsChartResult getWsChart(Long stationId, Long startTime, Long endTime);
+
+    /**
+     * 统计场站指定时间范围内的温度曲线数据(数据来源 data_weather_local.temperature)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 温度曲线统计结果
+     */
+    public TsfcChartResult getTsfcChart(Long stationId, Long startTime, Long endTime);
+
+    /**
+     * 统计场站指定时间范围内的湿度曲线数据(数据来源 data_weather_local.humidity)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 湿度曲线统计结果
+     */
+    public RhsfcChartResult getRhsfcChart(Long stationId, Long startTime, Long endTime);
+
+    /**
+     * 统计场站指定时间范围内的气压曲线数据(数据来源 data_weather_local.pressure)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 气压曲线统计结果
+     */
+    public PsfcChartResult getPsfcChart(Long stationId, Long startTime, Long endTime);
+
+    /**
+     * 统计场站指定时间范围内的辐照曲线数据(数据来源 data_weather_local.horizontal_irradiance)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 辐照曲线统计结果
+     */
+    public TirrChartResult getTirrChart(Long stationId, Long startTime, Long endTime);
+
+    /**
+     * 新增本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    public int insertDataWeatherLocal(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 修改本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    public int updateDataWeatherLocal(DataWeatherLocal dataWeatherLocal);
+
+    /**
+     * 批量删除本地气象采样数据
+     * 
+     * @param tms 需要删除的本地气象采样数据主键集合
+     * @return 结果
+     */
+    public int deleteDataWeatherLocalByTms(Date[] tms);
+
+    /**
+     * 删除本地气象采样数据信息
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 结果
+     */
+    public int deleteDataWeatherLocalByTm(Date tm);
+}

+ 297 - 0
forecast-backend-server/src/main/java/com/ufo/project/weather/service/impl/DataWeatherLocalServiceImpl.java

@@ -0,0 +1,297 @@
+package com.ufo.project.weather.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+import com.ufo.base.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.PsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.PsfcChartStat;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.RhsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.RhsfcChartStat;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TirrChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TirrChartStat;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TsfcChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.TsfcChartStat;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.WsChartResult;
+import com.ufo.project.forecast.shortterm.weather.domain.vo.WsChartStat;
+import com.ufo.project.weather.domain.DataWeatherLocal;
+import com.ufo.project.weather.mapper.DataWeatherLocalMapper;
+import com.ufo.project.weather.service.IDataWeatherLocalService;
+
+/**
+ * 本地气象采样数据Service业务层处理
+ *
+ * @author oleauto
+ * @date 2026-06-24
+ */
+@Service
+public class DataWeatherLocalServiceImpl implements IDataWeatherLocalService
+{
+    @Autowired
+    private DataWeatherLocalMapper dataWeatherLocalMapper;
+
+    /** 本地实测数据来源标识(用于曲线图导出区分) */
+    private static final String SOURCE_LOCAL = "本地实测";
+
+    /**
+     * 查询本地气象采样数据
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 本地气象采样数据
+     */
+    @Override
+    public DataWeatherLocal selectDataWeatherLocalByTm(Date tm)
+    {
+        return dataWeatherLocalMapper.selectDataWeatherLocalByTm(tm);
+    }
+
+    /**
+     * 查询本地气象采样数据列表
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 本地气象采样数据
+     */
+    @Override
+    public List<DataWeatherLocal> selectDataWeatherLocalList(DataWeatherLocal dataWeatherLocal)
+    {
+        return dataWeatherLocalMapper.selectDataWeatherLocalList(dataWeatherLocal);
+    }
+
+    /**
+     * 统计场站指定时间范围内的风速曲线数据(数据来源 data_weather_local.wind_speed)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 风速曲线统计结果
+     */
+    @Override
+    public WsChartResult getWsChart(Long stationId, Long startTime, Long endTime)
+    {
+        LocalCurvePoints cp = buildCurvePoints(stationId, startTime, endTime, DataWeatherLocal::getWindSpeed);
+        List<WsChartStat> stats = new ArrayList<WsChartStat>();
+        for (Object[] point : cp.points)
+        {
+            WsChartStat stat = new WsChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_LOCAL);
+            stats.add(stat);
+        }
+        WsChartResult result = new WsChartResult();
+        result.setStationId(stationId);
+        result.setTotalRecords(cp.total);
+        result.setValidRecords(cp.valid);
+        result.setStats(stats);
+        return result;
+    }
+
+    /**
+     * 统计场站指定时间范围内的温度曲线数据(数据来源 data_weather_local.temperature)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 温度曲线统计结果
+     */
+    @Override
+    public TsfcChartResult getTsfcChart(Long stationId, Long startTime, Long endTime)
+    {
+        LocalCurvePoints cp = buildCurvePoints(stationId, startTime, endTime, DataWeatherLocal::getTemperature);
+        List<TsfcChartStat> stats = new ArrayList<TsfcChartStat>();
+        for (Object[] point : cp.points)
+        {
+            TsfcChartStat stat = new TsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_LOCAL);
+            stats.add(stat);
+        }
+        TsfcChartResult result = new TsfcChartResult();
+        result.setStationId(stationId);
+        result.setTotalRecords(cp.total);
+        result.setValidRecords(cp.valid);
+        result.setStats(stats);
+        return result;
+    }
+
+    /**
+     * 统计场站指定时间范围内的湿度曲线数据(数据来源 data_weather_local.humidity)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 湿度曲线统计结果
+     */
+    @Override
+    public RhsfcChartResult getRhsfcChart(Long stationId, Long startTime, Long endTime)
+    {
+        LocalCurvePoints cp = buildCurvePoints(stationId, startTime, endTime, DataWeatherLocal::getHumidity);
+        List<RhsfcChartStat> stats = new ArrayList<RhsfcChartStat>();
+        for (Object[] point : cp.points)
+        {
+            RhsfcChartStat stat = new RhsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_LOCAL);
+            stats.add(stat);
+        }
+        RhsfcChartResult result = new RhsfcChartResult();
+        result.setStationId(stationId);
+        result.setTotalRecords(cp.total);
+        result.setValidRecords(cp.valid);
+        result.setStats(stats);
+        return result;
+    }
+
+    /**
+     * 统计场站指定时间范围内的气压曲线数据(数据来源 data_weather_local.pressure)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 气压曲线统计结果
+     */
+    @Override
+    public PsfcChartResult getPsfcChart(Long stationId, Long startTime, Long endTime)
+    {
+        LocalCurvePoints cp = buildCurvePoints(stationId, startTime, endTime, DataWeatherLocal::getPressure);
+        List<PsfcChartStat> stats = new ArrayList<PsfcChartStat>();
+        for (Object[] point : cp.points)
+        {
+            PsfcChartStat stat = new PsfcChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_LOCAL);
+            stats.add(stat);
+        }
+        PsfcChartResult result = new PsfcChartResult();
+        result.setStationId(stationId);
+        result.setTotalRecords(cp.total);
+        result.setValidRecords(cp.valid);
+        result.setStats(stats);
+        return result;
+    }
+
+    /**
+     * 统计场站指定时间范围内的辐照曲线数据(数据来源 data_weather_local.horizontal_irradiance)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(unix秒)
+     * @param endTime 结束时间(unix秒)
+     * @return 辐照曲线统计结果
+     */
+    @Override
+    public TirrChartResult getTirrChart(Long stationId, Long startTime, Long endTime)
+    {
+        LocalCurvePoints cp = buildCurvePoints(stationId, startTime, endTime, DataWeatherLocal::getHorizontalIrradiance);
+        List<TirrChartStat> stats = new ArrayList<TirrChartStat>();
+        for (Object[] point : cp.points)
+        {
+            TirrChartStat stat = new TirrChartStat((String) point[0], (Double) point[1]);
+            stat.setSource(SOURCE_LOCAL);
+            stats.add(stat);
+        }
+        TirrChartResult result = new TirrChartResult();
+        result.setStationId(stationId);
+        result.setTotalRecords(cp.total);
+        result.setValidRecords(cp.valid);
+        result.setStats(stats);
+        return result;
+    }
+
+    /**
+     * 通用曲线数据构建:查询、清洗并格式化时间
+     * 过滤取值为空、时间为空的记录
+     */
+    private LocalCurvePoints buildCurvePoints(Long stationId, Long startTime, Long endTime,
+            Function<DataWeatherLocal, Double> valueGetter)
+    {
+        List<DataWeatherLocal> list =
+                dataWeatherLocalMapper.selectDataWeatherLocalListByTimeRange(stationId, startTime, endTime);
+        List<Object[]> points = new ArrayList<Object[]>();
+        int valid = 0;
+        for (DataWeatherLocal record : list)
+        {
+            Double value = valueGetter.apply(record);
+            if (value == null || record.getTm() == null)
+            {
+                continue;
+            }
+            valid++;
+            String time = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, record.getTm());
+            points.add(new Object[] { time, round2(value) });
+        }
+        return new LocalCurvePoints(list.size(), valid, points);
+    }
+
+    /**
+     * 曲线数据构建结果
+     */
+    private static class LocalCurvePoints
+    {
+        private final int total;
+        private final int valid;
+        private final List<Object[]> points;
+
+        private LocalCurvePoints(int total, int valid, List<Object[]> points)
+        {
+            this.total = total;
+            this.valid = valid;
+            this.points = points;
+        }
+    }
+
+    /**
+     * 保留两位小数(四舍五入)
+     */
+    private double round2(double value)
+    {
+        return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    /**
+     * 新增本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    @Override
+    public int insertDataWeatherLocal(DataWeatherLocal dataWeatherLocal)
+    {
+        return dataWeatherLocalMapper.insertDataWeatherLocal(dataWeatherLocal);
+    }
+
+    /**
+     * 修改本地气象采样数据
+     * 
+     * @param dataWeatherLocal 本地气象采样数据
+     * @return 结果
+     */
+    @Override
+    public int updateDataWeatherLocal(DataWeatherLocal dataWeatherLocal)
+    {
+        return dataWeatherLocalMapper.updateDataWeatherLocal(dataWeatherLocal);
+    }
+
+    /**
+     * 批量删除本地气象采样数据
+     * 
+     * @param tms 需要删除的本地气象采样数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDataWeatherLocalByTms(Date[] tms)
+    {
+        return dataWeatherLocalMapper.deleteDataWeatherLocalByTms(tms);
+    }
+
+    /**
+     * 删除本地气象采样数据信息
+     * 
+     * @param tm 本地气象采样数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDataWeatherLocalByTm(Date tm)
+    {
+        return dataWeatherLocalMapper.deleteDataWeatherLocalByTm(tm);
+    }
+}

+ 1 - 0
forecast-backend-server/src/main/resources/mybatis/weather/DataShortTermWeatherForecastRawMapper.xml

@@ -150,6 +150,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where station_id = #{stationId}
           and forecast_time &gt;= #{startTime}
           and forecast_time &lt;= #{endTime}
+        order by forecast_time asc
     </select>
 
     <select id="selectDataShortTermWeatherForecastRawById" parameterType="Long" resultMap="DataShortTermWeatherForecastRawResult">

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

@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ufo.project.weather.mapper.DataWeatherLocalMapper">
+    
+    <resultMap type="DataWeatherLocal" id="DataWeatherLocalResult">
+        <result property="tm"    column="tm"    />
+        <result property="configStationId"    column="config_station_id"    />
+        <result property="horizontalIrradiance"    column="horizontal_irradiance"    />
+        <result property="inclinedIrradiance"    column="inclined_irradiance"    />
+        <result property="temperature"    column="temperature"    />
+        <result property="pressure"    column="pressure"    />
+        <result property="humidity"    column="humidity"    />
+        <result property="windSpeed"    column="wind_speed"    />
+        <result property="windDirection"    column="wind_direction"    />
+    </resultMap>
+
+    <sql id="selectDataWeatherLocalVo">
+        select tm, config_station_id, horizontal_irradiance, inclined_irradiance, temperature, pressure, humidity, wind_speed, wind_direction from data_weather_local
+    </sql>
+
+    <select id="selectDataWeatherLocalList" parameterType="DataWeatherLocal" resultMap="DataWeatherLocalResult">
+        <include refid="selectDataWeatherLocalVo"/>
+        <where>
+            <if test="tm != null "> and tm = #{tm}</if>
+            <if test="configStationId != null "> and config_station_id = #{configStationId}</if>
+            <if test="horizontalIrradiance != null "> and horizontal_irradiance = #{horizontalIrradiance}</if>
+            <if test="inclinedIrradiance != null "> and inclined_irradiance = #{inclinedIrradiance}</if>
+            <if test="temperature != null "> and temperature = #{temperature}</if>
+            <if test="pressure != null "> and pressure = #{pressure}</if>
+            <if test="humidity != null "> and humidity = #{humidity}</if>
+            <if test="windSpeed != null "> and wind_speed = #{windSpeed}</if>
+            <if test="windDirection != null "> and wind_direction = #{windDirection}</if>
+        </where>
+    </select>
+
+    <!--
+        tm 为 TIMESTAMPTZ,插入时已按各自来源时区归一化为 UTC 时刻存储,
+        因此与 startTime/endTime(unix秒,绝对时刻)直接按时刻比较即可,
+        不依赖任何固定时区(如 GMT+8)。to_timestamp(double precision) 以
+        UTC 为基准,与会话时区无关。
+    -->
+    <select id="selectDataWeatherLocalListByTimeRange" resultMap="DataWeatherLocalResult">
+        <include refid="selectDataWeatherLocalVo"/>
+        where config_station_id = #{stationId}
+          and tm &gt;= to_timestamp(#{startTime})
+          and tm &lt;= to_timestamp(#{endTime})
+        order by tm asc
+    </select>
+
+    <select id="selectDataWeatherLocalByTm" parameterType="Date" resultMap="DataWeatherLocalResult">
+        <include refid="selectDataWeatherLocalVo"/>
+        where tm = #{tm}
+    </select>
+        
+    <insert id="insertDataWeatherLocal" parameterType="DataWeatherLocal">
+        insert into data_weather_local
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="tm != null">tm,</if>
+            <if test="configStationId != null">config_station_id,</if>
+            <if test="horizontalIrradiance != null">horizontal_irradiance,</if>
+            <if test="inclinedIrradiance != null">inclined_irradiance,</if>
+            <if test="temperature != null">temperature,</if>
+            <if test="pressure != null">pressure,</if>
+            <if test="humidity != null">humidity,</if>
+            <if test="windSpeed != null">wind_speed,</if>
+            <if test="windDirection != null">wind_direction,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="tm != null">#{tm},</if>
+            <if test="configStationId != null">#{configStationId},</if>
+            <if test="horizontalIrradiance != null">#{horizontalIrradiance},</if>
+            <if test="inclinedIrradiance != null">#{inclinedIrradiance},</if>
+            <if test="temperature != null">#{temperature},</if>
+            <if test="pressure != null">#{pressure},</if>
+            <if test="humidity != null">#{humidity},</if>
+            <if test="windSpeed != null">#{windSpeed},</if>
+            <if test="windDirection != null">#{windDirection},</if>
+         </trim>
+    </insert>
+
+    <update id="updateDataWeatherLocal" parameterType="DataWeatherLocal">
+        update data_weather_local
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="configStationId != null">config_station_id = #{configStationId},</if>
+            <if test="horizontalIrradiance != null">horizontal_irradiance = #{horizontalIrradiance},</if>
+            <if test="inclinedIrradiance != null">inclined_irradiance = #{inclinedIrradiance},</if>
+            <if test="temperature != null">temperature = #{temperature},</if>
+            <if test="pressure != null">pressure = #{pressure},</if>
+            <if test="humidity != null">humidity = #{humidity},</if>
+            <if test="windSpeed != null">wind_speed = #{windSpeed},</if>
+            <if test="windDirection != null">wind_direction = #{windDirection},</if>
+        </trim>
+        where tm = #{tm}
+    </update>
+
+    <delete id="deleteDataWeatherLocalByTm" parameterType="Date">
+        delete from data_weather_local where tm = #{tm}
+    </delete>
+
+    <delete id="deleteDataWeatherLocalByTms" parameterType="String">
+        delete from data_weather_local where tm in 
+        <foreach item="tm" collection="array" open="(" separator="," close=")">
+            #{tm}
+        </foreach>
+    </delete>
+</mapper>

+ 44 - 28
forecast-backend-vue/src/views/ufo/electricity/weather/HumidityCurve.vue

@@ -34,8 +34,10 @@
 
     <a-card :bordered="false" class="detail-card" title="明细数据">
       <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
-        <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
-        <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(预测)">{{ summary.totalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(预测)">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</a-descriptions-item>
       </a-descriptions>
       <a-table
         :columns="columns"
@@ -78,10 +80,11 @@ const query = reactive({
   ]
 });
 
-const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
+const hasData = computed(() => !!summary.value && (summary.value.totalRecords > 0 || summary.value.localTotalRecords > 0));
 
 const columns = [
   {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
+  {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
   {title: '湿度(%)', dataIndex: 'rhsfc', width: 150, align: 'right'}
 ];
 
@@ -115,17 +118,20 @@ const loadStations = async () => {
   }
 };
 
-const buildOption = (stats) => {
-  const rows = (stats || []).map((item) => ({time: item.time, rhsfc: toNumber(item.rhsfc, 0)}));
+const buildOption = (forecastStats, localStats) => {
+  // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
+  // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
+  const toPairs = (stats, key) => (stats || [])
+    .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
+    .sort((a, b) => a[0] - b[0]);
   return {
     tooltip: {trigger: 'axis'},
-    legend: {top: 0, left: 'center', data: ['湿度']},
+    legend: {top: 0, left: 'center', data: ['预测', '本地']},
     grid: {left: 60, right: 24, top: 46, bottom: 60},
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: rows.map((item) => item.time),
-      axisLabel: {color: '#5a6a85'}
+      type: 'time',
+      axisLabel: {color: '#5a6a85'},
+      axisPointer: {snap: true}
     },
     yAxis: {
       type: 'value',
@@ -137,23 +143,20 @@ const buildOption = (stats) => {
       {type: 'inside', start: 0, end: 100},
       {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
     ],
-    series: [{
-      name: '湿度',
-      type: 'line',
-      smooth: true,
-      showSymbol: false,
-      data: rows.map((item) => item.rhsfc)
-    }]
+    series: [
+      {name: '预测', type: 'line', smooth: true, showSymbol: false, data: toPairs(forecastStats, 'rhsfc')},
+      {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'rhsfc')}
+    ]
   };
 };
 
-const renderChart = async (stats) => {
+const renderChart = async (forecastStats, localStats) => {
   await nextTick();
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
   }
-  chartInstance.setOption(buildOption(stats), true);
+  chartInstance.setOption(buildOption(forecastStats, localStats), true);
 };
 
 const resizeChart = () => {
@@ -177,19 +180,32 @@ const handleQuery = async () => {
     if (!data) {
       summary.value = null;
       detailRows.value = [];
-      renderChart([]);
+      renderChart([], []);
       return;
     }
+    const forecast = data.forecast || {};
+    const local = data.local || {};
     summary.value = {
-      totalRecords: data.totalRecords,
-      validRecords: data.validRecords
+      totalRecords: forecast.totalRecords || 0,
+      validRecords: forecast.validRecords || 0,
+      localTotalRecords: local.totalRecords || 0,
+      localValidRecords: local.validRecords || 0
     };
-    detailRows.value = (data.stats || []).map((item, index) => ({
-      key: index,
-      time: item.time,
-      rhsfc: toNumber(item.rhsfc, 0)
-    }));
-    renderChart(data.stats || []);
+    detailRows.value = [
+      ...(forecast.stats || []).map((item, index) => ({
+        key: `forecast-${index}`,
+        time: item.time,
+        source: item.source || '预测',
+        rhsfc: toNumber(item.rhsfc, 0)
+      })),
+      ...(local.stats || []).map((item, index) => ({
+        key: `local-${index}`,
+        time: item.time,
+        source: item.source || '本地',
+        rhsfc: toNumber(item.rhsfc, 0)
+      }))
+    ];
+    renderChart(forecast.stats || [], local.stats || []);
   } finally {
     loading.value = false;
   }

+ 44 - 28
forecast-backend-vue/src/views/ufo/electricity/weather/IrradianceCurve.vue

@@ -34,8 +34,10 @@
 
     <a-card :bordered="false" class="detail-card" title="明细数据">
       <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
-        <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
-        <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(预测)">{{ summary.totalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(预测)">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</a-descriptions-item>
       </a-descriptions>
       <a-table
         :columns="columns"
@@ -78,10 +80,11 @@ const query = reactive({
   ]
 });
 
-const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
+const hasData = computed(() => !!summary.value && (summary.value.totalRecords > 0 || summary.value.localTotalRecords > 0));
 
 const columns = [
   {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
+  {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
   {title: '辐照(W/m²)', dataIndex: 'tirr', width: 150, align: 'right'}
 ];
 
@@ -115,17 +118,20 @@ const loadStations = async () => {
   }
 };
 
-const buildOption = (stats) => {
-  const rows = (stats || []).map((item) => ({time: item.time, tirr: toNumber(item.tirr, 0)}));
+const buildOption = (forecastStats, localStats) => {
+  // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
+  // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
+  const toPairs = (stats, key) => (stats || [])
+    .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
+    .sort((a, b) => a[0] - b[0]);
   return {
     tooltip: {trigger: 'axis'},
-    legend: {top: 0, left: 'center', data: ['辐照']},
+    legend: {top: 0, left: 'center', data: ['预测', '本地']},
     grid: {left: 60, right: 24, top: 46, bottom: 60},
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: rows.map((item) => item.time),
-      axisLabel: {color: '#5a6a85'}
+      type: 'time',
+      axisLabel: {color: '#5a6a85'},
+      axisPointer: {snap: true}
     },
     yAxis: {
       type: 'value',
@@ -137,23 +143,20 @@ const buildOption = (stats) => {
       {type: 'inside', start: 0, end: 100},
       {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
     ],
-    series: [{
-      name: '辐照',
-      type: 'line',
-      smooth: true,
-      showSymbol: false,
-      data: rows.map((item) => item.tirr)
-    }]
+    series: [
+      {name: '预测', type: 'line', smooth: true, showSymbol: false, data: toPairs(forecastStats, 'tirr')},
+      {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'tirr')}
+    ]
   };
 };
 
-const renderChart = async (stats) => {
+const renderChart = async (forecastStats, localStats) => {
   await nextTick();
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
   }
-  chartInstance.setOption(buildOption(stats), true);
+  chartInstance.setOption(buildOption(forecastStats, localStats), true);
 };
 
 const resizeChart = () => {
@@ -177,19 +180,32 @@ const handleQuery = async () => {
     if (!data) {
       summary.value = null;
       detailRows.value = [];
-      renderChart([]);
+      renderChart([], []);
       return;
     }
+    const forecast = data.forecast || {};
+    const local = data.local || {};
     summary.value = {
-      totalRecords: data.totalRecords,
-      validRecords: data.validRecords
+      totalRecords: forecast.totalRecords || 0,
+      validRecords: forecast.validRecords || 0,
+      localTotalRecords: local.totalRecords || 0,
+      localValidRecords: local.validRecords || 0
     };
-    detailRows.value = (data.stats || []).map((item, index) => ({
-      key: index,
-      time: item.time,
-      tirr: toNumber(item.tirr, 0)
-    }));
-    renderChart(data.stats || []);
+    detailRows.value = [
+      ...(forecast.stats || []).map((item, index) => ({
+        key: `forecast-${index}`,
+        time: item.time,
+        source: item.source || '预测',
+        tirr: toNumber(item.tirr, 0)
+      })),
+      ...(local.stats || []).map((item, index) => ({
+        key: `local-${index}`,
+        time: item.time,
+        source: item.source || '本地',
+        tirr: toNumber(item.tirr, 0)
+      }))
+    ];
+    renderChart(forecast.stats || [], local.stats || []);
   } finally {
     loading.value = false;
   }

+ 44 - 28
forecast-backend-vue/src/views/ufo/electricity/weather/PressureCurve.vue

@@ -34,8 +34,10 @@
 
     <a-card :bordered="false" class="detail-card" title="明细数据">
       <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
-        <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
-        <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(预测)">{{ summary.totalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(预测)">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</a-descriptions-item>
       </a-descriptions>
       <a-table
         :columns="columns"
@@ -78,10 +80,11 @@ const query = reactive({
   ]
 });
 
-const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
+const hasData = computed(() => !!summary.value && (summary.value.totalRecords > 0 || summary.value.localTotalRecords > 0));
 
 const columns = [
   {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
+  {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
   {title: '气压(hPa)', dataIndex: 'psfc', width: 150, align: 'right'}
 ];
 
@@ -115,17 +118,20 @@ const loadStations = async () => {
   }
 };
 
-const buildOption = (stats) => {
-  const rows = (stats || []).map((item) => ({time: item.time, psfc: toNumber(item.psfc, 0)}));
+const buildOption = (forecastStats, localStats) => {
+  // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
+  // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
+  const toPairs = (stats, key) => (stats || [])
+    .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
+    .sort((a, b) => a[0] - b[0]);
   return {
     tooltip: {trigger: 'axis'},
-    legend: {top: 0, left: 'center', data: ['气压']},
+    legend: {top: 0, left: 'center', data: ['预测', '本地']},
     grid: {left: 60, right: 24, top: 46, bottom: 60},
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: rows.map((item) => item.time),
-      axisLabel: {color: '#5a6a85'}
+      type: 'time',
+      axisLabel: {color: '#5a6a85'},
+      axisPointer: {snap: true}
     },
     yAxis: {
       type: 'value',
@@ -137,23 +143,20 @@ const buildOption = (stats) => {
       {type: 'inside', start: 0, end: 100},
       {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
     ],
-    series: [{
-      name: '气压',
-      type: 'line',
-      smooth: true,
-      showSymbol: false,
-      data: rows.map((item) => item.psfc)
-    }]
+    series: [
+      {name: '预测', type: 'line', smooth: true, showSymbol: false, data: toPairs(forecastStats, 'psfc')},
+      {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'psfc')}
+    ]
   };
 };
 
-const renderChart = async (stats) => {
+const renderChart = async (forecastStats, localStats) => {
   await nextTick();
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
   }
-  chartInstance.setOption(buildOption(stats), true);
+  chartInstance.setOption(buildOption(forecastStats, localStats), true);
 };
 
 const resizeChart = () => {
@@ -177,19 +180,32 @@ const handleQuery = async () => {
     if (!data) {
       summary.value = null;
       detailRows.value = [];
-      renderChart([]);
+      renderChart([], []);
       return;
     }
+    const forecast = data.forecast || {};
+    const local = data.local || {};
     summary.value = {
-      totalRecords: data.totalRecords,
-      validRecords: data.validRecords
+      totalRecords: forecast.totalRecords || 0,
+      validRecords: forecast.validRecords || 0,
+      localTotalRecords: local.totalRecords || 0,
+      localValidRecords: local.validRecords || 0
     };
-    detailRows.value = (data.stats || []).map((item, index) => ({
-      key: index,
-      time: item.time,
-      psfc: toNumber(item.psfc, 0)
-    }));
-    renderChart(data.stats || []);
+    detailRows.value = [
+      ...(forecast.stats || []).map((item, index) => ({
+        key: `forecast-${index}`,
+        time: item.time,
+        source: item.source || '预测',
+        psfc: toNumber(item.psfc, 0)
+      })),
+      ...(local.stats || []).map((item, index) => ({
+        key: `local-${index}`,
+        time: item.time,
+        source: item.source || '本地',
+        psfc: toNumber(item.psfc, 0)
+      }))
+    ];
+    renderChart(forecast.stats || [], local.stats || []);
   } finally {
     loading.value = false;
   }

+ 44 - 28
forecast-backend-vue/src/views/ufo/electricity/weather/TemperatureCurve.vue

@@ -34,8 +34,10 @@
 
     <a-card :bordered="false" class="detail-card" title="明细数据">
       <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
-        <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
-        <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(预测)">{{ summary.totalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(预测)">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</a-descriptions-item>
       </a-descriptions>
       <a-table
         :columns="columns"
@@ -78,10 +80,11 @@ const query = reactive({
   ]
 });
 
-const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
+const hasData = computed(() => !!summary.value && (summary.value.totalRecords > 0 || summary.value.localTotalRecords > 0));
 
 const columns = [
   {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
+  {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
   {title: '温度(°C)', dataIndex: 'tsfc', width: 150, align: 'right'}
 ];
 
@@ -115,17 +118,20 @@ const loadStations = async () => {
   }
 };
 
-const buildOption = (stats) => {
-  const rows = (stats || []).map((item) => ({time: item.time, tsfc: toNumber(item.tsfc, 0)}));
+const buildOption = (forecastStats, localStats) => {
+  // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
+  // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
+  const toPairs = (stats, key) => (stats || [])
+    .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
+    .sort((a, b) => a[0] - b[0]);
   return {
     tooltip: {trigger: 'axis'},
-    legend: {top: 0, left: 'center', data: ['温度']},
+    legend: {top: 0, left: 'center', data: ['预测', '本地']},
     grid: {left: 60, right: 24, top: 46, bottom: 60},
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: rows.map((item) => item.time),
-      axisLabel: {color: '#5a6a85'}
+      type: 'time',
+      axisLabel: {color: '#5a6a85'},
+      axisPointer: {snap: true}
     },
     yAxis: {
       type: 'value',
@@ -137,23 +143,20 @@ const buildOption = (stats) => {
       {type: 'inside', start: 0, end: 100},
       {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
     ],
-    series: [{
-      name: '温度',
-      type: 'line',
-      smooth: true,
-      showSymbol: false,
-      data: rows.map((item) => item.tsfc)
-    }]
+    series: [
+      {name: '预测', type: 'line', smooth: true, showSymbol: false, data: toPairs(forecastStats, 'tsfc')},
+      {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'tsfc')}
+    ]
   };
 };
 
-const renderChart = async (stats) => {
+const renderChart = async (forecastStats, localStats) => {
   await nextTick();
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
   }
-  chartInstance.setOption(buildOption(stats), true);
+  chartInstance.setOption(buildOption(forecastStats, localStats), true);
 };
 
 const resizeChart = () => {
@@ -177,19 +180,32 @@ const handleQuery = async () => {
     if (!data) {
       summary.value = null;
       detailRows.value = [];
-      renderChart([]);
+      renderChart([], []);
       return;
     }
+    const forecast = data.forecast || {};
+    const local = data.local || {};
     summary.value = {
-      totalRecords: data.totalRecords,
-      validRecords: data.validRecords
+      totalRecords: forecast.totalRecords || 0,
+      validRecords: forecast.validRecords || 0,
+      localTotalRecords: local.totalRecords || 0,
+      localValidRecords: local.validRecords || 0
     };
-    detailRows.value = (data.stats || []).map((item, index) => ({
-      key: index,
-      time: item.time,
-      tsfc: toNumber(item.tsfc, 0)
-    }));
-    renderChart(data.stats || []);
+    detailRows.value = [
+      ...(forecast.stats || []).map((item, index) => ({
+        key: `forecast-${index}`,
+        time: item.time,
+        source: item.source || '预测',
+        tsfc: toNumber(item.tsfc, 0)
+      })),
+      ...(local.stats || []).map((item, index) => ({
+        key: `local-${index}`,
+        time: item.time,
+        source: item.source || '本地',
+        tsfc: toNumber(item.tsfc, 0)
+      }))
+    ];
+    renderChart(forecast.stats || [], local.stats || []);
   } finally {
     loading.value = false;
   }

+ 44 - 28
forecast-backend-vue/src/views/ufo/electricity/weather/WindSpeedCurve.vue

@@ -34,8 +34,10 @@
 
     <a-card :bordered="false" class="detail-card" title="明细数据">
       <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
-        <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
-        <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(预测)">{{ summary.totalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(预测)">{{ summary.validRecords }}</a-descriptions-item>
+        <a-descriptions-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
+        <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</a-descriptions-item>
       </a-descriptions>
       <a-table
         :columns="columns"
@@ -78,10 +80,11 @@ const query = reactive({
   ]
 });
 
-const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
+const hasData = computed(() => !!summary.value && (summary.value.totalRecords > 0 || summary.value.localTotalRecords > 0));
 
 const columns = [
   {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
+  {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
   {title: '风速(m/s)', dataIndex: 'ws', width: 150, align: 'right'}
 ];
 
@@ -115,17 +118,20 @@ const loadStations = async () => {
   }
 };
 
-const buildOption = (stats) => {
-  const rows = (stats || []).map((item) => ({time: item.time, ws: toNumber(item.ws, 0)}));
+const buildOption = (forecastStats, localStats) => {
+  // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
+  // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
+  const toPairs = (stats, key) => (stats || [])
+    .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
+    .sort((a, b) => a[0] - b[0]);
   return {
     tooltip: {trigger: 'axis'},
-    legend: {top: 0, left: 'center', data: ['风速']},
+    legend: {top: 0, left: 'center', data: ['预测', '本地']},
     grid: {left: 60, right: 24, top: 46, bottom: 60},
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: rows.map((item) => item.time),
-      axisLabel: {color: '#5a6a85'}
+      type: 'time',
+      axisLabel: {color: '#5a6a85'},
+      axisPointer: {snap: true}
     },
     yAxis: {
       type: 'value',
@@ -137,23 +143,20 @@ const buildOption = (stats) => {
       {type: 'inside', start: 0, end: 100},
       {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
     ],
-    series: [{
-      name: '风速',
-      type: 'line',
-      smooth: true,
-      showSymbol: false,
-      data: rows.map((item) => item.ws)
-    }]
+    series: [
+      {name: '预测', type: 'line', smooth: true, showSymbol: false, data: toPairs(forecastStats, 'ws')},
+      {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'ws')}
+    ]
   };
 };
 
-const renderChart = async (stats) => {
+const renderChart = async (forecastStats, localStats) => {
   await nextTick();
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
   }
-  chartInstance.setOption(buildOption(stats), true);
+  chartInstance.setOption(buildOption(forecastStats, localStats), true);
 };
 
 const resizeChart = () => {
@@ -177,19 +180,32 @@ const handleQuery = async () => {
     if (!data) {
       summary.value = null;
       detailRows.value = [];
-      renderChart([]);
+      renderChart([], []);
       return;
     }
+    const forecast = data.forecast || {};
+    const local = data.local || {};
     summary.value = {
-      totalRecords: data.totalRecords,
-      validRecords: data.validRecords
+      totalRecords: forecast.totalRecords || 0,
+      validRecords: forecast.validRecords || 0,
+      localTotalRecords: local.totalRecords || 0,
+      localValidRecords: local.validRecords || 0
     };
-    detailRows.value = (data.stats || []).map((item, index) => ({
-      key: index,
-      time: item.time,
-      ws: toNumber(item.ws, 0)
-    }));
-    renderChart(data.stats || []);
+    detailRows.value = [
+      ...(forecast.stats || []).map((item, index) => ({
+        key: `forecast-${index}`,
+        time: item.time,
+        source: item.source || '预测',
+        ws: toNumber(item.ws, 0)
+      })),
+      ...(local.stats || []).map((item, index) => ({
+        key: `local-${index}`,
+        time: item.time,
+        source: item.source || '本地',
+        ws: toNumber(item.ws, 0)
+      }))
+    ];
+    renderChart(forecast.stats || [], local.stats || []);
   } finally {
     loading.value = false;
   }