5 Achegas 0092d1df0b ... 5836076816

Autor SHA1 Mensaxe Data
  taosj 5836076816 [26110501] 光伏上传率统计页面: 场站(必选默认第一个)+起止日期(默认近7天)+查询/导出, 服务端分页表格(10行/页, 日期+9列上传率), 导出文件名光伏上传率统计_截止日.xlsx hai 2 días
  taosj 8667b8a317 [26110501] 光伏上传率统计接口: POST uploadRate/list(手动分页, total=范围天数)与uploadRate/export(全量范围EasyExcel导出, 文件名光伏上传率统计_截止日.xlsx) hai 2 días
  taosj 0fa94686cc [26110501] 光伏上传率统计Mapper聚合SQL与Service: 天xtype成功计数(上海时区分天/只统计status=1/左闭右开含截止日全天), 比率=成功数/字典numx100保留2位, 无记录天0.00, 手动分页窗口, 字典fail fast hai 2 días
  taosj 9556414570 [26110501] 光伏上传率统计新增VO: UploadRateStatQuery查询对象, UploadRateStatVO结果行(日期+9比率, EasyExcel列注解), UploadRateCountRow聚合行 hai 2 días
  taosj b1fff7cc44 [26110501] 增加光伏上传率统计幂等SQL: 字典每日文件上传配置(daily_file_upload_config, 9行JSON, supershort=16其余占位1) + 统计分析下光伏上传率统计菜单(order 15) hai 2 días

+ 41 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/controller/DataReportLogController.java

@@ -2,21 +2,30 @@ package com.ufo.project.forecast.fileupload.controller;
 
 
 import java.io.File;
 import java.io.File;
 import java.io.IOException;
 import java.io.IOException;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.util.List;
 import java.util.List;
+import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
+import com.alibaba.excel.EasyExcel;
 import com.ufo.base.common.utils.StringUtils;
 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.controller.BaseController;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.framework.web.page.TableDataInfo;
 import com.ufo.base.framework.web.page.TableDataInfo;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatQuery;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatVO;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
 
 
 /**
 /**
@@ -79,4 +88,36 @@ public class DataReportLogController extends BaseController
             return AjaxResult.error("读取上报文件失败: " + path);
             return AjaxResult.error("读取上报文件失败: " + path);
         }
         }
     }
     }
+
+    /**
+     * 光伏上传率统计查询(手动分页: total=范围内天数)
+     */
+    @PreAuthorize("@ss.hasPermi('fileupload:reportLog:uploadRate')")
+    @Log(title = "光伏上传率统计", businessType = BusinessType.OTHER)
+    @PostMapping("/uploadRate/list")
+    public TableDataInfo uploadRateList(@RequestBody UploadRateStatQuery query)
+    {
+        long total = dataReportLogService.uploadRateTotalDays(query);
+        List<UploadRateStatVO> rows = dataReportLogService.uploadRatePageList(query);
+        return new TableDataInfo(rows, (int) total);
+    }
+
+    /**
+     * 光伏上传率统计导出(全量日期范围)
+     */
+    @PreAuthorize("@ss.hasPermi('fileupload:reportLog:uploadRateExport')")
+    @Log(title = "光伏上传率统计", businessType = BusinessType.EXPORT)
+    @PostMapping("/uploadRate/export")
+    public void uploadRateExport(@RequestBody UploadRateStatQuery query, HttpServletResponse response) throws IOException
+    {
+        List<UploadRateStatVO> data = dataReportLogService.uploadRateExportList(query);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("光伏上传率统计_" + query.getEndTime().replace("-", "") + ".xlsx",
+                "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
+        EasyExcel.write(response.getOutputStream(), UploadRateStatVO.class)
+                .sheet("光伏上传率统计")
+                .doWrite(data);
+    }
 }
 }

+ 71 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/domain/vo/UploadRateCountRow.java

@@ -0,0 +1,71 @@
+package com.ufo.project.forecast.fileupload.domain.vo;
+
+import java.io.Serializable;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 光伏上传率统计聚合行(仅 Mapper 聚合结果映射用)
+ *
+ * @author ufo
+ * @date 2026-11-04
+ */
+public class UploadRateCountRow implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 上海时区日期 yyyy-MM-dd */
+    private String statDate;
+
+    /** 文件类型 */
+    private String type;
+
+    /** 成功条数 */
+    private Long cnt;
+
+    /** 获取上海时区日期 */
+    public String getStatDate()
+    {
+        return statDate;
+    }
+
+    /** 设置上海时区日期 */
+    public void setStatDate(String statDate)
+    {
+        this.statDate = statDate;
+    }
+
+    /** 获取文件类型 */
+    public String getType()
+    {
+        return type;
+    }
+
+    /** 设置文件类型 */
+    public void setType(String type)
+    {
+        this.type = type;
+    }
+
+    /** 获取成功条数 */
+    public Long getCnt()
+    {
+        return cnt;
+    }
+
+    /** 设置成功条数 */
+    public void setCnt(Long cnt)
+    {
+        this.cnt = cnt;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("statDate", getStatDate())
+            .append("type", getType())
+            .append("cnt", getCnt())
+            .toString();
+    }
+}

+ 103 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/domain/vo/UploadRateStatQuery.java

@@ -0,0 +1,103 @@
+package com.ufo.project.forecast.fileupload.domain.vo;
+
+import java.io.Serializable;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 光伏上传率统计查询对象
+ *
+ * @author ufo
+ * @date 2026-11-04
+ */
+public class UploadRateStatQuery implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 场站编码(必填) */
+    private String stationCode;
+
+    /** 起始时间 yyyy-MM-dd(必填) */
+    private String startTime;
+
+    /** 截止时间 yyyy-MM-dd(必填) */
+    private String endTime;
+
+    /** 页码,缺省按 1 */
+    private Integer pageNum;
+
+    /** 每页行数,缺省按 10 */
+    private Integer pageSize;
+
+    /** 获取场站编码 */
+    public String getStationCode()
+    {
+        return stationCode;
+    }
+
+    /** 设置场站编码 */
+    public void setStationCode(String stationCode)
+    {
+        this.stationCode = stationCode;
+    }
+
+    /** 获取起始时间 */
+    public String getStartTime()
+    {
+        return startTime;
+    }
+
+    /** 设置起始时间 */
+    public void setStartTime(String startTime)
+    {
+        this.startTime = startTime;
+    }
+
+    /** 获取截止时间 */
+    public String getEndTime()
+    {
+        return endTime;
+    }
+
+    /** 设置截止时间 */
+    public void setEndTime(String endTime)
+    {
+        this.endTime = endTime;
+    }
+
+    /** 获取页码 */
+    public Integer getPageNum()
+    {
+        return pageNum;
+    }
+
+    /** 设置页码 */
+    public void setPageNum(Integer pageNum)
+    {
+        this.pageNum = pageNum;
+    }
+
+    /** 获取每页行数 */
+    public Integer getPageSize()
+    {
+        return pageSize;
+    }
+
+    /** 设置每页行数 */
+    public void setPageSize(Integer pageSize)
+    {
+        this.pageSize = pageSize;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("stationCode", getStationCode())
+            .append("startTime", getStartTime())
+            .append("endTime", getEndTime())
+            .append("pageNum", getPageNum())
+            .append("pageSize", getPageSize())
+            .toString();
+    }
+}

+ 194 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/domain/vo/UploadRateStatVO.java

@@ -0,0 +1,194 @@
+package com.ufo.project.forecast.fileupload.domain.vo;
+
+import java.io.Serializable;
+import com.alibaba.excel.annotation.ExcelProperty;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 光伏上传率统计结果行(一行一天)
+ *
+ * @author ufo
+ * @date 2026-11-04
+ */
+public class UploadRateStatVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 日期时间 yyyy-MM-dd */
+    @ExcelProperty(index = 0, value = "日期时间")
+    private String date;
+
+    /** 超短期文件上传率(%) */
+    @ExcelProperty(index = 1, value = "超短期文件上传率(%)")
+    private Double supershortRate;
+
+    /** 短期文件上传率(%) */
+    @ExcelProperty(index = 2, value = "短期文件上传率(%)")
+    private Double shortRate;
+
+    /** 中期文件上传率(%) */
+    @ExcelProperty(index = 3, value = "中期文件上传率(%)")
+    private Double mediumRate;
+
+    /** 环境检测仪数据上传率(%) */
+    @ExcelProperty(index = 4, value = "环境检测仪数据上传率(%)")
+    private Double envlDetectorRate;
+
+    /** 逆变器运行信息文件上传率(%) */
+    @ExcelProperty(index = 5, value = "逆变器运行信息文件上传率(%)")
+    private Double inverterOperationRate;
+
+    /** 升压变数据上传率(%) */
+    @ExcelProperty(index = 6, value = "升压变数据上传率(%)")
+    private Double boosterTransformerRate;
+
+    /** 理论功率文件上传率(%) */
+    @ExcelProperty(index = 7, value = "理论功率文件上传率(%)")
+    private Double theoreticalPowerRate;
+
+    /** 数值天气预报文件上传率(%) */
+    @ExcelProperty(index = 8, value = "数值天气预报文件上传率(%)")
+    private Double numericalWeatherRate;
+
+    /** 发电计划文件上传率(%) */
+    @ExcelProperty(index = 9, value = "发电计划文件上传率(%)")
+    private Double powerGenerationPlanRate;
+
+    /** 获取日期时间 */
+    public String getDate()
+    {
+        return date;
+    }
+
+    /** 设置日期时间 */
+    public void setDate(String date)
+    {
+        this.date = date;
+    }
+
+    /** 获取超短期文件上传率 */
+    public Double getSupershortRate()
+    {
+        return supershortRate;
+    }
+
+    /** 设置超短期文件上传率 */
+    public void setSupershortRate(Double supershortRate)
+    {
+        this.supershortRate = supershortRate;
+    }
+
+    /** 获取短期文件上传率 */
+    public Double getShortRate()
+    {
+        return shortRate;
+    }
+
+    /** 设置短期文件上传率 */
+    public void setShortRate(Double shortRate)
+    {
+        this.shortRate = shortRate;
+    }
+
+    /** 获取中期文件上传率 */
+    public Double getMediumRate()
+    {
+        return mediumRate;
+    }
+
+    /** 设置中期文件上传率 */
+    public void setMediumRate(Double mediumRate)
+    {
+        this.mediumRate = mediumRate;
+    }
+
+    /** 获取环境检测仪数据上传率 */
+    public Double getEnvlDetectorRate()
+    {
+        return envlDetectorRate;
+    }
+
+    /** 设置环境检测仪数据上传率 */
+    public void setEnvlDetectorRate(Double envlDetectorRate)
+    {
+        this.envlDetectorRate = envlDetectorRate;
+    }
+
+    /** 获取逆变器运行信息文件上传率 */
+    public Double getInverterOperationRate()
+    {
+        return inverterOperationRate;
+    }
+
+    /** 设置逆变器运行信息文件上传率 */
+    public void setInverterOperationRate(Double inverterOperationRate)
+    {
+        this.inverterOperationRate = inverterOperationRate;
+    }
+
+    /** 获取升压变数据上传率 */
+    public Double getBoosterTransformerRate()
+    {
+        return boosterTransformerRate;
+    }
+
+    /** 设置升压变数据上传率 */
+    public void setBoosterTransformerRate(Double boosterTransformerRate)
+    {
+        this.boosterTransformerRate = boosterTransformerRate;
+    }
+
+    /** 获取理论功率文件上传率 */
+    public Double getTheoreticalPowerRate()
+    {
+        return theoreticalPowerRate;
+    }
+
+    /** 设置理论功率文件上传率 */
+    public void setTheoreticalPowerRate(Double theoreticalPowerRate)
+    {
+        this.theoreticalPowerRate = theoreticalPowerRate;
+    }
+
+    /** 获取数值天气预报文件上传率 */
+    public Double getNumericalWeatherRate()
+    {
+        return numericalWeatherRate;
+    }
+
+    /** 设置数值天气预报文件上传率 */
+    public void setNumericalWeatherRate(Double numericalWeatherRate)
+    {
+        this.numericalWeatherRate = numericalWeatherRate;
+    }
+
+    /** 获取发电计划文件上传率 */
+    public Double getPowerGenerationPlanRate()
+    {
+        return powerGenerationPlanRate;
+    }
+
+    /** 设置发电计划文件上传率 */
+    public void setPowerGenerationPlanRate(Double powerGenerationPlanRate)
+    {
+        this.powerGenerationPlanRate = powerGenerationPlanRate;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("date", getDate())
+            .append("supershortRate", getSupershortRate())
+            .append("shortRate", getShortRate())
+            .append("mediumRate", getMediumRate())
+            .append("envlDetectorRate", getEnvlDetectorRate())
+            .append("inverterOperationRate", getInverterOperationRate())
+            .append("boosterTransformerRate", getBoosterTransformerRate())
+            .append("theoreticalPowerRate", getTheoreticalPowerRate())
+            .append("numericalWeatherRate", getNumericalWeatherRate())
+            .append("powerGenerationPlanRate", getPowerGenerationPlanRate())
+            .toString();
+    }
+}

+ 22 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/mapper/DataReportLogMapper.java

@@ -1,7 +1,11 @@
 package com.ufo.project.forecast.fileupload.mapper;
 package com.ufo.project.forecast.fileupload.mapper;
 
 
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateCountRow;
 
 
 /**
 /**
  * 功率预测上报日志Mapper接口(MyBatis-Plus, 无需Mapper XML)
  * 功率预测上报日志Mapper接口(MyBatis-Plus, 无需Mapper XML)
@@ -11,4 +15,22 @@ import com.ufo.project.forecast.fileupload.domain.DataReportLog;
  */
  */
 public interface DataReportLogMapper extends BaseMapper<DataReportLog>
 public interface DataReportLogMapper extends BaseMapper<DataReportLog>
 {
 {
+    /**
+     * 按"天×type"统计上报成功条数(只统计status=1, 上海时区分天, 时间范围左闭右开含截止日全天)
+     *
+     * @param stationCode 场站编码
+     * @param windowStart 窗口起始日 yyyy-MM-dd
+     * @param windowEnd 窗口截止日 yyyy-MM-dd
+     * @return statDate/type/cnt 聚合行
+     */
+    @Select("SELECT DATE(est_report_time AT TIME ZONE 'Asia/Shanghai') AS stat_date, "
+            + "type, COUNT(*) AS cnt "
+            + "FROM data_report_log "
+            + "WHERE station_code = #{stationCode} "
+            + "AND status = 1 "
+            + "AND est_report_time >= (#{windowStart}::timestamp AT TIME ZONE 'Asia/Shanghai') "
+            + "AND est_report_time < (#{windowEnd}::timestamp AT TIME ZONE 'Asia/Shanghai') + INTERVAL '1 day' "
+            + "GROUP BY 1, 2")
+    List<UploadRateCountRow> selectUploadRateCounts(@Param("stationCode") String stationCode,
+            @Param("windowStart") String windowStart, @Param("windowEnd") String windowEnd);
 }
 }

+ 17 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/service/IDataReportLogService.java

@@ -2,6 +2,8 @@ package com.ufo.project.forecast.fileupload.service;
 
 
 import java.util.List;
 import java.util.List;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatQuery;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatVO;
 
 
 /**
 /**
  * 功率预测上报日志Service接口
  * 功率预测上报日志Service接口
@@ -26,4 +28,19 @@ public interface IDataReportLogService
      * @return 功率预测上报日志集合
      * @return 功率预测上报日志集合
      */
      */
     public List<DataReportLog> selectDataReportLogList(DataReportLog dataReportLog);
     public List<DataReportLog> selectDataReportLogList(DataReportLog dataReportLog);
+
+    /**
+     * 光伏上传率统计: 日期范围天数(手动分页的total)
+     */
+    public long uploadRateTotalDays(UploadRateStatQuery query);
+
+    /**
+     * 光伏上传率统计: 查询当前页(每页每天一行, 无记录天为0.00)
+     */
+    public List<UploadRateStatVO> uploadRatePageList(UploadRateStatQuery query);
+
+    /**
+     * 光伏上传率统计: 查询全量日期(导出用)
+     */
+    public List<UploadRateStatVO> uploadRateExportList(UploadRateStatQuery query);
 }
 }

+ 187 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/service/impl/DataReportLogServiceImpl.java

@@ -1,7 +1,12 @@
 package com.ufo.project.forecast.fileupload.service.impl;
 package com.ufo.project.forecast.fileupload.service.impl;
 
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
@@ -10,11 +15,19 @@ import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ufo.base.common.exception.ServiceException;
 import com.ufo.base.common.utils.StringUtils;
 import com.ufo.base.common.utils.StringUtils;
+import com.ufo.module.system.domain.SysDictData;
+import com.ufo.module.system.service.ISysDictTypeService;
 import com.ufo.project.config.domain.ConfigStation;
 import com.ufo.project.config.domain.ConfigStation;
 import com.ufo.project.config.service.IConfigStationService;
 import com.ufo.project.config.service.IConfigStationService;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateCountRow;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatQuery;
+import com.ufo.project.forecast.fileupload.domain.vo.UploadRateStatVO;
 import com.ufo.project.forecast.fileupload.mapper.DataReportLogMapper;
 import com.ufo.project.forecast.fileupload.mapper.DataReportLogMapper;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
 
 
@@ -33,6 +46,17 @@ public class DataReportLogServiceImpl implements IDataReportLogService
     @Autowired
     @Autowired
     private IConfigStationService configStationService;
     private IConfigStationService configStationService;
 
 
+    /** 字典类型: 每日文件上传配置 */
+    private static final String DICT_TYPE = "daily_file_upload_config";
+
+    /** 9 类上报文件 type(与字典及VO字段顺序一致) */
+    private static final List<String> ALL_TYPES = Arrays.asList("supershort", "short", "medium",
+            "envlDetector", "inverterOperation", "boosterTransformer",
+            "theoreticalPower", "numericalWeather", "powerGenerationPlan");
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
     /**
     /**
      * 查询功率预测上报日志
      * 查询功率预测上报日志
      *
      *
@@ -72,6 +96,42 @@ public class DataReportLogServiceImpl implements IDataReportLogService
         return list;
         return list;
     }
     }
 
 
+    @Override
+    public long uploadRateTotalDays(UploadRateStatQuery query)
+    {
+        LocalDate[] range = parseRange(query);
+        return ChronoUnit.DAYS.between(range[0], range[1]) + 1;
+    }
+
+    @Override
+    public List<UploadRateStatVO> uploadRatePageList(UploadRateStatQuery query)
+    {
+        LocalDate[] range = parseRange(query);
+        LocalDate start = range[0];
+        LocalDate end = range[1];
+        int pageNum = query.getPageNum() == null || query.getPageNum() < 1 ? 1 : query.getPageNum();
+        int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
+        LocalDate windowStart = start.plusDays((long) (pageNum - 1) * pageSize);
+        if (windowStart.isAfter(end))
+        {
+            // 超出末页: 返回空
+            return new ArrayList<>();
+        }
+        LocalDate windowEnd = windowStart.plusDays(pageSize - 1L);
+        if (windowEnd.isAfter(end))
+        {
+            windowEnd = end;
+        }
+        return buildDayVos(windowStart, windowEnd, query.getStationCode());
+    }
+
+    @Override
+    public List<UploadRateStatVO> uploadRateExportList(UploadRateStatQuery query)
+    {
+        LocalDate[] range = parseRange(query);
+        return buildDayVos(range[0], range[1], query.getStationCode());
+    }
+
     /**
     /**
      * 按计划上报时间过滤单日(queryDate, yyyy-MM-dd)
      * 按计划上报时间过滤单日(queryDate, yyyy-MM-dd)
      */
      */
@@ -135,4 +195,131 @@ public class DataReportLogServiceImpl implements IDataReportLogService
             }
             }
         }
         }
     }
     }
+
+    /**
+     * 查窗口内聚合并按天生成VO(无记录天全0.00)
+     */
+    private List<UploadRateStatVO> buildDayVos(LocalDate windowStart, LocalDate windowEnd, String stationCode)
+    {
+        Map<String, Integer> nums = loadNums();
+        List<UploadRateCountRow> rows = dataReportLogMapper.selectUploadRateCounts(
+                stationCode, windowStart.toString(), windowEnd.toString());
+        Map<String, Map<String, Long>> byDay = new HashMap<>();
+        for (UploadRateCountRow row : rows)
+        {
+            byDay.computeIfAbsent(row.getStatDate(), k -> new HashMap<>()).put(row.getType(), row.getCnt());
+        }
+        List<UploadRateStatVO> result = new ArrayList<>();
+        for (LocalDate day = windowStart; !day.isAfter(windowEnd); day = day.plusDays(1))
+        {
+            Map<String, Long> typeCnt = byDay.get(day.toString());
+            UploadRateStatVO vo = new UploadRateStatVO();
+            vo.setDate(day.toString());
+            vo.setSupershortRate(calcRate("supershort", typeCnt, nums));
+            vo.setShortRate(calcRate("short", typeCnt, nums));
+            vo.setMediumRate(calcRate("medium", typeCnt, nums));
+            vo.setEnvlDetectorRate(calcRate("envlDetector", typeCnt, nums));
+            vo.setInverterOperationRate(calcRate("inverterOperation", typeCnt, nums));
+            vo.setBoosterTransformerRate(calcRate("boosterTransformer", typeCnt, nums));
+            vo.setTheoreticalPowerRate(calcRate("theoreticalPower", typeCnt, nums));
+            vo.setNumericalWeatherRate(calcRate("numericalWeather", typeCnt, nums));
+            vo.setPowerGenerationPlanRate(calcRate("powerGenerationPlan", typeCnt, nums));
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 比率 = 成功条数/num×100, 保留2位小数(HALF_UP); 无记录为0.0
+     */
+    private Double calcRate(String type, Map<String, Long> typeCnt, Map<String, Integer> nums)
+    {
+        long cnt = typeCnt == null ? 0L : typeCnt.getOrDefault(type, 0L);
+        if (cnt == 0)
+        {
+            return 0.0;
+        }
+        return new BigDecimal(cnt * 100).divide(new BigDecimal(nums.get(type)), 2, RoundingMode.HALF_UP)
+                .doubleValue();
+    }
+
+    /**
+     * 参数校验: stationCode/startTime/endTime非空、格式yyyy-MM-dd、start<=end
+     */
+    private LocalDate[] parseRange(UploadRateStatQuery query)
+    {
+        if (StringUtils.isEmpty(query.getStationCode()))
+        {
+            throw new ServiceException("stationCode不能为空");
+        }
+        LocalDate start = parseDay(query.getStartTime(), "起始时间");
+        LocalDate end = parseDay(query.getEndTime(), "截止时间");
+        if (start.isAfter(end))
+        {
+            throw new ServiceException("起始时间不能晚于截止时间");
+        }
+        return new LocalDate[]{start, end};
+    }
+
+    private LocalDate parseDay(String value, String label)
+    {
+        if (StringUtils.isEmpty(value))
+        {
+            throw new ServiceException(label + "不能为空");
+        }
+        try
+        {
+            return LocalDate.parse(value);
+        }
+        catch (Exception e)
+        {
+            throw new ServiceException(label + "格式不正确, 应为yyyy-MM-dd: " + value);
+        }
+    }
+
+    /**
+     * 读字典 daily_file_upload_config → type→num; 未配置/JSON非法/缺项/num非法时fail fast
+     */
+    private Map<String, Integer> loadNums()
+    {
+        List<SysDictData> dictDatas = dictTypeService.selectDictDataByType(DICT_TYPE);
+        if (dictDatas == null || dictDatas.isEmpty())
+        {
+            throw new ServiceException("字典" + DICT_TYPE + "未配置, 请先在字典管理中添加每日文件上传配置");
+        }
+        Map<String, Integer> nums = new HashMap<>();
+        for (SysDictData dict : dictDatas)
+        {
+            String dictValue = dict.getDictValue();
+            try
+            {
+                JSONObject obj = JSON.parseObject(dictValue);
+                String type = obj == null ? null : obj.getString("type");
+                Integer num = obj == null ? null : obj.getInteger("num");
+                if (StringUtils.isEmpty(type) || num == null || num <= 0)
+                {
+                    throw new ServiceException("字典" + DICT_TYPE + "数据[" + dict.getDictLabel()
+                            + "]缺少type或num不合法: " + dictValue);
+                }
+                nums.put(type, num);
+            }
+            catch (ServiceException e)
+            {
+                throw e;
+            }
+            catch (Exception e)
+            {
+                throw new ServiceException("字典" + DICT_TYPE + "数据[" + dict.getDictLabel()
+                        + "]不是合法JSON: " + dictValue);
+            }
+        }
+        for (String type : ALL_TYPES)
+        {
+            if (!nums.containsKey(type))
+            {
+                throw new ServiceException("字典" + DICT_TYPE + "缺少" + type + "的配置");
+            }
+        }
+        return nums;
+    }
 }
 }

+ 20 - 0
forecast-backend-vue/src/api/fileupload/reportLog.js

@@ -16,3 +16,23 @@ export function getReportLogFileContent(id) {
     method: 'get'
     method: 'get'
   })
   })
 }
 }
+
+// 查询光伏上传率统计(手动分页)
+export function listUploadRate(data) {
+  return request({
+    url: '/fileupload/reportLog/uploadRate/list',
+    method: 'post',
+    data
+  })
+}
+
+// 导出光伏上传率统计
+export function exportUploadRate(data) {
+  return request({
+    url: '/fileupload/reportLog/uploadRate/export',
+    method: 'post',
+    data,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 194 - 0
forecast-backend-vue/src/views/ufo/electricity/statanalysis/uploadRateStat.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="upload-rate-stat-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.startTime"
+            class="full-control"
+            format="YYYY-MM-DD"
+            placeholder="起始时间"
+            value-format="YYYY-MM-DD"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.endTime"
+            class="full-control"
+            format="YYYY-MM-DD"
+            placeholder="截止时间"
+            value-format="YYYY-MM-DD"
+          />
+        </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-card :bordered="false" class="content-card">
+      <a-table
+        :columns="columns"
+        :data-source="rows"
+        :loading="loading"
+        :pagination="pagination"
+        :scroll="{x: 1760}"
+        row-key="date"
+        size="middle"
+        @change="handleTableChange"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="UploadRateStat">
+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 {exportUploadRate, listUploadRate} from '@/api/fileupload/reportLog';
+import StationSelector from '../components/StationSelector.vue';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const rows = ref([]);
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total) => `共 ${total} 天`
+});
+const query = reactive({
+  stationCode: undefined,
+  startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+  endTime: dayjs().format('YYYY-MM-DD')
+});
+
+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 !start.isAfter(end);
+});
+
+const columns = [
+  {title: '日期时间', dataIndex: 'date', width: 120},
+  {title: '超短期文件上传率(%)', dataIndex: 'supershortRate', width: 170},
+  {title: '短期文件上传率(%)', dataIndex: 'shortRate', width: 160},
+  {title: '中期文件上传率(%)', dataIndex: 'mediumRate', width: 160},
+  {title: '环境检测仪数据上传率(%)', dataIndex: 'envlDetectorRate', width: 190},
+  {title: '逆变器运行信息文件上传率(%)', dataIndex: 'inverterOperationRate', width: 210},
+  {title: '升压变数据上传率(%)', dataIndex: 'boosterTransformerRate', width: 180},
+  {title: '理论功率文件上传率(%)', dataIndex: 'theoreticalPowerRate', width: 190},
+  {title: '数值天气预报文件上传率(%)', dataIndex: 'numericalWeatherRate', width: 200},
+  {title: '发电计划文件上传率(%)', dataIndex: 'powerGenerationPlanRate', width: 180}
+];
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  const list = (res && res.rows) || [];
+  stationOptions.value = list.map((item) => ({label: item.stationName, value: item.stationCode}));
+  if (!query.stationCode && stationOptions.value.length) {
+    query.stationCode = stationOptions.value[0].value;
+  }
+};
+
+const fetchList = async (pageNum, pageSize) => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await listUploadRate({
+      stationCode: query.stationCode,
+      startTime: query.startTime,
+      endTime: query.endTime,
+      pageNum,
+      pageSize
+    });
+    rows.value = res.rows || [];
+    pagination.total = res.total || 0;
+    pagination.current = pageNum;
+    pagination.pageSize = pageSize;
+  } catch (e) {
+    // request.js 已统一提示错误
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleQuery = () => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+  fetchList(1, pagination.pageSize);
+};
+
+const handleTableChange = (pag) => {
+  fetchList(pag.current, pag.pageSize);
+};
+
+const handleExport = async () => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+  exporting.value = true;
+  try {
+    const res = await exportUploadRate({
+      stationCode: query.stationCode,
+      startTime: query.startTime,
+      endTime: query.endTime
+    });
+    const isFile = await blobValidate(res);
+    if (isFile) {
+      saveAs(new Blob([res]), `光伏上传率统计_${query.endTime.replaceAll('-', '')}.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();
+  handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.upload-rate-stat-page {
+  padding: 16px;
+}
+
+.full-control {
+  width: 100%;
+}
+
+.content-card {
+  margin-top: 16px;
+}
+</style>

+ 38 - 0
release_note/20261028/20261028_upload_rate_stat.sql

@@ -0,0 +1,38 @@
+-- =====================================================
+-- [26110501] 增加光伏上传率统计(幂等,可重复执行)
+-- 1. 字典配置:每日文件上传配置(daily_file_upload_config)
+--    每行 dict_value 为 JSON {"type":"...","num":N},num=该类型每日期望上传数
+--    supershort=16,其余 8 类为占位值 1(可在字典管理界面调整)
+-- 2. 统计分析 下增加 光伏上传率统计 菜单(order 15)
+-- =====================================================
+
+-- 1.1 字典类型: 每日文件上传配置
+INSERT INTO sys_dict_type (dict_id, dict_name, dict_type, status, create_by, create_time, remark)
+SELECT
+nextval(pg_get_serial_sequence('sys_dict_type', 'dict_id')),
+'每日文件上传配置', 'daily_file_upload_config', '0', 'admin', now(), '光伏上传率统计-每日文件上传配置'
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type t WHERE t.dict_type = 'daily_file_upload_config');
+
+-- 1.2 字典数据(先删后插,保证幂等)
+DELETE FROM sys_dict_data WHERE dict_type = 'daily_file_upload_config';
+
+INSERT INTO sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
+VALUES
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 1, '超短期文件', '{"type":"supershort","num":16}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 2, '短期文件', '{"type":"short","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 3, '中期文件', '{"type":"medium","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 4, '环境检测仪数据', '{"type":"envlDetector","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 5, '逆变器运行信息文件', '{"type":"inverterOperation","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 6, '升压变数据', '{"type":"boosterTransformer","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 7, '理论功率文件', '{"type":"theoreticalPower","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 8, '数值天气预报文件', '{"type":"numericalWeather","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数'),
+(nextval(pg_get_serial_sequence('sys_dict_data', 'dict_code')), 9, '发电计划文件', '{"type":"powerGenerationPlan","num":1}', 'daily_file_upload_config', '', '', 'N', '0', 'admin', now(), 'num=每日期望上传数');
+
+-- 2. 增加 光伏上传率统计 二级菜单(挂在 统计分析 下)
+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),
+15, 'upload-rate-stat', 'ufo/electricity/statanalysis/uploadRateStat', '', 'UploadRateStat', 1, 0, 'C', '0', '0', 'fileupload:reportLog:uploadRate', 'BarChartOutlined', 'admin', now(), '', NULL, '光伏上传率统计页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '光伏上传率统计');