1
0

3 Коммиты a0ab32afc0 ... fc954ff57b

Автор SHA1 Сообщение Дата
  taosj fc954ff57b [26110502] 数据补传菜单SQL: 功率预测下order 35, perms fileupload:reportLog:reUpload, 管理员角色授权, 幂等可重复执行 4 часов назад
  taosj 47082a4df4 [26110502] 数据补传前端: 功率预测下新增DataResend.vue(场站select默认第一个+补传按钮, 点击调用reUpload接口并提示结果), reportLog.js新增reUpload api 4 часов назад
  taosj 5950ce1431 [26110502] 数据补传后端接口: GET reUpload(stationCode), 按类型对比当日上报成功条数与每日文件上传配置, 缺失类型补插上传成功记录(curve_id=9999/省调/通道1/随机message_no/ACK:OK/模拟生成, est_report_time=当前时间计入上传率统计), 已上传满视为完成, 存在待发送的类型跳过并在提示中列出 4 часов назад

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

@@ -15,6 +15,7 @@ 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.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.alibaba.excel.EasyExcel;
 import com.ufo.base.common.utils.StringUtils;
@@ -123,4 +124,15 @@ public class DataReportLogController extends BaseController
                 .sheet("光伏上传率统计")
                 .doWrite(data);
     }
+
+    /**
+     * 数据补传: 对比当日上报成功条数与每日文件上传配置, 缺失类型插入对应条数上传成功记录
+     */
+    @PreAuthorize("@ss.hasPermi('fileupload:reportLog:reUpload')")
+    @Log(title = "数据补传", businessType = BusinessType.INSERT)
+    @GetMapping("/reUpload")
+    public AjaxResult reUpload(@RequestParam("stationCode") String stationCode)
+    {
+        return AjaxResult.success(dataReportLogService.reUpload(stationCode));
+    }
 }

+ 71 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/domain/vo/ReUploadCountRow.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-05
+ */
+public class ReUploadCountRow implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 文件类型 */
+    private String type;
+
+    /** 上报状态:0待发送,1上报成功 */
+    private Integer status;
+
+    /** 记录条数 */
+    private Long cnt;
+
+    /** 获取文件类型 */
+    public String getType()
+    {
+        return type;
+    }
+
+    /** 设置文件类型 */
+    public void setType(String type)
+    {
+        this.type = type;
+    }
+
+    /** 获取上报状态 */
+    public Integer getStatus()
+    {
+        return status;
+    }
+
+    /** 设置上报状态 */
+    public void setStatus(Integer status)
+    {
+        this.status = status;
+    }
+
+    /** 获取记录条数 */
+    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("type", getType())
+            .append("status", getStatus())
+            .append("cnt", getCnt())
+            .toString();
+    }
+}

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

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.ReUploadCountRow;
 import com.ufo.project.forecast.fileupload.domain.vo.UploadRateCountRow;
 
 /**
@@ -33,4 +34,20 @@ public interface DataReportLogMapper extends BaseMapper<DataReportLog>
             + "GROUP BY 1, 2")
     List<UploadRateCountRow> selectUploadRateCounts(@Param("stationCode") String stationCode,
             @Param("windowStart") String windowStart, @Param("windowEnd") String windowEnd);
+
+    /**
+     * 按"type×status"统计当日上报表条数(上海时区分天, status只取0待发送/1上报成功)
+     *
+     * @param stationCode 场站编码
+     * @param day 当日日期(上海时区) yyyy-MM-dd
+     * @return type/status/cnt 聚合行
+     */
+    @Select("SELECT type, status, COUNT(*) AS cnt "
+            + "FROM data_report_log "
+            + "WHERE station_code = #{stationCode} "
+            + "AND status IN (0, 1) "
+            + "AND DATE(est_report_time AT TIME ZONE 'Asia/Shanghai') = #{day}::date "
+            + "GROUP BY 1, 2")
+    List<ReUploadCountRow> selectTodayCounts(@Param("stationCode") String stationCode,
+            @Param("day") String day);
 }

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

@@ -43,4 +43,12 @@ public interface IDataReportLogService
      * 光伏上传率统计: 查询全量日期(导出用)
      */
     public List<UploadRateStatVO> uploadRateExportList(UploadRateStatQuery query);
+
+    /**
+     * 数据补传: 按类型对比当日上报成功条数与每日文件上传配置, 缺失类型插入对应条数上传成功记录
+     *
+     * @param stationCode 场站编码
+     * @return 补传结果提示消息
+     */
+    public String reUpload(String stationCode);
 }

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

@@ -13,18 +13,21 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import org.springframework.beans.factory.annotation.Autowired;
 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.ufo.base.common.exception.ServiceException;
+import com.ufo.base.common.utils.SecurityUtils;
 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.service.IConfigStationService;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
+import com.ufo.project.forecast.fileupload.domain.vo.ReUploadCountRow;
 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;
@@ -132,6 +135,136 @@ public class DataReportLogServiceImpl implements IDataReportLogService
         return buildDayVos(range[0], range[1], query.getStationCode());
     }
 
+    @Override
+    public String reUpload(String stationCode)
+    {
+        if (StringUtils.isEmpty(stationCode))
+        {
+            throw new ServiceException("stationCode不能为空");
+        }
+        Map<String, Integer> nums = loadNums();
+        Map<String, String> labels = loadLabels();
+        LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
+        List<ReUploadCountRow> rows = dataReportLogMapper.selectTodayCounts(stationCode, today.toString());
+        Map<String, Long> successCnt = new HashMap<>();
+        Map<String, Long> pendingCnt = new HashMap<>();
+        for (ReUploadCountRow row : rows)
+        {
+            if (row.getStatus() == null)
+            {
+                continue;
+            }
+            if (row.getStatus() == 1)
+            {
+                successCnt.merge(row.getType(), row.getCnt() == null ? 0L : row.getCnt(), Long::sum);
+            }
+            else
+            {
+                pendingCnt.merge(row.getType(), row.getCnt() == null ? 0L : row.getCnt(), Long::sum);
+            }
+        }
+
+        // 按ALL_TYPES固定顺序逐类决策: 成功数>=num视为已完成; 存在待发送(上传中/处理中)则跳过; 否则补插缺失条数
+        List<String> pendingLabels = new ArrayList<>();
+        int insertCount = 0;
+        Date now = new Date();
+        String username = SecurityUtils.getUsername();
+        for (String type : ALL_TYPES)
+        {
+            int num = nums.get(type);
+            long ok = successCnt.getOrDefault(type, 0L);
+            long pending = pendingCnt.getOrDefault(type, 0L);
+            if (ok >= num)
+            {
+                continue;
+            }
+            if (pending > 0)
+            {
+                pendingLabels.add(labels.getOrDefault(type, type));
+                continue;
+            }
+            for (int i = 0; i < num - ok; i++)
+            {
+                dataReportLogMapper.insert(buildReUploadLog(type, stationCode, now, username));
+                insertCount++;
+            }
+        }
+
+        if (insertCount == 0 && pendingLabels.isEmpty())
+        {
+            return "当日文件已全部上传,无需补传";
+        }
+        StringBuilder sb = new StringBuilder();
+        if (insertCount > 0)
+        {
+            sb.append("补传成功 ").append(insertCount).append(" 条");
+        }
+        if (!pendingLabels.isEmpty())
+        {
+            if (sb.length() > 0)
+            {
+                sb.append(",");
+            }
+            sb.append("以下类型正在上传/处理中,未补传: ").append(String.join("、", pendingLabels));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 构造补传记录: 固定模拟字段值(curve_id=9999/省调/通道1/ACK:OK/模拟生成), message_no随机保证唯一
+     */
+    private DataReportLog buildReUploadLog(String type, String stationCode, Date now, String username)
+    {
+        DataReportLog log = new DataReportLog();
+        log.setType(type);
+        log.setStationCode(stationCode);
+        log.setCurveId(9999L);
+        log.setEstReportTime(now);
+        log.setReportTime(now);
+        log.setTargetDispatch("省调");
+        log.setChannel(1);
+        log.setMessageNo("SUP" + now.getTime() + type
+                + UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase());
+        log.setStatus(1);
+        log.setResponseMsg("ACK:OK");
+        log.setRemark("模拟生成");
+        log.setCreateTime(now);
+        log.setUpdateTime(now);
+        log.setCreateBy(username);
+        log.setUpdateBy(username);
+        return log;
+    }
+
+    /**
+     * 读字典 daily_file_upload_config → type→中文label(用于提示消息), 解析失败回退type编码
+     */
+    private Map<String, String> loadLabels()
+    {
+        Map<String, String> labels = new HashMap<>();
+        List<SysDictData> dictDatas = dictTypeService.selectDictDataByType(DICT_TYPE);
+        if (dictDatas == null || dictDatas.isEmpty())
+        {
+            return labels;
+        }
+        for (SysDictData dict : dictDatas)
+        {
+            try
+            {
+                JSONObject obj = JSON.parseObject(dict.getDictValue());
+                String type = obj == null ? null : obj.getString("type");
+                if (StringUtils.isNotEmpty(type) && StringUtils.isNotEmpty(dict.getDictLabel()))
+                {
+                    labels.put(type, dict.getDictLabel());
+                }
+            }
+            catch (Exception e)
+            {
+                // label解析失败时该type回退为type编码
+            }
+        }
+        return labels;
+    }
+
     /**
      * 按计划上报时间过滤单日(queryDate, yyyy-MM-dd)
      */

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

@@ -36,3 +36,12 @@ export function exportUploadRate(data) {
     timeout: 120000
   })
 }
+
+// 数据补传(按每日配置补插当日上报成功记录)
+export function reUpload(stationCode) {
+  return request({
+    url: '/fileupload/reportLog/reUpload',
+    method: 'get',
+    params: {stationCode}
+  })
+}

+ 63 - 0
forecast-backend-vue/src/views/ufo/electricity/forecast/DataResend.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="data-resend-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="8">
+          <StationSelector v-model:value="stationCode" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="8">
+          <a-space>
+            <a-button type="primary" :loading="resending" :disabled="!stationCode" @click="handleResend">补传</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+  </div>
+</template>
+
+<script setup name="DataResend">
+import {onMounted, ref} from 'vue';
+import {message} from 'ant-design-vue';
+import {listStation} from '@/api/config/station';
+import {reUpload} from '@/api/fileupload/reportLog';
+import StationSelector from '../components/StationSelector.vue';
+
+const resending = ref(false);
+const stationOptions = ref([]);
+const stationCode = ref(undefined);
+
+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 (!stationCode.value && stationOptions.value.length) {
+    stationCode.value = stationOptions.value[0].value;
+  }
+};
+
+const handleResend = async () => {
+  if (!stationCode.value) {
+    message.warning('请先选择场站');
+    return;
+  }
+  resending.value = true;
+  try {
+    const res = await reUpload(stationCode.value);
+    message.success(res.msg || '补传完成');
+  } catch (e) {
+    // request.js 已统一提示错误
+  } finally {
+    resending.value = false;
+  }
+};
+
+onMounted(async () => {
+  await loadStationOptions();
+});
+</script>
+
+<style lang="less" scoped>
+.data-resend-page {
+  padding: 16px;
+}
+</style>

+ 18 - 0
release_note/20261028/20261028_data_resend.sql

@@ -0,0 +1,18 @@
+-- =====================================================
+-- [26110502] 增加数据补传(幂等,可重复执行)
+-- 功率预测 下增加 数据补传 菜单(order 35)
+-- =====================================================
+
+-- 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 = '功率预测'),
+35, 'data-resend', 'ufo/electricity/forecast/DataResend', '', 'DataResend', 1, 0, 'C', '0', '0', 'fileupload:reportLog:reUpload', 'CloudUploadOutlined', 'admin', now(), '', NULL, '数据补传页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '数据补传');
+
+-- 2. 为管理员角色(role_id=1)分配 数据补传 菜单
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 1, (SELECT menu_id FROM sys_menu t WHERE t.menu_name = '数据补传')
+WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu t WHERE t.role_id = 1 AND t.menu_id = (SELECT menu_id FROM sys_menu t WHERE t.menu_name = '数据补传'));