Procházet zdrojové kódy

[0017] 增加功率频率分布统计功能: DataController 新增日功率频率分布查询/导出接口(按日10区间统计条数与占比, endDay不含当天, 无数据日期补零), 新增 selectActualPowerRange 左闭右开取数, 前端新增 statanalysis 页面与菜单SQL

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
taosj před 1 týdnem
rodič
revize
60141c117a

+ 88 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/controller/DataController.java

@@ -4,11 +4,14 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
@@ -40,6 +43,8 @@ import com.ufo.project.data.domain.vo.UltraShortTermPowerForecastReportVO;
 import com.ufo.project.data.domain.vo.PowerErrorStatQuery;
 import com.ufo.project.data.domain.vo.PowerErrorStatCurveVO;
 import com.ufo.project.data.domain.vo.PowerErrorStatAccuracyVO;
+import com.ufo.project.data.domain.vo.PowerFrequencyDistributionQuery;
+import com.ufo.project.data.domain.vo.PowerFrequencyDistributionVO;
 import com.ufo.project.data.service.IPvPlantCumulativeReportService;
 import com.ufo.project.data.service.IMediumTermPowerForecastReportService;
 import com.ufo.project.data.service.IShortTermPowerForecastReportService;
@@ -49,6 +54,8 @@ import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecas
 import com.ufo.project.forecast.shortterm.power.service.IDataShortTermPowerForecastIssuedService;
 import com.ufo.project.forecast.shortterm.weather.domain.DataShortTermWeatherForecastIssued;
 import com.ufo.project.forecast.shortterm.weather.service.IDataShortTermWeatherForecastIssuedService;
+import com.ufo.project.config.domain.ConfigStation;
+import com.ufo.project.config.service.IConfigStationService;
 import com.ufo.project.model.domain.DataElectricityLocal;
 import com.ufo.project.model.service.IDataElectricityLocalService;
 import com.ufo.project.weather.domain.DataWeatherLocal;
@@ -93,6 +100,9 @@ public class DataController extends BaseController
     @Autowired
     private IPowerErrorStatService powerErrorStatService;
 
+    @Autowired
+    private IConfigStationService configStationService;
+
     /**
      * 辐照度功率对比
      */
@@ -294,6 +304,84 @@ public class DataController extends BaseController
         util.exportExcel(response, list, "功率误差统计精度");
     }
 
+    /**
+     * 日功率频率分布统计
+     */
+    @PreAuthorize("@ss.hasPermi('data:powerFrequencyDistribution:list')")
+    @PostMapping("/powerFrequencyDistribution")
+    public TableDataInfo powerFrequencyDistribution(@RequestBody PowerFrequencyDistributionQuery query)
+    {
+        List<PowerFrequencyDistributionVO> list = buildFrequencyDistributionList(
+                query.getStationCode(), query.getStartDay(), query.getEndDay());
+        return getDataTable(list);
+    }
+
+    /**
+     * 日功率频率分布统计导出
+     */
+    @PreAuthorize("@ss.hasPermi('data:powerFrequencyDistribution:export')")
+    @Log(title = "功率频率分布统计", businessType = BusinessType.EXPORT)
+    @PostMapping("/powerFrequencyDistribution/export")
+    public void powerFrequencyDistributionExport(HttpServletResponse response, @RequestBody PowerFrequencyDistributionQuery query)
+    {
+        List<PowerFrequencyDistributionVO> list = buildFrequencyDistributionList(
+                query.getStationCode(), query.getStartDay(), query.getEndDay());
+        ExcelUtil<PowerFrequencyDistributionVO> util = new ExcelUtil<>(PowerFrequencyDistributionVO.class);
+        util.exportExcel(response, list, "日功率频率分布统计");
+    }
+
+    /**
+     * 构建日功率频率分布统计列表(按日分组,无数据的日期补零)
+     * <p>取数区间左闭右开: tm &gt;= startDay 00:00(+08:00), tm &lt; endDay 00:00(+08:00)。</p>
+     */
+    private List<PowerFrequencyDistributionVO> buildFrequencyDistributionList(String stationCode, String startDay, String endDay)
+    {
+        Long stationId = resolveStationId(stationCode);
+        List<DataElectricityLocal> rows = stationId == null
+                ? new ArrayList<>()
+                : electricityLocalService.selectActualPowerRange(stationId, startDay, endDay);
+
+        LocalDate start = LocalDate.parse(startDay);
+        LocalDate end = LocalDate.parse(endDay);
+        LinkedHashMap<LocalDate, PowerFrequencyDistributionVO> dayMap = new LinkedHashMap<>();
+        for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1))
+        {
+            dayMap.put(day, new PowerFrequencyDistributionVO(day.toString()));
+        }
+        if (rows != null)
+        {
+            for (DataElectricityLocal row : rows)
+            {
+                if (row.getTm() == null || row.getActualPower() == null) continue;
+                LocalDate day = row.getTm().toInstant().atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
+                PowerFrequencyDistributionVO vo = dayMap.get(day);
+                if (vo == null) continue;
+                vo.bump(row.getActualPower() / 1000);
+            }
+        }
+        List<PowerFrequencyDistributionVO> result = new ArrayList<>(dayMap.values());
+        for (PowerFrequencyDistributionVO vo : result)
+        {
+            vo.buildPercentAndText();
+        }
+        return result;
+    }
+
+    /**
+     * 场站编码解析为场站id,未匹配返回 null
+     */
+    private Long resolveStationId(String stationCode)
+    {
+        ConfigStation condition = new ConfigStation();
+        condition.setStationCode(stationCode);
+        List<ConfigStation> list = configStationService.selectConfigStationList(condition);
+        if (list == null || list.isEmpty())
+        {
+            return null;
+        }
+        return list.get(0).getId();
+    }
+
     /*
      * 辐照度累计值报表
      */

+ 54 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/PowerFrequencyDistributionQuery.java

@@ -0,0 +1,54 @@
+package com.ufo.project.data.domain.vo;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 日功率频率分布统计查询参数
+ *
+ * @author ufo
+ * @date 2026-10-22
+ */
+public class PowerFrequencyDistributionQuery
+{
+    /** 场站编码 */
+    @NotBlank(message = "场站编码不能为空")
+    private String stationCode;
+
+    /** 起始日期 yyyy-MM-dd(包含) */
+    @NotBlank(message = "起始日期不能为空")
+    private String startDay;
+
+    /** 结束日期 yyyy-MM-dd(不包含) */
+    @NotBlank(message = "结束日期不能为空")
+    private String endDay;
+
+    public String getStationCode()
+    {
+        return stationCode;
+    }
+
+    public void setStationCode(String stationCode)
+    {
+        this.stationCode = stationCode;
+    }
+
+    public String getStartDay()
+    {
+        return startDay;
+    }
+
+    public void setStartDay(String startDay)
+    {
+        this.startDay = startDay;
+    }
+
+    public String getEndDay()
+    {
+        return endDay;
+    }
+
+    public void setEndDay(String endDay)
+    {
+        this.endDay = endDay;
+    }
+}

+ 545 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/PowerFrequencyDistributionVO.java

@@ -0,0 +1,545 @@
+package com.ufo.project.data.domain.vo;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 日功率频率分布统计视图对象
+ * <p>每个区间包含条数(Num)、占比(Percent,%)与组合展示文本(Text,形如 18(29.51%),用于Excel导出列)。</p>
+ *
+ * @author ufo
+ * @date 2026-10-22
+ */
+public class PowerFrequencyDistributionVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 日期 yyyy-MM-dd */
+    @Excel(name = "时间")
+    private String day;
+
+    /** 数据总条数 */
+    @Excel(name = "数据总条数")
+    private int totalRecord;
+
+    /** [0,2)MW 条数 */
+    private int lt2Num;
+
+    /** [0,2)MW 占比(%) */
+    private BigDecimal lt2Percent;
+
+    /** [0,2)MW 组合展示文本 */
+    @Excel(name = "[0-2)MW")
+    private String lt2Text;
+
+    /** [2,4)MW 条数 */
+    private int lt4Num;
+
+    /** [2,4)MW 占比(%) */
+    private BigDecimal lt4Percent;
+
+    /** [2,4)MW 组合展示文本 */
+    @Excel(name = "[2-4)MW")
+    private String lt4Text;
+
+    /** [4,6)MW 条数 */
+    private int lt6Num;
+
+    /** [4,6)MW 占比(%) */
+    private BigDecimal lt6Percent;
+
+    /** [4,6)MW 组合展示文本 */
+    @Excel(name = "[4-6)MW")
+    private String lt6Text;
+
+    /** [6,8)MW 条数 */
+    private int lt8Num;
+
+    /** [6,8)MW 占比(%) */
+    private BigDecimal lt8Percent;
+
+    /** [6,8)MW 组合展示文本 */
+    @Excel(name = "[6-8)MW")
+    private String lt8Text;
+
+    /** [8,10)MW 条数 */
+    private int lt10Num;
+
+    /** [8,10)MW 占比(%) */
+    private BigDecimal lt10Percent;
+
+    /** [8,10)MW 组合展示文本 */
+    @Excel(name = "[8-10)MW")
+    private String lt10Text;
+
+    /** [10,12)MW 条数 */
+    private int lt12Num;
+
+    /** [10,12)MW 占比(%) */
+    private BigDecimal lt12Percent;
+
+    /** [10,12)MW 组合展示文本 */
+    @Excel(name = "[10-12)MW")
+    private String lt12Text;
+
+    /** [12,14)MW 条数 */
+    private int lt14Num;
+
+    /** [12,14)MW 占比(%) */
+    private BigDecimal lt14Percent;
+
+    /** [12,14)MW 组合展示文本 */
+    @Excel(name = "[12-14)MW")
+    private String lt14Text;
+
+    /** [14,16)MW 条数 */
+    private int lt16Num;
+
+    /** [14,16)MW 占比(%) */
+    private BigDecimal lt16Percent;
+
+    /** [14,16)MW 组合展示文本 */
+    @Excel(name = "[14-16)MW")
+    private String lt16Text;
+
+    /** [16,18)MW 条数 */
+    private int lt18Num;
+
+    /** [16,18)MW 占比(%) */
+    private BigDecimal lt18Percent;
+
+    /** [16,18)MW 组合展示文本 */
+    @Excel(name = "[16-18)MW")
+    private String lt18Text;
+
+    /** [18,+∞)MW 条数 */
+    private int ge18Num;
+
+    /** [18,+∞)MW 占比(%) */
+    private BigDecimal ge18Percent;
+
+    /** [18,+∞)MW 组合展示文本 */
+    @Excel(name = "≥18MW")
+    private String ge18Text;
+
+    public PowerFrequencyDistributionVO()
+    {
+    }
+
+    public PowerFrequencyDistributionVO(String day)
+    {
+        this.day = day;
+    }
+
+    /**
+     * 按功率值(MW)累加到对应区间,负值计入 [0,2)MW 区间
+     */
+    public void bump(double mw)
+    {
+        totalRecord++;
+        if (mw < 2)
+        {
+            lt2Num++;
+        }
+        else if (mw < 4)
+        {
+            lt4Num++;
+        }
+        else if (mw < 6)
+        {
+            lt6Num++;
+        }
+        else if (mw < 8)
+        {
+            lt8Num++;
+        }
+        else if (mw < 10)
+        {
+            lt10Num++;
+        }
+        else if (mw < 12)
+        {
+            lt12Num++;
+        }
+        else if (mw < 14)
+        {
+            lt14Num++;
+        }
+        else if (mw < 16)
+        {
+            lt16Num++;
+        }
+        else if (mw < 18)
+        {
+            lt18Num++;
+        }
+        else
+        {
+            ge18Num++;
+        }
+    }
+
+    /**
+     * 计算各区间占比(保留两位小数,四舍五入)与组合展示文本 num(percent%)
+     */
+    public void buildPercentAndText()
+    {
+        lt2Percent = percentOf(lt2Num);
+        lt2Text = buildText(lt2Num, lt2Percent);
+        lt4Percent = percentOf(lt4Num);
+        lt4Text = buildText(lt4Num, lt4Percent);
+        lt6Percent = percentOf(lt6Num);
+        lt6Text = buildText(lt6Num, lt6Percent);
+        lt8Percent = percentOf(lt8Num);
+        lt8Text = buildText(lt8Num, lt8Percent);
+        lt10Percent = percentOf(lt10Num);
+        lt10Text = buildText(lt10Num, lt10Percent);
+        lt12Percent = percentOf(lt12Num);
+        lt12Text = buildText(lt12Num, lt12Percent);
+        lt14Percent = percentOf(lt14Num);
+        lt14Text = buildText(lt14Num, lt14Percent);
+        lt16Percent = percentOf(lt16Num);
+        lt16Text = buildText(lt16Num, lt16Percent);
+        lt18Percent = percentOf(lt18Num);
+        lt18Text = buildText(lt18Num, lt18Percent);
+        ge18Percent = percentOf(ge18Num);
+        ge18Text = buildText(ge18Num, ge18Percent);
+    }
+
+    private BigDecimal percentOf(int num)
+    {
+        if (totalRecord == 0)
+        {
+            return new BigDecimal("0.00");
+        }
+        return BigDecimal.valueOf((long) num * 100L)
+                .divide(BigDecimal.valueOf(totalRecord), 2, RoundingMode.HALF_UP);
+    }
+
+    private String buildText(int num, BigDecimal percent)
+    {
+        return num + "(" + percent.toPlainString() + "%)";
+    }
+
+    public String getDay()
+    {
+        return day;
+    }
+
+    public void setDay(String day)
+    {
+        this.day = day;
+    }
+
+    public int getTotalRecord()
+    {
+        return totalRecord;
+    }
+
+    public void setTotalRecord(int totalRecord)
+    {
+        this.totalRecord = totalRecord;
+    }
+
+    public int getLt2Num()
+    {
+        return lt2Num;
+    }
+
+    public void setLt2Num(int lt2Num)
+    {
+        this.lt2Num = lt2Num;
+    }
+
+    public BigDecimal getLt2Percent()
+    {
+        return lt2Percent;
+    }
+
+    public void setLt2Percent(BigDecimal lt2Percent)
+    {
+        this.lt2Percent = lt2Percent;
+    }
+
+    public String getLt2Text()
+    {
+        return lt2Text;
+    }
+
+    public void setLt2Text(String lt2Text)
+    {
+        this.lt2Text = lt2Text;
+    }
+
+    public int getLt4Num()
+    {
+        return lt4Num;
+    }
+
+    public void setLt4Num(int lt4Num)
+    {
+        this.lt4Num = lt4Num;
+    }
+
+    public BigDecimal getLt4Percent()
+    {
+        return lt4Percent;
+    }
+
+    public void setLt4Percent(BigDecimal lt4Percent)
+    {
+        this.lt4Percent = lt4Percent;
+    }
+
+    public String getLt4Text()
+    {
+        return lt4Text;
+    }
+
+    public void setLt4Text(String lt4Text)
+    {
+        this.lt4Text = lt4Text;
+    }
+
+    public int getLt6Num()
+    {
+        return lt6Num;
+    }
+
+    public void setLt6Num(int lt6Num)
+    {
+        this.lt6Num = lt6Num;
+    }
+
+    public BigDecimal getLt6Percent()
+    {
+        return lt6Percent;
+    }
+
+    public void setLt6Percent(BigDecimal lt6Percent)
+    {
+        this.lt6Percent = lt6Percent;
+    }
+
+    public String getLt6Text()
+    {
+        return lt6Text;
+    }
+
+    public void setLt6Text(String lt6Text)
+    {
+        this.lt6Text = lt6Text;
+    }
+
+    public int getLt8Num()
+    {
+        return lt8Num;
+    }
+
+    public void setLt8Num(int lt8Num)
+    {
+        this.lt8Num = lt8Num;
+    }
+
+    public BigDecimal getLt8Percent()
+    {
+        return lt8Percent;
+    }
+
+    public void setLt8Percent(BigDecimal lt8Percent)
+    {
+        this.lt8Percent = lt8Percent;
+    }
+
+    public String getLt8Text()
+    {
+        return lt8Text;
+    }
+
+    public void setLt8Text(String lt8Text)
+    {
+        this.lt8Text = lt8Text;
+    }
+
+    public int getLt10Num()
+    {
+        return lt10Num;
+    }
+
+    public void setLt10Num(int lt10Num)
+    {
+        this.lt10Num = lt10Num;
+    }
+
+    public BigDecimal getLt10Percent()
+    {
+        return lt10Percent;
+    }
+
+    public void setLt10Percent(BigDecimal lt10Percent)
+    {
+        this.lt10Percent = lt10Percent;
+    }
+
+    public String getLt10Text()
+    {
+        return lt10Text;
+    }
+
+    public void setLt10Text(String lt10Text)
+    {
+        this.lt10Text = lt10Text;
+    }
+
+    public int getLt12Num()
+    {
+        return lt12Num;
+    }
+
+    public void setLt12Num(int lt12Num)
+    {
+        this.lt12Num = lt12Num;
+    }
+
+    public BigDecimal getLt12Percent()
+    {
+        return lt12Percent;
+    }
+
+    public void setLt12Percent(BigDecimal lt12Percent)
+    {
+        this.lt12Percent = lt12Percent;
+    }
+
+    public String getLt12Text()
+    {
+        return lt12Text;
+    }
+
+    public void setLt12Text(String lt12Text)
+    {
+        this.lt12Text = lt12Text;
+    }
+
+    public int getLt14Num()
+    {
+        return lt14Num;
+    }
+
+    public void setLt14Num(int lt14Num)
+    {
+        this.lt14Num = lt14Num;
+    }
+
+    public BigDecimal getLt14Percent()
+    {
+        return lt14Percent;
+    }
+
+    public void setLt14Percent(BigDecimal lt14Percent)
+    {
+        this.lt14Percent = lt14Percent;
+    }
+
+    public String getLt14Text()
+    {
+        return lt14Text;
+    }
+
+    public void setLt14Text(String lt14Text)
+    {
+        this.lt14Text = lt14Text;
+    }
+
+    public int getLt16Num()
+    {
+        return lt16Num;
+    }
+
+    public void setLt16Num(int lt16Num)
+    {
+        this.lt16Num = lt16Num;
+    }
+
+    public BigDecimal getLt16Percent()
+    {
+        return lt16Percent;
+    }
+
+    public void setLt16Percent(BigDecimal lt16Percent)
+    {
+        this.lt16Percent = lt16Percent;
+    }
+
+    public String getLt16Text()
+    {
+        return lt16Text;
+    }
+
+    public void setLt16Text(String lt16Text)
+    {
+        this.lt16Text = lt16Text;
+    }
+
+    public int getLt18Num()
+    {
+        return lt18Num;
+    }
+
+    public void setLt18Num(int lt18Num)
+    {
+        this.lt18Num = lt18Num;
+    }
+
+    public BigDecimal getLt18Percent()
+    {
+        return lt18Percent;
+    }
+
+    public void setLt18Percent(BigDecimal lt18Percent)
+    {
+        this.lt18Percent = lt18Percent;
+    }
+
+    public String getLt18Text()
+    {
+        return lt18Text;
+    }
+
+    public void setLt18Text(String lt18Text)
+    {
+        this.lt18Text = lt18Text;
+    }
+
+    public int getGe18Num()
+    {
+        return ge18Num;
+    }
+
+    public void setGe18Num(int ge18Num)
+    {
+        this.ge18Num = ge18Num;
+    }
+
+    public BigDecimal getGe18Percent()
+    {
+        return ge18Percent;
+    }
+
+    public void setGe18Percent(BigDecimal ge18Percent)
+    {
+        this.ge18Percent = ge18Percent;
+    }
+
+    public String getGe18Text()
+    {
+        return ge18Text;
+    }
+
+    public void setGe18Text(String ge18Text)
+    {
+        this.ge18Text = ge18Text;
+    }
+}

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

@@ -41,6 +41,17 @@ public interface DataElectricityLocalMapper
     public List<DataElectricityLocal> selectActualPowerList(@Param("configStationId") Long configStationId,
             @Param("startTime") String startTime, @Param("endTime") String endTime);
 
+    /**
+     * 查询场站时间段内的实发功率数据(左闭右开)
+     *
+     * @param configStationId 场站id
+     * @param startTime 起始时间(ISO 8601带时区字符串,如 2026-08-02T00:00:00+08:00,包含)
+     * @param endTime 结束时间(ISO 8601带时区字符串,不包含)
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    public List<DataElectricityLocal> selectActualPowerRange(@Param("configStationId") Long configStationId,
+            @Param("startTime") String startTime, @Param("endTime") String endTime);
+
     /**
      * 新增功率采样数据
      * 

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

@@ -47,9 +47,20 @@ public interface IDataElectricityLocalService
      */
     public List<DataElectricityLocal> selectActualPowerList(Long configStationId, String startTime, String endTime);
 
+    /**
+     * 查询场站日期区间内的实发功率数据(左闭右开)
+     * <p>tm &gt;= startDay 00:00:00(+08:00) and tm &lt; endDay 00:00:00(+08:00),即结束日期不含当天。</p>
+     *
+     * @param configStationId 场站id
+     * @param startDay 起始日期 yyyy-MM-dd(包含)
+     * @param endDay 结束日期 yyyy-MM-dd(不包含)
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    public List<DataElectricityLocal> selectActualPowerRange(Long configStationId, String startDay, String endDay);
+
     /**
      * 新增功率采样数据
-     * 
+     *
      * @param dataElectricityLocal 功率采样数据
      * @return 结果
      */

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

@@ -77,6 +77,23 @@ public class DataElectricityLocalServiceImpl implements IDataElectricityLocalSer
         return dataElectricityLocalMapper.selectActualPowerList(configStationId, start, end);
     }
 
+    /**
+     * 查询场站日期区间内的实发功率数据(左闭右开)
+     * <p>tm &gt;= startDay 00:00:00(+08:00) and tm &lt; endDay 00:00:00(+08:00),即结束日期不含当天。</p>
+     *
+     * @param configStationId 场站id
+     * @param startDay 起始日期 yyyy-MM-dd(包含)
+     * @param endDay 结束日期 yyyy-MM-dd(不包含)
+     * @return 功率采样数据集合(按 tm 升序)
+     */
+    @Override
+    public List<DataElectricityLocal> selectActualPowerRange(Long configStationId, String startDay, String endDay)
+    {
+        String start = startDay + "T00:00:00+08:00";
+        String end = endDay + "T00:00:00+08:00";
+        return dataElectricityLocalMapper.selectActualPowerRange(configStationId, start, end);
+    }
+
     /**
      * 将多种时间格式转换为 ISO 8601 带时区字符串(如 2026-08-02T00:00:00+08:00)。
      * 仅日期格式默认使用 Asia/Shanghai 时区。

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

@@ -50,6 +50,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by tm asc
     </select>
 
+    <!--
+        左闭右开: tm >= startTime and tm < endTime,用于"结束日期不含当天"的按日统计。
+    -->
+    <select id="selectActualPowerRange" 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=",">

+ 21 - 0
forecast-backend-vue/src/api/data/powerFrequencyDistribution.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+// 查询日功率频率分布统计
+export function listPowerFrequencyDistribution(data) {
+  return request({
+    url: '/data/powerFrequencyDistribution',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出日功率频率分布统计
+export function exportPowerFrequencyDistribution(data) {
+  return request({
+    url: '/data/powerFrequencyDistribution/export',
+    method: 'post',
+    data: data,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 191 - 0
forecast-backend-vue/src/views/ufo/electricity/statanalysis/powerFrequencyDistribution.vue

@@ -0,0 +1,191 @@
+<template>
+  <div class="power-frequency-distribution-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationCode" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.startDay"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="起始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.endDay"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="结束时间(不含当天)"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="4">
+          <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-card :bordered="false" class="content-card">
+      <BusinessDataTable
+        :columns="columns"
+        :data-source="rows"
+        :pagination="{ pageSize: 10 }"
+        :row-key="'day'"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="PowerFrequencyDistribution">
+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 {
+  exportPowerFrequencyDistribution,
+  listPowerFrequencyDistribution
+} from '@/api/data/powerFrequencyDistribution';
+import BusinessDataTable from '../components/BusinessDataTable.vue';
+import StationSelector from '../components/StationSelector.vue';
+import {normalizeRows} from '../components/dataAdapter';
+import {toStationOptions} from '../components/stationAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const rows = ref([]);
+
+const query = reactive({
+  stationCode: undefined,
+  startDay: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+  endDay: dayjs().format('YYYY-MM-DD')
+});
+
+const queryValid = computed(() => {
+  if (!query.stationCode || !query.startDay || !query.endDay) return false;
+  const start = dayjs(query.startDay);
+  const end = dayjs(query.endDay);
+  if (!start.isValid() || !end.isValid()) return false;
+  return !start.isAfter(end);
+});
+
+const renderBucket = (key) => ({record}) => {
+  const num = record[`${key}Num`] ?? 0;
+  const percent = record[`${key}Percent`] ?? '0.00';
+  return `${num}(${percent}%)`;
+};
+
+const bucketColumn = (key, title) => ({
+  title,
+  key: `${key}Text`,
+  width: 100,
+  align: 'right',
+  customRender: renderBucket(key)
+});
+
+const columns = [
+  {title: '时间', dataIndex: 'day', key: 'day', width: 120},
+  {title: '数据总条数', dataIndex: 'totalRecord', key: 'totalRecord', width: 110, align: 'right'},
+  bucketColumn('lt2', '[0-2)MW'),
+  bucketColumn('lt4', '[2-4)MW'),
+  bucketColumn('lt6', '[4-6)MW'),
+  bucketColumn('lt8', '[6-8)MW'),
+  bucketColumn('lt10', '[8-10)MW'),
+  bucketColumn('lt12', '[10-12)MW'),
+  bucketColumn('lt14', '[12-14)MW'),
+  bucketColumn('lt16', '[14-16)MW'),
+  bucketColumn('lt18', '[16-18)MW'),
+  bucketColumn('ge18', '≥18MW')
+];
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  stationOptions.value = toStationOptions(normalizeRows(res));
+  if (!query.stationCode && stationOptions.value.length) {
+    query.stationCode = stationOptions.value[0].value;
+  }
+};
+
+const buildParams = () => ({
+  stationCode: query.stationCode,
+  startDay: query.startDay,
+  endDay: query.endDay
+});
+
+const fetchList = async () => {
+  const res = await listPowerFrequencyDistribution(buildParams());
+  rows.value = normalizeRows(res);
+};
+
+const handleQuery = async () => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+
+  loading.value = true;
+  try {
+    await fetchList();
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleExport = async () => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+
+  exporting.value = true;
+  try {
+    const res = await exportPowerFrequencyDistribution(buildParams());
+    const isFile = await blobValidate(res);
+    if (isFile) {
+      const name = `功率频率分布统计-${query.startDay}-${query.endDay}.xlsx`;
+      saveAs(new Blob([res]), name);
+      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>
+.power-frequency-distribution-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+.content-card {
+  margin-top: 16px;
+}
+</style>

+ 9 - 0
release_note/20261022/20261022_power_frequency_distribution_menu.sql

@@ -0,0 +1,9 @@
+-- 功率频率分布统计菜单补丁(任务 2610017, 幂等, 可重复执行)
+-- 1. 增加 功率频率分布统计 二级菜单(挂在 统计分析 下)
+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)
+SELECT
+(SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu),
+'功率频率分布统计',
+(SELECT menu_id FROM sys_menu t WHERE t.menu_name = '统计分析' AND t.parent_id = 0),
+21, 'power-frequency-distribution', 'ufo/electricity/statanalysis/powerFrequencyDistribution', '', 'PowerFrequencyDistribution', 1, 0, 'C', '0', '0', 'data:powerFrequencyDistribution:list', 'BarChartOutlined', 'admin', now(), '', NULL, '功率频率分布统计页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '功率频率分布统计');