Procházet zdrojové kódy

[0016] 完成上传状态查询页面: 上报日志列表支持文件类别/上传通道/查询时间筛选并回填场站名称, 新增报文原文内容查看接口与统计分析下页面菜单

taosj před 1 týdnem
rodič
revize
92b184096d

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

@@ -1,12 +1,20 @@
 package com.ufo.project.forecast.fileupload.controller;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import com.ufo.base.common.utils.StringUtils;
 import com.ufo.base.framework.web.controller.BaseController;
+import com.ufo.base.framework.web.domain.AjaxResult;
 import com.ufo.base.framework.web.page.TableDataInfo;
 import com.ufo.project.forecast.fileupload.domain.DataReportLog;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
@@ -21,6 +29,10 @@ import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
 @RequestMapping("/fileupload/reportLog")
 public class DataReportLogController extends BaseController
 {
+    /** 上报文件根目录, content_path为其下的相对路径 */
+    @Value("${project.forecast.reportfile.rootfolder}")
+    private String reportFileRootFolder;
+
     @Autowired
     private IDataReportLogService dataReportLogService;
 
@@ -35,4 +47,39 @@ public class DataReportLogController extends BaseController
         List<DataReportLog> list = dataReportLogService.selectDataReportLogList(dataReportLog);
         return getDataTable(list);
     }
+
+    /**
+     * 查询上报报文原文内容(按日志主键)
+     */
+    @PreAuthorize("@ss.hasPermi('fileupload:reportLog:list')")
+    @GetMapping("/file/content/{id}")
+    public AjaxResult fileContent(@PathVariable("id") Long id)
+    {
+        DataReportLog log = dataReportLogService.selectDataReportLogById(id);
+        if (log == null)
+        {
+            return AjaxResult.error("上报日志不存在");
+        }
+        String path = log.getContentPath();
+        if (StringUtils.isEmpty(path))
+        {
+            return AjaxResult.error("该记录无上报名文文件");
+        }
+        File file = new File(reportFileRootFolder, path);
+        if (!file.exists() || !file.isFile())
+        {
+            return AjaxResult.error("上报文件不存在: " + path);
+        }
+        try
+        {
+            byte[] bytes = Files.readAllBytes(file.toPath());
+            AjaxResult result = AjaxResult.success();
+            result.put("content", new String(bytes, StandardCharsets.UTF_8));
+            return result;
+        }
+        catch (IOException e)
+        {
+            return AjaxResult.error("读取上报文件失败: " + path);
+        }
+    }
 }

+ 55 - 0
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/domain/DataReportLog.java

@@ -25,9 +25,16 @@ public class DataReportLog
     /** 文件类型:supershort|short|medium */
     private String type;
 
+    /** 场站编码 */
+    private String stationCode;
+
     /** 关联预测曲线ID(forecast_curve) */
     private Long curveId;
 
+    /** 计划上报时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date estReportTime;
+
     /** 上报发起时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date reportTime;
@@ -75,6 +82,14 @@ public class DataReportLog
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date endTime;
 
+    /** 查询日期(查询用, yyyy-MM-dd, 按计划上报时间est_report_time过滤单日) */
+    @TableField(exist = false)
+    private String queryDate;
+
+    /** 场站名称(非表字段, 按station_code关联config_station回填) */
+    @TableField(exist = false)
+    private String stationName;
+
     public void setId(Long id)
     {
         this.id = id;
@@ -102,6 +117,24 @@ public class DataReportLog
     {
         return curveId;
     }
+    public void setStationCode(String stationCode)
+    {
+        this.stationCode = stationCode;
+    }
+
+    public String getStationCode()
+    {
+        return stationCode;
+    }
+    public void setEstReportTime(Date estReportTime)
+    {
+        this.estReportTime = estReportTime;
+    }
+
+    public Date getEstReportTime()
+    {
+        return estReportTime;
+    }
     public void setReportTime(Date reportTime)
     {
         this.reportTime = reportTime;
@@ -228,13 +261,33 @@ public class DataReportLog
     {
         return endTime;
     }
+    public void setQueryDate(String queryDate)
+    {
+        this.queryDate = queryDate;
+    }
+
+    public String getQueryDate()
+    {
+        return queryDate;
+    }
+    public void setStationName(String stationName)
+    {
+        this.stationName = stationName;
+    }
+
+    public String getStationName()
+    {
+        return stationName;
+    }
 
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
             .append("id", getId())
             .append("type", getType())
+            .append("stationCode", getStationCode())
             .append("curveId", getCurveId())
+            .append("estReportTime", getEstReportTime())
             .append("reportTime", getReportTime())
             .append("targetDispatch", getTargetDispatch())
             .append("channel", getChannel())
@@ -249,6 +302,8 @@ public class DataReportLog
             .append("updateBy", getUpdateBy())
             .append("startTime", getStartTime())
             .append("endTime", getEndTime())
+            .append("queryDate", getQueryDate())
+            .append("stationName", getStationName())
             .toString();
     }
 }

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

@@ -11,6 +11,14 @@ import com.ufo.project.forecast.fileupload.domain.DataReportLog;
  */
 public interface IDataReportLogService
 {
+    /**
+     * 查询功率预测上报日志
+     *
+     * @param id 功率预测上报日志主键
+     * @return 功率预测上报日志
+     */
+    public DataReportLog selectDataReportLogById(Long id);
+
     /**
      * 查询功率预测上报日志列表
      *

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

@@ -1,10 +1,19 @@
 package com.ufo.project.forecast.fileupload.service.impl;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.ufo.base.common.utils.StringUtils;
+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.mapper.DataReportLogMapper;
 import com.ufo.project.forecast.fileupload.service.IDataReportLogService;
@@ -21,6 +30,21 @@ public class DataReportLogServiceImpl implements IDataReportLogService
     @Autowired
     private DataReportLogMapper dataReportLogMapper;
 
+    @Autowired
+    private IConfigStationService configStationService;
+
+    /**
+     * 查询功率预测上报日志
+     *
+     * @param id 功率预测上报日志主键
+     * @return 功率预测上报日志
+     */
+    @Override
+    public DataReportLog selectDataReportLogById(Long id)
+    {
+        return dataReportLogMapper.selectById(id);
+    }
+
     /**
      * 查询功率预测上报日志列表
      *
@@ -32,15 +56,83 @@ public class DataReportLogServiceImpl implements IDataReportLogService
     {
         LambdaQueryWrapper<DataReportLog> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StringUtils.isNotEmpty(dataReportLog.getType()), DataReportLog::getType, dataReportLog.getType())
+                .eq(StringUtils.isNotEmpty(dataReportLog.getStationCode()), DataReportLog::getStationCode, dataReportLog.getStationCode())
                 .eq(dataReportLog.getCurveId() != null, DataReportLog::getCurveId, dataReportLog.getCurveId())
                 .like(StringUtils.isNotEmpty(dataReportLog.getTargetDispatch()), DataReportLog::getTargetDispatch, dataReportLog.getTargetDispatch())
                 .eq(StringUtils.isNotEmpty(dataReportLog.getMessageNo()), DataReportLog::getMessageNo, dataReportLog.getMessageNo())
                 .eq(dataReportLog.getChannel() != null, DataReportLog::getChannel, dataReportLog.getChannel())
                 .eq(dataReportLog.getStatus() != null, DataReportLog::getStatus, dataReportLog.getStatus())
                 .ge(dataReportLog.getStartTime() != null, DataReportLog::getReportTime, dataReportLog.getStartTime())
-                .le(dataReportLog.getEndTime() != null, DataReportLog::getReportTime, dataReportLog.getEndTime())
-                .orderByDesc(DataReportLog::getReportTime)
+                .le(dataReportLog.getEndTime() != null, DataReportLog::getReportTime, dataReportLog.getEndTime());
+        applyEstReportDate(wrapper, dataReportLog.getQueryDate());
+        wrapper.orderByDesc(DataReportLog::getEstReportTime)
                 .orderByDesc(DataReportLog::getId);
-        return dataReportLogMapper.selectList(wrapper);
+        List<DataReportLog> list = dataReportLogMapper.selectList(wrapper);
+        fillStationName(list);
+        return list;
+    }
+
+    /**
+     * 按计划上报时间过滤单日(queryDate, yyyy-MM-dd)
+     */
+    private void applyEstReportDate(LambdaQueryWrapper<DataReportLog> wrapper, String queryDate)
+    {
+        if (StringUtils.isEmpty(queryDate))
+        {
+            return;
+        }
+        try
+        {
+            LocalDate day = LocalDate.parse(queryDate);
+            Date start = Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant());
+            Date end = Date.from(day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+            wrapper.ge(DataReportLog::getEstReportTime, start)
+                    .lt(DataReportLog::getEstReportTime, end);
+        }
+        catch (Exception e)
+        {
+            // queryDate格式非法时忽略该条件
+        }
+    }
+
+    /**
+     * 按station_code关联config_station回填场站名称
+     */
+    private void fillStationName(List<DataReportLog> list)
+    {
+        if (list == null || list.isEmpty())
+        {
+            return;
+        }
+        Set<String> stationCodes = new LinkedHashSet<>();
+        for (DataReportLog item : list)
+        {
+            if (StringUtils.isNotEmpty(item.getStationCode()))
+            {
+                stationCodes.add(item.getStationCode());
+            }
+        }
+        if (stationCodes.isEmpty())
+        {
+            return;
+        }
+        Map<String, String> nameMap = new HashMap<>();
+        for (String stationCode : stationCodes)
+        {
+            ConfigStation query = new ConfigStation();
+            query.setStationCode(stationCode);
+            List<ConfigStation> stations = configStationService.selectConfigStationList(query);
+            if (stations != null && !stations.isEmpty())
+            {
+                nameMap.put(stationCode, stations.get(0).getStationName());
+            }
+        }
+        for (DataReportLog item : list)
+        {
+            if (StringUtils.isNotEmpty(item.getStationCode()) && nameMap.containsKey(item.getStationCode()))
+            {
+                item.setStationName(nameMap.get(item.getStationCode()));
+            }
+        }
     }
 }

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

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询功率预测上报日志列表
+export function listReportLog(query) {
+  return request({
+    url: '/fileupload/reportLog/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询上报报文原文内容
+export function getReportLogFileContent(id) {
+  return request({
+    url: '/fileupload/reportLog/file/content/' + id,
+    method: 'get'
+  })
+}

+ 232 - 0
forecast-backend-vue/src/views/ufo/electricity/statanalysis/reportLogQuery.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="report-log-query-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <a-select
+            v-model:value="query.type"
+            :options="typeOptions"
+            placeholder="文件类别"
+            allow-clear
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="6">
+          <a-select
+            v-model:value="query.channel"
+            :options="channelOptions"
+            placeholder="上传通道"
+            allow-clear
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="6">
+          <a-date-picker
+            v-model:value="query.queryDate"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="查询时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="6">
+          <a-space>
+            <a-button type="primary" :loading="loading" @click="handleQuery">查询</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"
+        row-key="id"
+        size="middle"
+        @change="handleTableChange"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.dataIndex === 'type'">
+            {{ typeLabelMap[record.type] || record.type }}
+          </template>
+          <template v-else-if="column.dataIndex === 'status'">
+            <a-tag :color="statusColorMap[record.status] || 'default'">
+              {{ statusLabelMap[record.status] ?? record.status }}
+            </a-tag>
+          </template>
+          <template v-else-if="column.dataIndex === 'contentPath'">
+            {{ fileName(record) }}
+          </template>
+          <template v-else-if="column.key === 'action'">
+            <a-button
+              type="link"
+              size="small"
+              :disabled="!record.contentPath"
+              @click="handleViewFile(record)"
+            >查看文件</a-button>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+
+    <a-modal
+      v-model:open="fileModalVisible"
+      title="文件内容"
+      :footer="null"
+      width="720px"
+      destroy-on-close
+    >
+      <a-spin :spinning="fileLoading">
+        <pre class="file-content">{{ fileContent }}</pre>
+      </a-spin>
+    </a-modal>
+  </div>
+</template>
+
+<script setup name="ReportLogQuery">
+import {onMounted, reactive, ref} from 'vue';
+import {message} from 'ant-design-vue';
+import dayjs from 'dayjs';
+import {getReportLogFileContent, listReportLog} from '@/api/fileupload/reportLog';
+
+const loading = ref(false);
+const rows = ref([]);
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showTotal: (total) => `共 ${total} 条`
+});
+
+const query = reactive({
+  type: undefined,
+  channel: undefined,
+  queryDate: dayjs().format('YYYY-MM-DD')
+});
+
+const typeOptions = [
+  {label: '超短期功率预报', value: 'supershort'},
+  {label: '短期功率预报', value: 'short'},
+  {label: '中期功率预报', value: 'medium'}
+];
+
+const channelOptions = [
+  {label: 'IEC104', value: 1},
+  {label: 'XML文件', value: 2},
+  {label: 'FTP', value: 3},
+  {label: 'MQTT', value: 4}
+];
+
+const typeLabelMap = {
+  supershort: '超短期功率预报',
+  short: '短期功率预报',
+  medium: '中期功率预报'
+};
+
+const statusLabelMap = {
+  0: '待发送',
+  1: '上报成功',
+  2: '上报失败',
+  3: '超时'
+};
+
+const statusColorMap = {
+  0: 'default',
+  1: 'success',
+  2: 'error',
+  3: 'warning'
+};
+
+const columns = [
+  {title: '场站名称', dataIndex: 'stationName', width: 180},
+  {title: '上报文件类型', dataIndex: 'type', width: 140},
+  {title: '计划上报时间', dataIndex: 'estReportTime', width: 170},
+  {title: '实际上报时间', dataIndex: 'reportTime', width: 170},
+  {title: '状态', dataIndex: 'status', width: 100},
+  {title: '文件名称', dataIndex: 'contentPath', width: 240, ellipsis: true},
+  {title: '操作', key: 'action', width: 100, fixed: 'right'}
+];
+
+const fileName = (record) => {
+  if (!record.contentPath) return '';
+  const parts = record.contentPath.split(/[\\/]/);
+  return parts[parts.length - 1];
+};
+
+const fetchList = async (pageNum, pageSize) => {
+  loading.value = true;
+  try {
+    const res = await listReportLog({
+      pageNum,
+      pageSize,
+      type: query.type,
+      channel: query.channel,
+      queryDate: query.queryDate
+    });
+    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 = () => {
+  fetchList(1, pagination.pageSize);
+};
+
+const handleTableChange = (pag) => {
+  fetchList(pag.current, pag.pageSize);
+};
+
+const fileModalVisible = ref(false);
+const fileLoading = ref(false);
+const fileContent = ref('');
+
+const handleViewFile = async (record) => {
+  fileModalVisible.value = true;
+  fileLoading.value = true;
+  fileContent.value = '';
+  try {
+    const res = await getReportLogFileContent(record.id);
+    fileContent.value = res.content ?? '';
+  } catch (e) {
+    // request.js 已统一提示错误
+  } finally {
+    fileLoading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchList(1, pagination.pageSize);
+});
+</script>
+
+<style lang="less" scoped>
+.report-log-query-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+.content-card {
+  margin-top: 16px;
+}
+.file-content {
+  max-height: 600px;
+  overflow: auto;
+  margin: 0;
+  padding: 12px;
+  background: #f5f5f5;
+  border-radius: 4px;
+  white-space: pre-wrap;
+  word-break: break-all;
+  font-size: 12px;
+}
+</style>

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

@@ -0,0 +1,9 @@
+-- 上传状态查询菜单补丁(任务 2610016, 幂等, 可重复执行)
+-- 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),
+20, 'report-log-query', 'ufo/electricity/statanalysis/reportLogQuery', '', 'ReportLogQuery', 1, 0, 'C', '0', '0', 'fileupload:reportLog:list', 'CloudUploadOutlined', 'admin', now(), '', NULL, '上传状态查询页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '上传状态查询');