Browse Source

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

xiaoguohua 2 months ago
parent
commit
ee56253d64
56 changed files with 3870 additions and 211 deletions
  1. 6 0
      forecast-backend-server/pom.xml
  2. 86 0
      forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigChannelController.java
  3. 151 0
      forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigDeviceController.java
  4. 30 0
      forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigPointController.java
  5. 161 0
      forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigChannel.java
  6. 187 0
      forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigDevice.java
  7. 14 0
      forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigPoint.java
  8. 27 0
      forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigChannelMapper.java
  9. 27 0
      forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigDeviceMapper.java
  10. 9 1
      forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigStationMapper.java
  11. 27 0
      forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigChannelService.java
  12. 27 0
      forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigDeviceService.java
  13. 9 1
      forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigStationService.java
  14. 66 0
      forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigChannelServiceImpl.java
  15. 63 0
      forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigDeviceServiceImpl.java
  16. 12 0
      forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigStationServiceImpl.java
  17. 110 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/ShortTermPowerForecastingController.java
  18. 277 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/controller/UltraShortTermPowerForecastingController.java
  19. 116 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/ShortTermPowerForecastCsvRow.java
  20. 70 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/ShortTermPowerForecastData.java
  21. 246 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastCsvRow.java
  22. 276 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerForecastData.java
  23. 67 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/domain/UltraShortTermPowerSingleExport.java
  24. 138 0
      forecast-backend-server/src/main/java/com/ufo/project/forecast/shortterm/power/util/ExcelForecastFileUtil.java
  25. 138 0
      forecast-backend-server/src/main/java/com/ufo/project/model/domain/DataElectricityLocal.java
  26. 75 0
      forecast-backend-server/src/main/java/com/ufo/project/model/mapper/DataElectricityLocalMapper.java
  27. 15 0
      forecast-backend-server/src/main/java/com/ufo/project/model/mapper/ModelPredictionTasksMapper.java
  28. 73 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/IDataElectricityLocalService.java
  29. 12 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/IModelPredictionTasksService.java
  30. 109 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/DataElectricityLocalServiceImpl.java
  31. 16 0
      forecast-backend-server/src/main/java/com/ufo/project/model/service/impl/ModelPredictionTasksServiceImpl.java
  32. 108 0
      forecast-backend-server/src/main/resources/mybatis/config/ConfigChannelMapper.xml
  33. 88 0
      forecast-backend-server/src/main/resources/mybatis/config/ConfigDeviceMapper.xml
  34. 7 0
      forecast-backend-server/src/main/resources/mybatis/config/ConfigStationMapper.xml
  35. 97 0
      forecast-backend-server/src/main/resources/mybatis/model/DataElectricityLocalMapper.xml
  36. 12 1
      forecast-backend-server/src/main/resources/mybatis/model/ModelPredictionTasksMapper.xml
  37. 12 0
      forecast-backend-server/src/main/resources/sql/20260819_short_term_power_forecasting.sql
  38. 12 0
      forecast-backend-server/src/main/resources/sql/20260819_ultra_short_term_power_forecasting.sql
  39. 21 0
      forecast-backend-vue/src/api/config/channel.js
  40. 21 0
      forecast-backend-vue/src/api/config/device.js
  41. 21 0
      forecast-backend-vue/src/api/power/shortTermPowerForecasting.js
  42. 21 0
      forecast-backend-vue/src/api/power/ultraShortTermPowerForecasting.js
  43. 1 0
      forecast-backend-vue/src/components/TableActionBar/index.vue
  44. 12 0
      forecast-backend-vue/src/router/electricity.js
  45. 136 31
      forecast-backend-vue/src/views/ufo/config/station/StationConfigCrud.vue
  46. 2 2
      forecast-backend-vue/src/views/ufo/config/station/channel/index.vue
  47. 2 2
      forecast-backend-vue/src/views/ufo/config/station/device/index.vue
  48. 105 23
      forecast-backend-vue/src/views/ufo/config/station/index.vue
  49. 60 129
      forecast-backend-vue/src/views/ufo/config/station/moduleConfig.js
  50. 4 1
      forecast-backend-vue/src/views/ufo/electricity/components/BusinessDataTable.vue
  51. 39 20
      forecast-backend-vue/src/views/ufo/electricity/components/PowerLineChart.vue
  52. 150 0
      forecast-backend-vue/src/views/ufo/electricity/forecast/ShortTermPowerForecasting.vue
  53. 244 0
      forecast-backend-vue/src/views/ufo/electricity/forecast/UltraShortTermPowerForecasting.vue
  54. 10 0
      release_note/2026.08.20/20260820_dp.sql
  55. 17 0
      release_note/2026.08.20/20260820_release.pdf
  56. 28 0
      release_note/2026.08.20/readme_tsj.md

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

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

+ 86 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigChannelController.java

@@ -0,0 +1,86 @@
+package com.ufo.project.config.controller;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+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.RestController;
+import com.ufo.base.framework.aspectj.lang.annotation.Log;
+import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
+import com.ufo.project.config.domain.ConfigChannel;
+import com.ufo.project.config.service.IConfigChannelService;
+import com.ufo.base.framework.web.controller.BaseController;
+import com.ufo.base.framework.web.domain.AjaxResult;
+import com.ufo.base.common.utils.poi.ExcelUtil;
+import com.ufo.base.framework.web.page.TableDataInfo;
+
+/**
+ * 通道配置Controller
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+@RestController
+@RequestMapping("/config/channel")
+public class ConfigChannelController extends BaseController
+{
+    @Autowired
+    private IConfigChannelService configChannelService;
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ConfigChannel configChannel)
+    {
+        startPage();
+        List<ConfigChannel> list = configChannelService.selectConfigChannelList(configChannel);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:export')")
+    @Log(title = "通道配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, ConfigChannel configChannel)
+    {
+        List<ConfigChannel> list = configChannelService.selectConfigChannelList(configChannel);
+        ExcelUtil<ConfigChannel> util = new ExcelUtil<ConfigChannel>(ConfigChannel.class);
+        util.exportExcel(response, list, "通道配置");
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return success(configChannelService.selectConfigChannelById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:add')")
+    @Log(title = "通道配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody ConfigChannel configChannel)
+    {
+        return toAjax(configChannelService.insertConfigChannel(configChannel));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:edit')")
+    @Log(title = "通道配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody ConfigChannel configChannel)
+    {
+        return toAjax(configChannelService.updateConfigChannel(configChannel));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationChannel:remove')")
+    @Log(title = "通道配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(configChannelService.deleteConfigChannelByIds(ids));
+    }
+}

+ 151 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigDeviceController.java

@@ -0,0 +1,151 @@
+package com.ufo.project.config.controller;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+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.RestController;
+import com.ufo.base.framework.aspectj.lang.annotation.Log;
+import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
+import com.ufo.project.config.domain.ConfigChannel;
+import com.ufo.project.config.domain.ConfigDevice;
+import com.ufo.project.config.domain.ConfigStation;
+import com.ufo.project.config.service.IConfigChannelService;
+import com.ufo.project.config.service.IConfigDeviceService;
+import com.ufo.project.config.service.IConfigStationService;
+import com.ufo.base.framework.web.controller.BaseController;
+import com.ufo.base.framework.web.domain.AjaxResult;
+import com.ufo.base.common.utils.poi.ExcelUtil;
+import com.ufo.base.framework.web.page.TableDataInfo;
+
+/**
+ * 设备配置Controller
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+@RestController
+@RequestMapping("/config/device")
+public class ConfigDeviceController extends BaseController
+{
+    @Autowired
+    private IConfigDeviceService configDeviceService;
+
+    @Autowired
+    private IConfigStationService configStationService;
+
+    @Autowired
+    private IConfigChannelService configChannelService;
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ConfigDevice configDevice)
+    {
+        startPage();
+        List<ConfigDevice> list = configDeviceService.selectConfigDeviceList(configDevice);
+        fillNames(list);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:export')")
+    @Log(title = "设备配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, ConfigDevice configDevice)
+    {
+        List<ConfigDevice> list = configDeviceService.selectConfigDeviceList(configDevice);
+        fillNames(list);
+        ExcelUtil<ConfigDevice> util = new ExcelUtil<ConfigDevice>(ConfigDevice.class);
+        util.exportExcel(response, list, "设备配置");
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return success(configDeviceService.selectConfigDeviceById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:add')")
+    @Log(title = "设备配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody ConfigDevice configDevice)
+    {
+        return toAjax(configDeviceService.insertConfigDevice(configDevice));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:edit')")
+    @Log(title = "设备配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody ConfigDevice configDevice)
+    {
+        return toAjax(configDeviceService.updateConfigDevice(configDevice));
+    }
+
+    @PreAuthorize("@ss.hasPermi('config:stationDevice:remove')")
+    @Log(title = "设备配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(configDeviceService.deleteConfigDeviceByIds(ids));
+    }
+
+    /**
+     * 批量回填场站名称、通道名称(一次性查询,避免逐条查询)
+     */
+    private void fillNames(List<ConfigDevice> list)
+    {
+        if (list == null || list.isEmpty())
+        {
+            return;
+        }
+        List<Long> stationIds = new ArrayList<>();
+        List<Long> channelIds = new ArrayList<>();
+        for (ConfigDevice device : list)
+        {
+            if (device.getConfigStationId() != null && !stationIds.contains(device.getConfigStationId()))
+            {
+                stationIds.add(device.getConfigStationId());
+            }
+            if (device.getConfigChannelId() != null && !channelIds.contains(device.getConfigChannelId()))
+            {
+                channelIds.add(device.getConfigChannelId());
+            }
+        }
+        if (!stationIds.isEmpty())
+        {
+            List<ConfigStation> stations = configStationService.selectConfigStationByIds(stationIds.toArray(new Long[stationIds.size()]));
+            Map<Long, String> stationNameMap = new HashMap<>();
+            for (ConfigStation station : stations)
+            {
+                stationNameMap.put(station.getId(), station.getStationName());
+            }
+            for (ConfigDevice device : list)
+            {
+                device.setStationName(stationNameMap.get(device.getConfigStationId()));
+            }
+        }
+        if (!channelIds.isEmpty())
+        {
+            List<ConfigChannel> channels = configChannelService.selectConfigChannelByIds(channelIds.toArray(new Long[channelIds.size()]));
+            Map<Long, String> channelNameMap = new HashMap<>();
+            for (ConfigChannel channel : channels)
+            {
+                channelNameMap.put(channel.getId(), channel.getName());
+            }
+            for (ConfigDevice device : list)
+            {
+                device.setChannelName(channelNameMap.get(device.getConfigChannelId()));
+            }
+        }
+    }
+}

+ 30 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/controller/ConfigPointController.java

@@ -1,6 +1,9 @@
 package com.ufo.project.config.controller;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -14,7 +17,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
 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.config.domain.ConfigDevice;
 import com.ufo.project.config.domain.ConfigPoint;
+import com.ufo.project.config.service.IConfigDeviceService;
 import com.ufo.project.config.service.IConfigPointService;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
@@ -34,12 +39,37 @@ public class ConfigPointController extends BaseController
     @Autowired
     private IConfigPointService configPointService;
 
+    @Autowired
+    private IConfigDeviceService configDeviceService;
+
     @PreAuthorize("@ss.hasPermi('config:stationPoint:list')")
     @GetMapping("/list")
     public TableDataInfo list(ConfigPoint configPoint)
     {
         startPage();
         List<ConfigPoint> list = configPointService.selectConfigPointList(configPoint);
+        // 收集列表中的设备id,一次性查询config_device设备名称,避免逐条查询
+        List<Long> deviceIds = new ArrayList<>();
+        for (ConfigPoint point : list)
+        {
+            if (point.getConfigDeviceId() != null && !deviceIds.contains(point.getConfigDeviceId()))
+            {
+                deviceIds.add(point.getConfigDeviceId());
+            }
+        }
+        if (!deviceIds.isEmpty())
+        {
+            List<ConfigDevice> deviceList = configDeviceService.selectConfigDeviceByIds(deviceIds.toArray(new Long[deviceIds.size()]));
+            Map<Long, String> deviceNameMap = new HashMap<>();
+            for (ConfigDevice device : deviceList)
+            {
+                deviceNameMap.put(device.getId(), device.getName());
+            }
+            for (ConfigPoint point : list)
+            {
+                point.setConfigDeviceName(deviceNameMap.get(point.getConfigDeviceId()));
+            }
+        }
         return getDataTable(list);
     }
 

+ 161 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigChannel.java

@@ -0,0 +1,161 @@
+package com.ufo.project.config.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import com.ufo.base.framework.web.domain.BaseEntity;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 设备对象 config_device
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public class ConfigChannel extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    private Long id;
+
+    /** 编号 */
+    @Excel(name = "编号")
+    private String code;
+
+    /** 名称 */
+    @Excel(name = "名称")
+    private String name;
+
+    /** 是否禁用 */
+    @Excel(name = "是否禁用")
+    private String disabled;
+
+    /** 通信模块ID */
+    @Excel(name = "通信模块ID")
+    private Long configTransmitId;
+
+    /** 通信配置 */
+    @Excel(name = "通信配置")
+    private String transmitConfig;
+
+    /** 协议模块ID */
+    @Excel(name = "协议模块ID")
+    private Long configProtocalId;
+
+    /** 协议配置 */
+    @Excel(name = "协议配置")
+    private String protocolConfig;
+
+    
+
+    public Long getId() {
+		return id;
+	}
+
+
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+
+
+	public String getCode() {
+		return code;
+	}
+
+
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+
+
+	public String getName() {
+		return name;
+	}
+
+
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+
+
+	public String getDisabled() {
+		return disabled;
+	}
+
+
+
+	public void setDisabled(String disabled) {
+		this.disabled = disabled;
+	}
+
+
+
+	public Long getConfigTransmitId() {
+		return configTransmitId;
+	}
+
+
+
+	public void setConfigTransmitId(Long configTransmitId) {
+		this.configTransmitId = configTransmitId;
+	}
+
+
+
+	public String getTransmitConfig() {
+		return transmitConfig;
+	}
+
+
+
+	public void setTransmitConfig(String transmitConfig) {
+		this.transmitConfig = transmitConfig;
+	}
+
+
+
+	public Long getConfigProtocalId() {
+		return configProtocalId;
+	}
+
+
+
+	public void setConfigProtocalId(Long configProtocalId) {
+		this.configProtocalId = configProtocalId;
+	}
+
+
+
+	public String getProtocolConfig() {
+		return protocolConfig;
+	}
+
+
+
+	public void setProtocolConfig(String protocolConfig) {
+		this.protocolConfig = protocolConfig;
+	}
+
+
+
+	@Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("code", getCode())
+            .append("name", getName())
+            .append("disabled", getDisabled())
+            .append("configTransmitId", getConfigTransmitId())
+            .append("transmitConfig", getTransmitConfig())
+            .append("configProtocalId", getConfigProtocalId())
+            .append("protocolConfig", getProtocolConfig())
+            .toString();
+    }
+}

+ 187 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigDevice.java

@@ -0,0 +1,187 @@
+package com.ufo.project.config.domain;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import com.ufo.base.framework.web.domain.BaseEntity;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 设备对象 config_device
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public class ConfigDevice
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    private Long id;
+
+    /** 名称 */
+    @Excel(name = "名称")
+    private String name;
+    /**
+     * 配置通道ID
+     */
+    @Excel(name = "通道")
+    private Long configChannelId;
+    /**
+     * 配置站点ID
+     */
+    @Excel(name = "场站")
+    private Long configStationId;
+
+    @Excel(name = "设备类型")
+    private String ilk;
+
+    /**
+     * Parent配置设备ID
+     */
+    @Excel(name = "父级设备")
+    private Long configDeviceId;
+
+    /**
+     * 场站名称(关联config_station查询)
+     */
+    private String stationName;
+
+    /**
+     * 通道名称(关联config_channel查询)
+     */
+    private String channelName;
+
+    private String remark;
+
+    
+
+    public static long getSerialversionuid() {
+		return serialVersionUID;
+	}
+
+
+
+	public Long getId() {
+		return id;
+	}
+
+
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+
+
+	public String getName() {
+		return name;
+	}
+
+
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+
+
+	public Long getConfigChannelId() {
+		return configChannelId;
+	}
+
+
+
+	public void setConfigChannelId(Long configChannelId) {
+		this.configChannelId = configChannelId;
+	}
+
+
+
+	public Long getConfigStationId() {
+		return configStationId;
+	}
+
+
+
+	public void setConfigStationId(Long configStationId) {
+		this.configStationId = configStationId;
+	}
+
+
+
+	public String getIlk() {
+		return ilk;
+	}
+
+
+
+	public void setIlk(String ilk) {
+		this.ilk = ilk;
+	}
+
+
+
+	public Long getConfigDeviceId() {
+		return configDeviceId;
+	}
+
+
+
+	public void setConfigDeviceId(Long configDeviceId) {
+		this.configDeviceId = configDeviceId;
+	}
+
+
+
+	public String getRemark() {
+		return remark;
+	}
+
+
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+
+
+	public String getStationName() {
+		return stationName;
+	}
+
+
+
+	public void setStationName(String stationName) {
+		this.stationName = stationName;
+	}
+
+
+
+	public String getChannelName() {
+		return channelName;
+	}
+
+
+
+	public void setChannelName(String channelName) {
+		this.channelName = channelName;
+	}
+
+
+
+	@Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("name", getName())
+            .append("configChannelId", getConfigChannelId())
+            .append("configStationId", getConfigStationId())
+            .append("ilk", getIlk())
+            .append("configDeviceId", getConfigDeviceId())
+            .append("remark", getRemark())
+            .append("stationName", getStationName())
+            .append("channelName", getChannelName())
+            .toString();
+    }
+}

+ 14 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/domain/ConfigPoint.java

@@ -39,6 +39,9 @@ public class ConfigPoint extends BaseEntity
     @Excel(name = "设备号")
     private Long configDeviceId;
 
+    /** 设备名称(关联config_device查询) */
+    private String configDeviceName;
+
     /** 初始值 */
     @Excel(name = "初始值")
     private String initValue;
@@ -155,6 +158,16 @@ public class ConfigPoint extends BaseEntity
         return configDeviceId;
     }
 
+    public void setConfigDeviceName(String configDeviceName)
+    {
+        this.configDeviceName = configDeviceName;
+    }
+
+    public String getConfigDeviceName()
+    {
+        return configDeviceName;
+    }
+
     public void setInitValue(String initValue)
     {
         this.initValue = initValue;
@@ -304,6 +317,7 @@ public class ConfigPoint extends BaseEntity
             .append("addr", getAddr())
             .append("category", getCategory())
             .append("configDeviceId", getConfigDeviceId())
+            .append("configDeviceName", getConfigDeviceName())
             .append("initValue", getInitValue())
             .append("willInverse", getWillInverse())
             .append("kbComposite", getKbComposite())

+ 27 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigChannelMapper.java

@@ -0,0 +1,27 @@
+package com.ufo.project.config.mapper;
+
+import java.util.List;
+import com.ufo.project.config.domain.ConfigChannel;
+
+/**
+ * 通道Mapper接口
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public interface ConfigChannelMapper
+{
+    public ConfigChannel selectConfigChannelById(Long id);
+
+    public List<ConfigChannel> selectConfigChannelList(ConfigChannel configChannel);
+
+    public List<ConfigChannel> selectConfigChannelByIds(Long[] ids);
+
+    public int insertConfigChannel(ConfigChannel configChannel);
+
+    public int updateConfigChannel(ConfigChannel configChannel);
+
+    public int deleteConfigChannelByIds(Long[] ids);
+
+    public int deleteConfigChannelById(Long id);
+}

+ 27 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigDeviceMapper.java

@@ -0,0 +1,27 @@
+package com.ufo.project.config.mapper;
+
+import java.util.List;
+import com.ufo.project.config.domain.ConfigDevice;
+
+/**
+ * 设备Mapper接口
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public interface ConfigDeviceMapper
+{
+    public ConfigDevice selectConfigDeviceById(Long id);
+
+    public List<ConfigDevice> selectConfigDeviceList(ConfigDevice configDevice);
+
+    public List<ConfigDevice> selectConfigDeviceByIds(Long[] ids);
+
+    public int insertConfigDevice(ConfigDevice configDevice);
+
+    public int updateConfigDevice(ConfigDevice configDevice);
+
+    public int deleteConfigDeviceByIds(Long[] ids);
+
+    public int deleteConfigDeviceById(Long id);
+}

+ 9 - 1
forecast-backend-server/src/main/java/com/ufo/project/config/mapper/ConfigStationMapper.java

@@ -19,9 +19,17 @@ public interface ConfigStationMapper
      */
     public ConfigStation selectConfigStationById(Long id);
 
+    /**
+     * 批量查询场站信息
+     *
+     * @param ids 场站信息主键集合
+     * @return 场站信息集合
+     */
+    public List<ConfigStation> selectConfigStationByIds(Long[] ids);
+
     /**
      * 查询场站信息列表
-     * 
+     *
      * @param configStation 场站信息
      * @return 场站信息集合
      */

+ 27 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigChannelService.java

@@ -0,0 +1,27 @@
+package com.ufo.project.config.service;
+
+import java.util.List;
+import com.ufo.project.config.domain.ConfigChannel;
+
+/**
+ * 通道Service接口
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public interface IConfigChannelService
+{
+    public ConfigChannel selectConfigChannelById(Long id);
+
+    public List<ConfigChannel> selectConfigChannelList(ConfigChannel configChannel);
+
+    public List<ConfigChannel> selectConfigChannelByIds(Long[] ids);
+
+    public int insertConfigChannel(ConfigChannel configChannel);
+
+    public int updateConfigChannel(ConfigChannel configChannel);
+
+    public int deleteConfigChannelByIds(Long[] ids);
+
+    public int deleteConfigChannelById(Long id);
+}

+ 27 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigDeviceService.java

@@ -0,0 +1,27 @@
+package com.ufo.project.config.service;
+
+import java.util.List;
+import com.ufo.project.config.domain.ConfigDevice;
+
+/**
+ * 设备Service接口
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+public interface IConfigDeviceService
+{
+    public ConfigDevice selectConfigDeviceById(Long id);
+
+    public List<ConfigDevice> selectConfigDeviceList(ConfigDevice configDevice);
+
+    public List<ConfigDevice> selectConfigDeviceByIds(Long[] ids);
+
+    public int insertConfigDevice(ConfigDevice configDevice);
+
+    public int updateConfigDevice(ConfigDevice configDevice);
+
+    public int deleteConfigDeviceByIds(Long[] ids);
+
+    public int deleteConfigDeviceById(Long id);
+}

+ 9 - 1
forecast-backend-server/src/main/java/com/ufo/project/config/service/IConfigStationService.java

@@ -19,9 +19,17 @@ public interface IConfigStationService
      */
     public ConfigStation selectConfigStationById(Long id);
 
+    /**
+     * 批量查询场站信息
+     *
+     * @param ids 场站信息主键集合
+     * @return 场站信息集合
+     */
+    public List<ConfigStation> selectConfigStationByIds(Long[] ids);
+
     /**
      * 查询场站信息列表
-     * 
+     *
      * @param configStation 场站信息
      * @return 场站信息集合
      */

+ 66 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigChannelServiceImpl.java

@@ -0,0 +1,66 @@
+package com.ufo.project.config.service.impl;
+
+import java.util.List;
+import com.ufo.base.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ufo.project.config.mapper.ConfigChannelMapper;
+import com.ufo.project.config.domain.ConfigChannel;
+import com.ufo.project.config.service.IConfigChannelService;
+
+/**
+ * 通道Service业务层处理
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+@Service
+public class ConfigChannelServiceImpl implements IConfigChannelService
+{
+    @Autowired
+    private ConfigChannelMapper configChannelMapper;
+
+    @Override
+    public ConfigChannel selectConfigChannelById(Long id)
+    {
+        return configChannelMapper.selectConfigChannelById(id);
+    }
+
+    @Override
+    public List<ConfigChannel> selectConfigChannelList(ConfigChannel configChannel)
+    {
+        return configChannelMapper.selectConfigChannelList(configChannel);
+    }
+
+    @Override
+    public List<ConfigChannel> selectConfigChannelByIds(Long[] ids)
+    {
+        return configChannelMapper.selectConfigChannelByIds(ids);
+    }
+
+    @Override
+    public int insertConfigChannel(ConfigChannel configChannel)
+    {
+        configChannel.setCreateTime(DateUtils.getNowDate());
+        return configChannelMapper.insertConfigChannel(configChannel);
+    }
+
+    @Override
+    public int updateConfigChannel(ConfigChannel configChannel)
+    {
+        configChannel.setUpdateTime(DateUtils.getNowDate());
+        return configChannelMapper.updateConfigChannel(configChannel);
+    }
+
+    @Override
+    public int deleteConfigChannelByIds(Long[] ids)
+    {
+        return configChannelMapper.deleteConfigChannelByIds(ids);
+    }
+
+    @Override
+    public int deleteConfigChannelById(Long id)
+    {
+        return configChannelMapper.deleteConfigChannelById(id);
+    }
+}

+ 63 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigDeviceServiceImpl.java

@@ -0,0 +1,63 @@
+package com.ufo.project.config.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ufo.project.config.mapper.ConfigDeviceMapper;
+import com.ufo.project.config.domain.ConfigDevice;
+import com.ufo.project.config.service.IConfigDeviceService;
+
+/**
+ * 设备Service业务层处理
+ *
+ * @author ufo
+ * @date 2026-08-06
+ */
+@Service
+public class ConfigDeviceServiceImpl implements IConfigDeviceService
+{
+    @Autowired
+    private ConfigDeviceMapper configDeviceMapper;
+
+    @Override
+    public ConfigDevice selectConfigDeviceById(Long id)
+    {
+        return configDeviceMapper.selectConfigDeviceById(id);
+    }
+
+    @Override
+    public List<ConfigDevice> selectConfigDeviceList(ConfigDevice configDevice)
+    {
+        return configDeviceMapper.selectConfigDeviceList(configDevice);
+    }
+
+    @Override
+    public List<ConfigDevice> selectConfigDeviceByIds(Long[] ids)
+    {
+        return configDeviceMapper.selectConfigDeviceByIds(ids);
+    }
+
+    @Override
+    public int insertConfigDevice(ConfigDevice configDevice)
+    {
+        return configDeviceMapper.insertConfigDevice(configDevice);
+    }
+
+    @Override
+    public int updateConfigDevice(ConfigDevice configDevice)
+    {
+        return configDeviceMapper.updateConfigDevice(configDevice);
+    }
+
+    @Override
+    public int deleteConfigDeviceByIds(Long[] ids)
+    {
+        return configDeviceMapper.deleteConfigDeviceByIds(ids);
+    }
+
+    @Override
+    public int deleteConfigDeviceById(Long id)
+    {
+        return configDeviceMapper.deleteConfigDeviceById(id);
+    }
+}

+ 12 - 0
forecast-backend-server/src/main/java/com/ufo/project/config/service/impl/ConfigStationServiceImpl.java

@@ -32,6 +32,18 @@ public class ConfigStationServiceImpl implements IConfigStationService
         return configStationMapper.selectConfigStationById(id);
     }
 
+    /**
+     * 批量查询场站信息
+     *
+     * @param ids 场站信息主键集合
+     * @return 场站信息集合
+     */
+    @Override
+    public List<ConfigStation> selectConfigStationByIds(Long[] ids)
+    {
+        return configStationMapper.selectConfigStationByIds(ids);
+    }
+
     /**
      * 查询场站信息列表
      * 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 108 - 0
forecast-backend-server/src/main/resources/mybatis/config/ConfigChannelMapper.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.config.mapper.ConfigChannelMapper">
+
+    <resultMap type="ConfigChannel" id="ConfigChannelResult">
+        <result property="id"    column="id"    />
+        <result property="code"    column="code"    />
+        <result property="name"    column="name"    />
+        <result property="disabled"    column="disabled"    />
+        <result property="configTransmitId"    column="config_transmit_id"    />
+        <result property="transmitConfig"    column="transmit_config"    />
+        <result property="configProtocalId"    column="config_protocol_id"    />
+        <result property="protocolConfig"    column="protocol_config"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectConfigChannelVo">
+        select id, code, name, disabled, config_transmit_id, transmit_config, config_protocol_id, protocol_config, create_by, create_time, update_by, update_time, remark from config_channel
+    </sql>
+
+    <select id="selectConfigChannelList" parameterType="ConfigChannel" resultMap="ConfigChannelResult">
+        <include refid="selectConfigChannelVo"/>
+        <where>
+            <if test="code != null and code != ''"> and code = #{code}</if>
+            <if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="disabled != null and disabled != ''"> and disabled = #{disabled}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectConfigChannelById" parameterType="Long" resultMap="ConfigChannelResult">
+        <include refid="selectConfigChannelVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectConfigChannelByIds" parameterType="Long" resultMap="ConfigChannelResult">
+        select id, name from config_channel where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <insert id="insertConfigChannel" parameterType="ConfigChannel" useGeneratedKeys="true" keyProperty="id">
+        insert into config_channel
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="code != null and code != ''">code,</if>
+            <if test="name != null and name != ''">name,</if>
+            <if test="disabled != null and disabled != ''">disabled,</if>
+            <if test="configTransmitId != null">config_transmit_id,</if>
+            <if test="transmitConfig != null and transmitConfig != ''">transmit_config,</if>
+            <if test="configProtocalId != null">config_protocol_id,</if>
+            <if test="protocolConfig != null and protocolConfig != ''">protocol_config,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null and updateBy != ''">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="code != null and code != ''">#{code},</if>
+            <if test="name != null and name != ''">#{name},</if>
+            <if test="disabled != null and disabled != ''">#{disabled},</if>
+            <if test="configTransmitId != null">#{configTransmitId},</if>
+            <if test="transmitConfig != null and transmitConfig != ''">#{transmitConfig},</if>
+            <if test="configProtocalId != null">#{configProtocalId},</if>
+            <if test="protocolConfig != null and protocolConfig != ''">#{protocolConfig},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null and updateBy != ''">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateConfigChannel" parameterType="ConfigChannel">
+        update config_channel
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="code != null and code != ''">code = #{code},</if>
+            <if test="name != null and name != ''">name = #{name},</if>
+            <if test="disabled != null and disabled != ''">disabled = #{disabled},</if>
+            <if test="configTransmitId != null">config_transmit_id = #{configTransmitId},</if>
+            <if test="transmitConfig != null and transmitConfig != ''">transmit_config = #{transmitConfig},</if>
+            <if test="configProtocalId != null">config_protocol_id = #{configProtocalId},</if>
+            <if test="protocolConfig != null and protocolConfig != ''">protocol_config = #{protocolConfig},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteConfigChannelById" parameterType="Long">
+        delete from config_channel where id = #{id}
+    </delete>
+
+    <delete id="deleteConfigChannelByIds" parameterType="Long">
+        delete from config_channel where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 88 - 0
forecast-backend-server/src/main/resources/mybatis/config/ConfigDeviceMapper.xml

@@ -0,0 +1,88 @@
+<?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.config.mapper.ConfigDeviceMapper">
+
+    <resultMap type="ConfigDevice" id="ConfigDeviceResult">
+        <result property="id"    column="id"    />
+        <result property="name"    column="name"    />
+        <result property="configChannelId"    column="config_channel_id"    />
+        <result property="configStationId"    column="config_station_id"    />
+        <result property="ilk"    column="ilk"    />
+        <result property="configDeviceId"    column="config_device_id"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectConfigDeviceVo">
+        select id, name, config_channel_id, config_station_id, ilk, config_device_id, remark from config_device
+    </sql>
+
+    <select id="selectConfigDeviceList" parameterType="ConfigDevice" resultMap="ConfigDeviceResult">
+        <include refid="selectConfigDeviceVo"/>
+        <where>
+            <if test="configStationId != null"> and config_station_id = #{configStationId}</if>
+            <if test="configChannelId != null"> and config_channel_id = #{configChannelId}</if>
+            <if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="ilk != null and ilk != ''"> and ilk = #{ilk}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectConfigDeviceById" parameterType="Long" resultMap="ConfigDeviceResult">
+        <include refid="selectConfigDeviceVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectConfigDeviceByIds" parameterType="Long" resultMap="ConfigDeviceResult">
+        select id, name from config_device where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <insert id="insertConfigDevice" parameterType="ConfigDevice" useGeneratedKeys="true" keyProperty="id">
+        insert into config_device
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="name != null and name != ''">name,</if>
+            <if test="configChannelId != null">config_channel_id,</if>
+            <if test="configStationId != null">config_station_id,</if>
+            <if test="ilk != null and ilk != ''">ilk,</if>
+            <if test="configDeviceId != null">config_device_id,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="name != null and name != ''">#{name},</if>
+            <if test="configChannelId != null">#{configChannelId},</if>
+            <if test="configStationId != null">#{configStationId},</if>
+            <if test="ilk != null and ilk != ''">#{ilk},</if>
+            <if test="configDeviceId != null">#{configDeviceId},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateConfigDevice" parameterType="ConfigDevice">
+        update config_device
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="name != null and name != ''">name = #{name},</if>
+            <if test="configChannelId != null">config_channel_id = #{configChannelId},</if>
+            <if test="configStationId != null">config_station_id = #{configStationId},</if>
+            <if test="ilk != null and ilk != ''">ilk = #{ilk},</if>
+            <if test="configDeviceId != null">config_device_id = #{configDeviceId},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            update_time = CURRENT_TIMESTAMP,
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteConfigDeviceById" parameterType="Long">
+        delete from config_device where id = #{id}
+    </delete>
+
+    <delete id="deleteConfigDeviceByIds" parameterType="Long">
+        delete from config_device where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 7 - 0
forecast-backend-server/src/main/resources/mybatis/config/ConfigStationMapper.xml

@@ -80,6 +80,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectConfigStationVo"/>
         where id = #{id}
     </select>
+
+    <select id="selectConfigStationByIds" parameterType="Long" resultMap="ConfigStationResult">
+        select id, station_name from config_station where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
         
     <insert id="insertConfigStation" parameterType="ConfigStation">
         insert into config_station

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

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

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

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

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

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

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

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

+ 21 - 0
forecast-backend-vue/src/api/config/channel.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function listChannel(query) {
+  return request({ url: '/config/channel/list', method: 'get', params: query })
+}
+
+export function getChannel(id) {
+  return request({ url: '/config/channel/' + id, method: 'get' })
+}
+
+export function addChannel(data) {
+  return request({ url: '/config/channel', method: 'post', data })
+}
+
+export function updateChannel(data) {
+  return request({ url: '/config/channel', method: 'put', data })
+}
+
+export function delChannel(id) {
+  return request({ url: '/config/channel/' + id, method: 'delete' })
+}

+ 21 - 0
forecast-backend-vue/src/api/config/device.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function listDevice(query) {
+  return request({ url: '/config/device/list', method: 'get', params: query })
+}
+
+export function getDevice(id) {
+  return request({ url: '/config/device/' + id, method: 'get' })
+}
+
+export function addDevice(data) {
+  return request({ url: '/config/device', method: 'post', data })
+}
+
+export function updateDevice(data) {
+  return request({ url: '/config/device', method: 'put', data })
+}
+
+export function delDevice(id) {
+  return request({ url: '/config/device/' + id, method: 'delete' })
+}

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

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

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

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

+ 1 - 0
forecast-backend-vue/src/components/TableActionBar/index.vue

@@ -94,6 +94,7 @@ const props = defineProps({
   font-size: 13px;
   border-radius: 4px;
   cursor: pointer;
+  white-space: nowrap;
   transition: background-color 0.2s;
 
   &.view-btn {

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

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

+ 136 - 31
forecast-backend-vue/src/views/ufo/config/station/StationConfigCrud.vue

@@ -3,7 +3,7 @@
     <a-card :bordered="false">
       <a-form :model="queryParams" layout="inline">
         <a-form-item v-if="hasStation" label="场站">
-          <a-select v-model:value="queryParams.stationId" allow-clear show-search class="query-select" :options="stationOptions" :filter-option="filterOption" />
+          <a-select v-model:value="queryParams[stationField]" allow-clear show-search class="query-select" :options="stationOptions" :filter-option="filterOption" />
         </a-form-item>
         <a-form-item v-for="item in searchFields" :key="item.name" :label="item.label">
           <a-select v-if="item.type === 'select'" v-model:value="queryParams[item.name]" allow-clear class="query-select" :options="item.options" />
@@ -11,25 +11,44 @@
         </a-form-item>
         <a-form-item>
           <a-space>
-            <a-button type="primary" :loading="loading" @click="handleQuery">查询</a-button>
-            <a-button @click="resetQuery">重置</a-button>
-            <a-button v-hasPermi="[moduleConfig.perm + ':add']" type="primary" @click="openForm()">新增</a-button>
-            <a-button v-hasPermi="[moduleConfig.perm + ':export']" @click="handleExport">导出</a-button>
+            <a-button type="primary" :loading="loading" @click="handleQuery"><BearJiaIcon icon="SearchOutlined"/>查询</a-button>
+            <a-button @click="resetQuery"><BearJiaIcon icon="LoadingOutlined"/>重置</a-button>
           </a-space>
         </a-form-item>
       </a-form>
     </a-card>
 
-    <a-card :bordered="false" class="section">
-      <a-table :columns="columns" :data-source="rows" :loading="loading" :pagination="pagination" row-key="id" :scroll="tableScroll" @change="handleTableChange">
+    <a-card ref="tableCardRef" :bordered="false" class="section">
+      <TableToolbar
+        :columns="columns"
+        :selected-columns="selectedColumnKeys"
+        :loading="loading"
+        :export-config="exportConfig"
+        @columnSettingsChange="handleColumnSettingsChange"
+        @refresh="handleQuery"
+        @fullscreen="handleFullscreen"
+        @export="handleExport"
+        @reset="handleTableReset"
+      >
+        <template #left>
+          <a-button v-hasPermi="[moduleConfig.perm + ':add']" type="primary" @click="openForm()"><BearJiaIcon icon="plus-outlined"/>新增</a-button>
+          <a-button v-hasPermi="[moduleConfig.perm + ':remove']" danger type="primary" :disabled="selectedRowKeys.length <= 0" @click="handleBatchDelete"><BearJiaIcon icon="delete-outlined"/>删除</a-button>
+          <a-button v-hasPermi="[moduleConfig.perm + ':export']" @click="handleExport"><BearJiaIcon icon="export-outlined"/>导出</a-button>
+        </template>
+      </TableToolbar>
+      <a-table :columns="displayColumns" :data-source="rows" :loading="loading" :pagination="pagination" row-key="id" :row-selection="rowSelection" :size="tableConfigStore.size" :scroll="tableScroll" @change="handleTableChange">
         <template #bodyCell="{ column, record }">
-          <template v-if="column.key === 'operation'">
-            <a-space>
-              <a v-hasPermi="[moduleConfig.perm + ':edit']" @click="openForm(record)">编辑</a>
-              <a-popconfirm :title="'确认删除该' + moduleConfig.title + '?'" @confirm="removeRecord(record)">
-                <a v-hasPermi="[moduleConfig.perm + ':remove']" class="danger-link">删除</a>
-              </a-popconfirm>
-            </a-space>
+          <a-switch v-if="isSwitchColumn(column)" :checked="record[column.dataIndex] === switchCheckedValue(column)" disabled size="small" />
+          <template v-else-if="column.key === 'operation'">
+            <TableActionBar :record="record" :has-view="false" :has-edit="true" :has-delete="true">
+              <template #edit>
+                <a v-hasPermi="[moduleConfig.perm + ':edit']" class="action-btn edit-btn" @click="openForm(record)"><BearJiaIcon icon="edit-outlined"/>修改</a>
+              </template>
+              <template #delete>
+                <a-divider type="vertical"/>
+                <a v-hasPermi="[moduleConfig.perm + ':remove']" class="action-btn delete-btn" @click="handleDeleteRecord(record)"><BearJiaIcon icon="delete-outlined"/>删除</a>
+              </template>
+            </TableActionBar>
           </template>
         </template>
       </a-table>
@@ -39,14 +58,16 @@
       <a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 7 }" :wrapper-col="{ span: 15 }">
         <a-row :gutter="16">
           <a-col v-if="hasStation" :span="12">
-            <a-form-item label="所属场站" name="stationId">
-              <a-select v-model:value="form.stationId" show-search :options="stationOptions" :filter-option="filterOption" @change="handleStationChange" />
+            <a-form-item label="所属场站" :name="stationField">
+              <a-select v-model:value="form[stationField]" show-search :options="stationOptions" :filter-option="filterOption" @change="handleStationChange" />
             </a-form-item>
           </a-col>
           <a-col v-for="item in moduleConfig.fields" :key="item[0]" :span="12">
             <a-form-item :label="item[1]" :name="item[0]">
               <a-select v-if="item[2] === 'select'" v-model:value="form[item[0]]" allow-clear :options="toOptions(item[4])" />
               <a-select v-else-if="item[2] === 'device'" v-model:value="form[deviceFieldName]" allow-clear show-search :options="deviceOptions" :filter-option="filterOption" @change="handleDeviceChange" />
+              <a-select v-else-if="item[2] === 'channel'" v-model:value="form[channelFieldName]" allow-clear show-search :options="channelOptions" :filter-option="filterOption" />
+              <a-switch v-else-if="item[2] === 'switch'" v-model:checked="form[item[0]]" :checked-value="item[4]?.[0]?.[1] ?? true" :un-checked-value="item[4]?.[1]?.[1] ?? false" />
               <a-input-number v-else-if="item[2] === 'number'" v-model:value="form[item[0]]" class="full-input" />
               <a-date-picker v-else-if="item[2] === 'date'" v-model:value="form[item[0]]" value-format="YYYY-MM-DD" class="full-input" />
               <a-input v-else v-model:value="form[item[0]]" allow-clear />
@@ -60,10 +81,15 @@
 
 <script setup>
 import { computed, reactive, ref } from 'vue';
-import { message } from 'ant-design-vue';
+import { message, Modal } from 'ant-design-vue';
 import BearJiaUtil from '@/utils/BearJiaUtil';
 import { listStation } from '@/api/config/station';
 import { listStationDevice } from '@/api/config/stationDevice';
+import { listChannel } from '@/api/config/channel';
+import TableActionBar from '@/components/TableActionBar/index.vue';
+import TableToolbar from '@/components/BearJiaProTable/TableToolbar.vue';
+import { useTableConfigStore } from '@/stores/tableConfig';
+import { BearJiaIcon } from '@/utils/BearJiaIcon.js';
 
 const props = defineProps({ moduleConfig: { type: Object, required: true }, api: { type: Object, required: true } });
 
@@ -71,31 +97,64 @@ const loading = ref(false);
 const rows = ref([]);
 const stationOptions = ref([]);
 const allDevices = ref([]);
+const allChannels = ref([]);
 const formOpen = ref(false);
 const formRef = ref();
-const queryParams = reactive({ pageNum: 1, pageSize: 10, stationId: undefined });
+const selectedRowKeys = ref([]);
+const tableCardRef = ref();
+const selectedColumnKeys = ref([]);
+const tableConfigStore = useTableConfigStore();
+const stationField = computed(() => props.moduleConfig.stationField || 'stationId');
+const queryParams = reactive({ pageNum: 1, pageSize: 10, [stationField.value]: undefined });
 const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true });
 const form = reactive({});
 
 const hasStation = computed(() => props.moduleConfig.hasStation !== false);
 const deviceFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'device')?.[0]);
+const channelFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'channel')?.[0]);
 const fieldMap = computed(() => new Map(props.moduleConfig.fields.map((item) => [item[0], item])));
-const searchFields = computed(() => props.moduleConfig.search.filter((name) => name !== 'stationId').map((name) => ({ name, label: fieldMap.value.get(name)?.[1] || name, type: fieldMap.value.get(name)?.[2], options: toOptions(fieldMap.value.get(name)?.[4]) })));
+const searchFields = computed(() => props.moduleConfig.search.filter((name) => name !== stationField.value).map((name) => ({ name, label: fieldMap.value.get(name)?.[1] || name, type: fieldMap.value.get(name)?.[2], options: toOptions(fieldMap.value.get(name)?.[4]) })));
+const columnTitle = (name) => {
+  if (name === 'stationName') return '场站';
+  if (name === 'channelName') return '通道';
+  return fieldMap.value.get(name)?.[1] || name;
+};
 const columns = computed(() => [
   { title: 'ID', dataIndex: 'id', width: 80 },
-  ...props.moduleConfig.columns.map((name) => ({ title: name === 'stationName' ? '场站' : fieldMap.value.get(name)?.[1] || name, dataIndex: name, width: 140, ellipsis: true })),
-  { title: '操作', key: 'operation', width: 130, fixed: 'right' }
+  ...props.moduleConfig.columns.map((name) => ({ title: columnTitle(name), dataIndex: name, width: 140, ellipsis: true })),
+  { title: '操作', key: 'operation', dataIndex: 'operation', width: 200, fixed: 'right' }
 ]);
-const tableScroll = computed(() => ({ x: columns.value.reduce((sum, col) => sum + (col.width || 140), 0) }));
+const storageKey = 'station-config-columns-' + props.moduleConfig.api;
+const readSavedColumns = () => {
+  try {
+    const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
+    if (Array.isArray(saved) && saved.length > 0) {
+      const valid = saved.filter((k) => columns.value.some((c) => c.dataIndex === k));
+      if (valid.length > 0) return valid;
+    }
+  } catch (e) { /* 忽略存储解析错误 */ }
+  return columns.value.map((c) => c.dataIndex);
+};
+selectedColumnKeys.value = readSavedColumns();
+const displayColumns = computed(() => columns.value.filter((col) => selectedColumnKeys.value.includes(col.dataIndex)));
+const tableScroll = computed(() => ({ x: displayColumns.value.reduce((sum, col) => sum + (col.width || 140), 0) + 40 }));
+const exportConfig = { enabled: true };
 const rules = computed(() => ({
-  ...(hasStation.value ? { stationId: [{ required: true, message: '请选择所属场站', trigger: 'change' }] } : {}),
-  ...Object.fromEntries(props.moduleConfig.fields.filter((item) => item[3]).map((item) => [item[0], [{ required: true, message: '请输入' + item[1], trigger: item[2] === 'select' || item[2] === 'device' ? 'change' : 'blur' }]]))
+  ...(hasStation.value ? { [stationField.value]: [{ required: true, message: '请选择所属场站', trigger: 'change' }] } : {}),
+  ...Object.fromEntries(props.moduleConfig.fields.filter((item) => item[3]).map((item) => [item[0], [{ required: true, message: '请输入' + item[1], trigger: item[2] === 'select' || item[2] === 'device' || item[2] === 'switch' ? 'change' : 'blur' }]]))
 }));
 const deviceOptions = computed(() => allDevices.value
-  .filter((item) => !form.stationId || String(item.stationId) === String(form.stationId))
+  .filter((item) => !form[stationField.value] || String(item.stationId) === String(form[stationField.value]))
   .map((item) => ({ label: item.deviceName || item.name || item.id, value: item.id, raw: item })));
+const channelOptions = computed(() => allChannels.value.map((item) => ({ label: item.name || item.id, value: item.id })));
+const rowSelection = computed(() => ({
+  selectedRowKeys: selectedRowKeys.value,
+  onChange: (keys) => { selectedRowKeys.value = keys; }
+}));
 
 const toOptions = (items = []) => items.map((item) => ({ label: item[0], value: item[1] }));
+const isSwitchColumn = (column) => fieldMap.value.get(column.dataIndex)?.[2] === 'switch';
+const switchCheckedValue = (column) => fieldMap.value.get(column.dataIndex)?.[4]?.[0]?.[1] ?? true;
 const filterOption = (input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase());
 const resetObject = (target, source = {}) => { Object.keys(target).forEach((key) => delete target[key]); Object.assign(target, source); };
 
@@ -110,6 +169,11 @@ const loadDevices = async () => {
   const res = await listStationDevice({ pageNum: 1, pageSize: 500 });
   allDevices.value = res?.rows || res?.data || [];
 };
+const loadChannels = async () => {
+  if (!channelFieldName.value) return;
+  const res = await listChannel({ pageNum: 1, pageSize: 500 });
+  allChannels.value = res?.rows || res?.data || [];
+};
 const handleQuery = async () => {
   loading.value = true;
   try {
@@ -134,12 +198,14 @@ const handleTableChange = (page) => {
 };
 const openForm = async (record) => {
   await loadDevices();
+  await loadChannels();
   if (record?.id) {
     const res = await props.api.get(record.id);
     resetObject(form, { ...res.data });
   } else {
-    resetObject(form, { stationId: queryParams.stationId });
+    resetObject(form, { [stationField.value]: queryParams[stationField.value] });
   }
+  props.moduleConfig.fields.filter((item) => item[2] === 'switch').forEach((item) => { if (form[item[0]] === undefined) form[item[0]] = item[4]?.[1]?.[1] ?? false; });
   formOpen.value = true;
 };
 const handleStationChange = () => {
@@ -159,15 +225,55 @@ const submitForm = async () => {
   formOpen.value = false;
   handleQuery();
 };
-const removeRecord = async (record) => {
-  await props.api.del(record.id);
-  message.success('删除成功');
-  handleQuery();
+const handleDeleteRecord = (record) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: `确认删除该${moduleConfig.title}?`,
+    onOk: async () => {
+      await props.api.del(record.id);
+      message.success('删除成功');
+      handleQuery();
+    }
+  });
+};
+const handleBatchDelete = () => {
+  if (selectedRowKeys.value.length <= 0) {
+    message.warning('请至少选择一条数据');
+    return;
+  }
+  Modal.confirm({
+    title: '确认删除',
+    content: `确认删除选中的 ${selectedRowKeys.value.length} 条数据?`,
+    onOk: async () => {
+      await props.api.del(selectedRowKeys.value);
+      message.success('删除成功');
+      selectedRowKeys.value = [];
+      handleQuery();
+    }
+  });
 };
 const handleExport = () => BearJiaUtil.download('config/' + props.moduleConfig.api + '/export', queryParams, props.moduleConfig.exportName);
+const handleColumnSettingsChange = (keys) => {
+  selectedColumnKeys.value = keys;
+  localStorage.setItem(storageKey, JSON.stringify(keys));
+};
+const handleFullscreen = (isFullscreen) => {
+  const el = tableCardRef.value?.$el || tableCardRef.value;
+  if (isFullscreen) {
+    el?.requestFullscreen?.();
+  } else {
+    document.exitFullscreen?.();
+  }
+};
+const handleTableReset = () => {
+  selectedColumnKeys.value = columns.value.map((c) => c.dataIndex);
+  localStorage.removeItem(storageKey);
+  handleQuery();
+};
 
 loadStations();
 loadDevices();
+loadChannels();
 handleQuery();
 </script>
 
@@ -176,6 +282,5 @@ handleQuery();
   .section { margin-top: 12px; }
   .query-select { width: 180px; }
   .full-input { width: 100%; }
-  .danger-link { color: #ff4d4f; }
 }
 </style>

+ 2 - 2
forecast-backend-vue/src/views/ufo/config/station/channel/index.vue

@@ -5,8 +5,8 @@
 <script setup name="StationChannel">
 import StationConfigCrud from '../StationConfigCrud.vue';
 import { stationConfigModules } from '../moduleConfig';
-import { listStationChannel, getStationChannel, addStationChannel, updateStationChannel, delStationChannel } from '@/api/config/stationChannel';
+import { listChannel, getChannel, addChannel, updateChannel, delChannel } from '@/api/config/channel';
 
 const moduleConfig = stationConfigModules.find((item) => item.key === 'channel');
-const api = { list: listStationChannel, get: getStationChannel, add: addStationChannel, update: updateStationChannel, del: delStationChannel };
+const api = { list: listChannel, get: getChannel, add: addChannel, update: updateChannel, del: delChannel };
 </script>

+ 2 - 2
forecast-backend-vue/src/views/ufo/config/station/device/index.vue

@@ -5,8 +5,8 @@
 <script setup name="StationDevice">
 import StationConfigCrud from '../StationConfigCrud.vue';
 import { stationConfigModules } from '../moduleConfig';
-import { listStationDevice, getStationDevice, addStationDevice, updateStationDevice, delStationDevice } from '@/api/config/stationDevice';
+import { listDevice, getDevice, addDevice, updateDevice, delDevice } from '@/api/config/device';
 
 const moduleConfig = stationConfigModules.find((item) => item.key === 'device');
-const api = { list: listStationDevice, get: getStationDevice, add: addStationDevice, update: updateStationDevice, del: delStationDevice };
+const api = { list: listDevice, get: getDevice, add: addDevice, update: updateDevice, del: delDevice };
 </script>

+ 105 - 23
forecast-backend-vue/src/views/ufo/config/station/index.vue

@@ -16,22 +16,39 @@
         </a-form-item>
         <a-form-item>
           <a-space>
-            <a-button type="primary" :loading="loading" @click="handleQuery">查询</a-button>
-            <a-button @click="resetQuery">重置</a-button>
-            <a-button v-hasPermi="['config:station:add']" type="primary" @click="openForm()">新增</a-button>
-            <a-button v-hasPermi="['config:station:export']" @click="handleExport">导出</a-button>
+            <a-button type="primary" :loading="loading" @click="handleQuery"><BearJiaIcon icon="SearchOutlined"/>查询</a-button>
+            <a-button @click="resetQuery"><BearJiaIcon icon="LoadingOutlined"/>重置</a-button>
           </a-space>
         </a-form-item>
       </a-form>
     </a-card>
 
-    <a-card :bordered="false" class="section">
-      <a-table
+    <a-card ref="tableCardRef" :bordered="false" class="section">
+      <TableToolbar
         :columns="columns"
+        :selected-columns="selectedColumnKeys"
+        :loading="loading"
+        :export-config="exportConfig"
+        @columnSettingsChange="handleColumnSettingsChange"
+        @refresh="handleQuery"
+        @fullscreen="handleFullscreen"
+        @export="handleExport"
+        @reset="handleTableReset"
+      >
+        <template #left>
+          <a-button v-hasPermi="['config:station:add']" type="primary" @click="openForm()"><BearJiaIcon icon="plus-outlined"/>新增</a-button>
+          <a-button v-hasPermi="['config:station:remove']" danger type="primary" :disabled="selectedRowKeys.length <= 0" @click="handleBatchDelete"><BearJiaIcon icon="delete-outlined"/>删除</a-button>
+          <a-button v-hasPermi="['config:station:export']" @click="handleExport"><BearJiaIcon icon="export-outlined"/>导出</a-button>
+        </template>
+      </TableToolbar>
+      <a-table
+        :columns="displayColumns"
         :data-source="rows"
         :loading="loading"
         :pagination="pagination"
         row-key="id"
+        :row-selection="rowSelection"
+        :size="tableConfigStore.size"
         @change="handleTableChange"
       >
         <template #bodyCell="{ column, record }">
@@ -42,12 +59,15 @@
             <a-tag :color="record.isEnable === '1' ? 'success' : 'default'">{{ enableText(record.isEnable) }}</a-tag>
           </template>
           <template v-else-if="column.key === 'operation'">
-            <a-space>
-              <a v-hasPermi="['config:station:edit']" @click="openForm(record)">编辑</a>
-              <a-popconfirm title="确认删除该场站?" @confirm="removeRecord(record)">
-                <a v-hasPermi="['config:station:remove']" class="danger-link">删除</a>
-              </a-popconfirm>
-            </a-space>
+            <TableActionBar :record="record" :has-view="false" :has-edit="true" :has-delete="true">
+              <template #edit>
+                <a v-hasPermi="['config:station:edit']" class="action-btn edit-btn" @click="openForm(record)"><BearJiaIcon icon="edit-outlined"/>修改</a>
+              </template>
+              <template #delete>
+                <a-divider type="vertical"/>
+                <a v-hasPermi="['config:station:remove']" class="action-btn delete-btn" @click="handleDeleteRecord(record)"><BearJiaIcon icon="delete-outlined"/>删除</a>
+              </template>
+            </TableActionBar>
           </template>
         </template>
       </a-table>
@@ -123,15 +143,23 @@
 </template>
 
 <script setup name="ConfigStation">
-import { reactive, ref } from 'vue';
-import { message } from 'ant-design-vue';
+import { computed, reactive, ref } from 'vue';
+import { message, Modal } from 'ant-design-vue';
 import BearJiaUtil from '@/utils/BearJiaUtil';
 import { addStation, delStation, getStation, listStation, updateStation } from '@/api/config/station';
+import TableActionBar from '@/components/TableActionBar/index.vue';
+import TableToolbar from '@/components/BearJiaProTable/TableToolbar.vue';
+import { useTableConfigStore } from '@/stores/tableConfig';
+import { BearJiaIcon } from '@/utils/BearJiaIcon.js';
 
 const loading = ref(false);
 const rows = ref([]);
 const formOpen = ref(false);
 const formRef = ref();
+const selectedRowKeys = ref([]);
+const tableCardRef = ref();
+const selectedColumnKeys = ref([]);
+const tableConfigStore = useTableConfigStore();
 
 const queryParams = reactive({
   pageNum: 1,
@@ -151,6 +179,11 @@ const pagination = reactive({
 
 const form = reactive({});
 
+const rowSelection = computed(() => ({
+  selectedRowKeys: selectedRowKeys.value,
+  onChange: (keys) => { selectedRowKeys.value = keys; }
+}));
+
 const stationTypeOptions = [
   { label: '风电', value: 0 },
   { label: '光伏', value: 1 }
@@ -176,8 +209,22 @@ const columns = [
   { title: '负责人', dataIndex: 'responsiblePersonName', width: 120 },
   { title: '电话', dataIndex: 'responsiblePersonPhone', width: 140 },
   { title: '启用', dataIndex: 'isEnable', key: 'isEnable', width: 90 },
-  { title: '操作', key: 'operation', width: 130, fixed: 'right' }
+  { title: '操作', key: 'operation', dataIndex: 'operation', width: 200, fixed: 'right' }
 ];
+const storageKey = 'station-config-columns-station';
+const readSavedColumns = () => {
+  try {
+    const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
+    if (Array.isArray(saved) && saved.length > 0) {
+      const valid = saved.filter((k) => columns.some((c) => c.dataIndex === k));
+      if (valid.length > 0) return valid;
+    }
+  } catch (e) { /* 忽略存储解析错误 */ }
+  return columns.map((c) => c.dataIndex);
+};
+selectedColumnKeys.value = readSavedColumns();
+const displayColumns = computed(() => columns.filter((col) => selectedColumnKeys.value.includes(col.dataIndex)));
+const exportConfig = { enabled: true };
 
 const stationTypeText = (value) => stationTypeOptions.find((item) => item.value === Number(value))?.label || '-';
 const enableText = (value) => enableOptions.find((item) => item.value === String(value))?.label || '-';
@@ -241,15 +288,54 @@ const submitForm = async () => {
   handleQuery();
 };
 
-const removeRecord = async (record) => {
-  await delStation(record.id);
-  message.success('删除成功');
-  handleQuery();
+const handleDeleteRecord = (record) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: '确认删除该场站?',
+    onOk: async () => {
+      await delStation(record.id);
+      message.success('删除成功');
+      handleQuery();
+    }
+  });
+};
+const handleBatchDelete = () => {
+  if (selectedRowKeys.value.length <= 0) {
+    message.warning('请至少选择一条数据');
+    return;
+  }
+  Modal.confirm({
+    title: '确认删除',
+    content: `确认删除选中的 ${selectedRowKeys.value.length} 条数据?`,
+    onOk: async () => {
+      await delStation(selectedRowKeys.value);
+      message.success('删除成功');
+      selectedRowKeys.value = [];
+      handleQuery();
+    }
+  });
 };
 
 const handleExport = () => {
   BearJiaUtil.download('config/station/export', queryParams, '场站信息.xlsx');
 };
+const handleColumnSettingsChange = (keys) => {
+  selectedColumnKeys.value = keys;
+  localStorage.setItem(storageKey, JSON.stringify(keys));
+};
+const handleFullscreen = (isFullscreen) => {
+  const el = tableCardRef.value?.$el || tableCardRef.value;
+  if (isFullscreen) {
+    el?.requestFullscreen?.();
+  } else {
+    document.exitFullscreen?.();
+  }
+};
+const handleTableReset = () => {
+  selectedColumnKeys.value = columns.map((c) => c.dataIndex);
+  localStorage.removeItem(storageKey);
+  handleQuery();
+};
 
 handleQuery();
 </script>
@@ -267,9 +353,5 @@ handleQuery();
   .full-input {
     width: 100%;
   }
-
-  .danger-link {
-    color: #ff4d4f;
-  }
 }
 </style>

+ 60 - 129
forecast-backend-vue/src/views/ufo/config/station/moduleConfig.js

@@ -169,194 +169,125 @@ export const stationConfigModules = [
   {
     "key": "device",
     "title": "设备配置",
-    "api": "stationDevice",
+    "api": "device",
     "perm": "config:stationDevice",
     "exportName": "设备配置.xlsx",
+    "stationField": "configStationId",
     "fields": [
       [
-        "deviceName",
+        "name",
         "设备名称",
         "input",
         true
       ],
       [
-        "deviceType",
-        "设备类型",
-        "input"
-      ],
-      [
-        "brand",
-        "品牌",
-        "input"
-      ],
-      [
-        "model",
-        "型号",
-        "input"
+        "configChannelId",
+        "通道",
+        "channel"
       ],
       [
-        "serialNumber",
-        "序列号",
-        "input"
-      ],
-      [
-        "status",
-        "状态",
+        "ilk",
+        "设备类型",
         "select",
         false,
         [
           [
-            "在线",
-            "在线"
+            "气象设备",
+            "WEATHER"
           ],
           [
-            "故障",
-            "故障"
-          ],
-          [
-            "离线",
-            "离线"
-          ],
-          [
-            "维护",
-            "维护"
+            "AGVC设备",
+            "AGVC"
           ]
         ]
       ],
       [
-        "installDate",
-        "安装日期",
-        "date"
+        "remark",
+        "备注",
+        "input"
       ]
     ],
     "columns": [
       "stationName",
-      "deviceName",
-      "deviceType",
-      "brand",
-      "model",
-      "serialNumber",
-      "status",
-      "installDate"
+      "name",
+      "channelName",
+      "ilk",
+      "remark"
     ],
     "search": [
-      "stationId",
-      "deviceName",
-      "deviceType",
-      "status"
+      "configStationId",
+      "name",
+      "ilk"
     ]
   },
   {
     "key": "channel",
     "title": "通道配置",
-    "api": "stationChannel",
+    "api": "channel",
     "perm": "config:stationChannel",
     "exportName": "通道配置.xlsx",
+    "hasStation": false,
     "fields": [
       [
-        "channelName",
-        "通道名称",
+        "code",
+        "编号",
+        "input"
+      ],
+      [
+        "name",
+        "名称",
         "input",
         true
       ],
       [
-        "protocol",
-        "协议",
-        "select",
+        "disabled",
+        "是否停用",
+        "switch",
         false,
         [
           [
-            "FTP",
-            "FTP"
-          ],
-          [
-            "SFTP",
-            "SFTP"
-          ],
-          [
-            "Modbus TCP",
-            "Modbus TCP"
-          ],
-          [
-            "MQTT",
-            "MQTT"
-          ],
-          [
-            "HTTP",
-            "HTTP"
-          ],
-          [
-            "OPC UA",
-            "OPC UA"
+            "是",
+            "1"
           ],
           [
-            "IEC 104",
-            "IEC 104"
+            "否",
+            "0"
           ]
         ]
       ],
       [
-        "host",
-        "主机地址",
-        "input"
-      ],
-      [
-        "port",
-        "端口",
+        "configTransmitId",
+        "通信模块ID",
         "number"
       ],
       [
-        "collectInterval",
-        "采集间隔",
-        "input"
-      ],
-      [
-        "dataType",
-        "数据类型",
+        "transmitConfig",
+        "通信配置",
         "input"
       ],
       [
-        "status",
-        "状态",
-        "select",
-        false,
-        [
-          [
-            "正常",
-            "正常"
-          ],
-          [
-            "延迟",
-            "延迟"
-          ],
-          [
-            "断开",
-            "断开"
-          ]
-        ]
+        "configProtocalId",
+        "协议模块ID",
+        "number"
       ],
       [
-        "lastSyncTime",
-        "最后同步时间",
+        "protocolConfig",
+        "协议配置",
         "input"
       ]
     ],
     "columns": [
-      "stationName",
-      "channelName",
-      "protocol",
-      "host",
-      "port",
-      "collectInterval",
-      "dataType",
-      "status",
-      "lastSyncTime"
+      "code",
+      "name",
+      "disabled",
+      "configTransmitId",
+      "transmitConfig",
+      "configProtocalId",
+      "protocolConfig"
     ],
     "search": [
-      "stationId",
-      "channelName",
-      "protocol",
-      "status"
+      "code",
+      "name"
     ]
   },
   {
@@ -459,8 +390,8 @@ export const stationConfigModules = [
     "hasStation": false,
     "fields": [
       [
-        "configDeviceId",
-        "设备号",
+        "configDeviceName",
+        "设备名称",
         "device",
         true
       ],
@@ -512,7 +443,7 @@ export const stationConfigModules = [
       [
         "willInverse",
         "是否取反",
-        "select",
+        "switch",
         true,
         [
           [
@@ -613,7 +544,7 @@ export const stationConfigModules = [
       "code",
       "addr",
       "category",
-      "configDeviceId",
+      "configDeviceName",
       "initValue",
       "willInverse",
       "kbComposite",

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

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

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

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

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

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

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

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

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

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

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

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

+ 28 - 0
release_note/2026.08.20/readme_tsj.md

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