前端vue3+后端spring boot导出数据

有个项目需要提供数据导出功能。

该项目前端用vue3编写,后端是spring boot 2,数据库是mysql8。

工作流程是:

1)前端请求数据导出
2)后端接到请求后,开启一个数据导出线程,然后立刻返回信息到前端
3)前端定期轮询,看导出是否已完成
4)后端的数据导出线程,将数据导出,生成文件,存放在后端
5)前端获知导出完成,请求下载文件
6)后端读取文件内容,以流的方式传输给前端,供前端下载

这里面可以看出,数据导出采用了异步方式。为什么采用异步方式,主要是数据量比较大,差不多200万条。同步方式的话,前端必超时。而且200万条记录,后端导出,生成文件,也不能一次性将200万条记录取出,然后生成文件,而是采用分页的方式,比如每次拿一万条,循环提取,直至取完。另外,数据导出也不应该占用主线程,避免其他业务受影响。

下面是详细介绍。

一、前端

前端一共请求3个接口。一个请求导出,一个导出状态查询,一个下载。首先向后端请求导出,由于是异步的,请求发出后,立即返回;此后定期查询导出状态;发现导出状态已完成后,即向后端请求下载。

1、请求数据导出

点击按钮“开始导出”

<el-button v-if="exportState.ready" type="primary" plain class="float-right"@click="startExport">开始导出</el-button>
import { start as startApi, checkStatus as checkStatusApi, exportCsv } from "@/modules/api/sensor/export.js";async function startExport() {const valid = await form1.value.validate(); // 等待表单验证通过if (valid) {startApi(formState).then((res) => {waiting();const taskId = res.data;checkExportStatus(taskId);//查询导出状态});}
}

2、查询导出状态

import { saveAs } from 'file-saver'; // 或者自己写 blob 下载逻辑function checkExportStatus(taskId) {//使用定时器const timer1 = setInterval(async () => {try {const res = await checkStatusApi(taskId);const { status, filename } = res.data;if (status === 'DONE') {clearInterval(timer1);const response = await exportCsv(filename);//向后端发出下载请求const blob = new Blob([response], { type: 'text/csv;charset=utf-8' });saveAs(blob, getFileName());//保存文件,一个第三方组件done();} else if (status === 'ERROR') {clearInterval(timer1);over();ElMessage.error('导出失败: ' + filename);}} catch (err) {clearInterval(timer1);over();ElMessage.error('导出失败: ' + err.message || '网络异常');}}, 1000);
}

3、下载

上面代码中的exportCsv。

4、向后端请求的API

import { request, requestBlob } from "@/request";const prefix = "/export";export const exportCsv = (filename) => {return requestBlob({url: prefix + "/download/" + filename,method: "get",});
};
export const start = (params) => {console.log(params);return request({url: prefix + "/start",params,method: "post",});
};
export const checkStatus = (taskId) => {return request({url: prefix + "/status/" + taskId,method: "get",});
};

二、后端

后端需要做比较多的工作。为了支持可能数量巨大的数据的下载请求,不致影响主线程性能,同时也避免客户端因为等待超时而断连,需要开辟新线程、异步方式来处理数据导出,因此需要引入线程池和任务管理。

后端的处理导出的流程是,接收到前端的请求后,从数据库中获取数据,如果数据量特别大,还要分页,采用循环多次查找;然后将数据输出到csv格式的文件中,文件保存在服务器。当前端侦察到导出完成,即请求下载,后端就将文件内容读出,以二进制流的形式返回给前端。前端侦察导出状态时,后端会将文件名返回给前端。为什么后端要先生成文件,貌似多此一举呢?原因是整个导出过程是异步的,后端没有办法一步到位将流返回给前端。

1、线程池

首先要注册一个线程池。

@Configuration
@EnableAsync
public class AsyncConfig {@Beanpublic TaskExecutor executor(){ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); //核心线程数executor.setMaxPoolSize(20);  //最大线程数executor.setQueueCapacity(1000); //队列大小executor.setKeepAliveSeconds(300); //线程最大空闲时间executor.setThreadNamePrefix("fsx-Executor-"); //指定用于新创建的线程名称的前缀。executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize(); // ✅ 加上这一行return executor;}
}

2、任务管理

@Component
public class TaskManager {// 任务状态:PENDING, DONE, ERRORprivate final Map<String, String> taskStatusMap = new ConcurrentHashMap<>();private final Map<String, String> taskResultMap = new ConcurrentHashMap<>();public void markTaskDone(String taskId, String fileName) {taskStatusMap.put(taskId, "DONE");taskResultMap.put(taskId, fileName);}public void markTaskFailed(String taskId, String errorMsg) {taskStatusMap.put(taskId, "ERROR");taskResultMap.put(taskId, errorMsg);}public String getStatus(String taskId) {return taskStatusMap.getOrDefault(taskId, "PENDING");}public String getResult(String taskId) {return taskResultMap.get(taskId);}public void clearTask(String taskId) {taskStatusMap.remove(taskId);taskResultMap.remove(taskId);}
}

3、控制器

@RestController
@RequestMapping("/export")
public class ExportController {@AutowiredSensorDataService sensorDataService;@Autowiredprivate TaskManager taskManager;//任务管理@Value("${export.path}")private String exportPath;//请求导出@PostMapping("/start")@ResponseBodypublic Result startExport(ExportParam paramObj) {String taskId = UUID.randomUUID().toString();sensorDataService.asyncExportData(taskId, paramObj); // 异步执行return Result.ok().put("data",taskId);}//查询导出状态@GetMapping("/status/{taskId}")@ResponseBodypublic Result checkStatus(@PathVariable String taskId) {String status = taskManager.getStatus(taskId);String filename = taskManager.getResult(taskId);Map<String, String> data = new HashMap<>();data.put("taskId", taskId);data.put("status", status);data.put("filename", filename);//文件名(不含路径)return Result.ok().put("data",data);}//下载导出文件@GetMapping(value = "/download/{fileName:.+}")public void exportFile(@PathVariable String fileName,HttpServletResponse response) {try {// 2. 构建文件路径(确保与写入时一致)String filePath = exportPath + fileName;// 3. 设置响应头response.setContentType("text/csv");response.setCharacterEncoding("utf-8");response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));// 4. 读取文件内容并写入响应输出流try (InputStream inputStream = new FileInputStream(filePath)) {byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {response.getOutputStream().write(buffer, 0, bytesRead);}response.getOutputStream().flush();}} catch (Exception e) {try {response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件下载失败:" + e.getMessage());} catch (IOException ex) {ex.printStackTrace();}e.printStackTrace();}}
}

4、service

@Service
public class SensorDataServiceImpl implements SensorDataService {@Value("${export.path}")private String exportPath;@Value("${export.page-size:10000}")private Integer exportPageSize;//每页多少条记录@Override@Async("executor")  // 指定使用定义的线程池public void asyncExportData(String taskId, ExportParam param) {System.out.println("当前线程: " + Thread.currentThread().getName());try {// 执行导出逻辑exportDataToFile(taskId, param);} catch (Exception e) {System.err.println("导出数据时发生异常:");e.printStackTrace();}}// 数据导出主方法private void exportDataToFile(String taskId, ExportParam param) throws Exception {// 1. 定义文件路径(请确保该目录存在且有写权限)String exportDir = exportPath;String fileName = getDownloadDataFileName(param);String filePath = exportDir + fileName;// 2. 创建 CSV 文件并写入表头try (CSVWriter writer = new CSVWriter(new FileWriter(filePath))) {// 获取表头(根据 param 可以动态生成)String[] headers = getHeaders(param);writer.writeNext(headers);// 3. 分页查询数据int pageNumber = 0;int pageSize = exportPageSize; // 每页查询 5000 条boolean hasMore = true;while (hasMore) {String sql = getSqlWithPagination(param, pageSize, pageNumber);List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);if (rows.isEmpty()) {hasMore = false;} else {for (Map<String, Object> row : rows) {String[] rowData = formatRow(row, headers);writer.writeNext(rowData);}writer.flush(); // 及时刷新,避免内存积压pageNumber++;}}// 4. 导出完成后记录任务状态和文件路径taskManager.markTaskDone(taskId, fileName);} catch (Exception e) {// 记录错误信息taskManager.markTaskFailed(taskId, e.getMessage());throw e;}}// 构建带分页的 SQLprivate String getSqlWithPagination(ExportParam paramObj, int pageSize, int pageNumber) {String baseSql = getSql(paramObj);return (baseSql.length() > 0) ? baseSql + " LIMIT " + pageSize + " OFFSET " + (pageNumber * pageSize) : "";}
}

三、效果

1、组件全貌

在这里插入图片描述

2、点击开始导出

在这里插入图片描述

3、导出成功

在这里插入图片描述

四、小结

有的表数据量特别巨大,一个月有记录几百万条。按分页查找,每页5万条记录处理,下载一个月数据需要2、3分钟。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/95905.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/95905.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于RK3588的微电网协调控制器:实现分布式能源的智能调控与优化运行

微电网协调控制器方案通过集成先进算法和实时数据技术&#xff0c;实现分布式能源的光伏、储能、风电等设备的智能协调与优化运行‌12。关键功能包括&#xff1a;‌协同优化调度‌&#xff1a;采用模型预测控制&#xff08;MPC&#xff09;动态调整光伏出力、储能充放电策略和负…

机器学习——TF-IDF文本特征提取评估权重 + Jieba 库进行分词(以《红楼梦》为例)

使用 Jieba 库进行 TF-IDF 关键词提取&#xff08;以《红楼梦》为例&#xff09;在中文文本分析中&#xff0c;TF-IDF&#xff08;Term Frequency - Inverse Document Frequency&#xff09; 是最常用的关键词提取方法之一。它通过评估词在单个文档中的出现频率和在所有文档中的…

一周学会Matplotlib3 Python 数据可视化-多子图及布局实现

锋哥原创的Matplotlib3 Python数据可视化视频教程&#xff1a; 2026版 Matplotlib3 Python 数据可视化 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 课程介绍 本课程讲解利用python进行数据可视化 科研绘图-Matplotlib&#xff0c;学习Matplotlib图形参数基本设置&…

Spark执行计划与UI分析

文章目录1.Spark任务阶段划分1.1 job&#xff0c;stage与task1.2 job划分1.3 stage和task划分2.任务执行时机3.task内部数据存储与流动4.根据sparkUI了解Spark执行计划4.1查看job和stage4.2 查看DAG图4.3查看task1.Spark任务阶段划分 1.1 job&#xff0c;stage与task 首先根据…

16-docker的容器监控方案-prometheus实战篇

文章目录一.前置知识1.监控与报警2.监控系统的设计3.监控系统的分类二、prometheus概述1.什么是prometheus2.prometheus的历史3.为什么要学习prometheus4.prometheus的使用场景5.prometheus的宏观架构图6.prometheus软件下载地址三、部署prometheus server监控软件1.同步集群时…

集成电路学习:什么是Image Processing图像处理

Image Processing,即图像处理,是计算机视觉、人工智能、多媒体等领域的重要基础。它利用计算机对图像进行分析、加工和处理,以达到预期目的的技术。以下是对图像处理的详细解析: 一、定义与分类 定义: 图像处理是指用计算机对图像进行分析,以达到所需结果的技术,又称…

基于Android的随身小管家APP的设计与实现/基于SSM框架的财务管理系统/android Studio/java/原生开发

基于Android的随身小管家APP的设计与实现/基于SSM框架/android Studio/java/原生开发

Web 开发 16

1 在 JavaScript&#xff08;包括 JSX&#xff09;中&#xff0c;函数体的写法和返回值处理在 JavaScript&#xff08;包括 JSX&#xff09;中&#xff0c;函数体的写法和返回值处理确实有一些简洁的语法规则&#xff0c;尤其是在箭头函数中。这些规则常常让人混淆&#xff0c;…

超高车辆碰撞预警系统如何帮助提升城市立交隧道安全?

超高车辆带来的安全隐患立交桥和隧道的设计通常基于常规车辆的高度标准。然而&#xff0c;随着重型运输业和超高货车的增加&#xff0c;很多超高车辆会误入这些限高区域&#xff0c;造成潜在的安全隐患。超高车辆与立交桥梁或隧道顶盖发生碰撞时&#xff0c;可能导致结构受损&a…

三种变量类型在局部与全局作用域的区别

一、基本概念作用域&#xff08;Scope&#xff09;&#xff1a; 全局作用域&#xff1a;定义在所有函数外部的变量或函数&#xff0c;具有文件作用域&#xff0c;生命周期为整个程序运行期间。局部作用域&#xff1a;定义在函数、块&#xff08;如 {}&#xff09;或类内部的变量…

InfluxDB 数据迁移工具:跨数据库同步方案(二)

六、基于 API 的同步方案实战6.1 API 原理介绍InfluxDB 提供的 HTTP API 是实现数据迁移的重要途径。通过这个 API&#xff0c;我们可以向 InfluxDB 发送 HTTP 请求&#xff0c;以实现数据的读取和写入操作。在数据读取方面&#xff0c;使用GET请求&#xff0c;通过指定数据库名…

JVM安全点轮询汇编函数解析

OpenJDK 17 源码的实现逻辑&#xff0c;handle_polling_page_exception 函数在方法返回时的调用流程如下&#xff1a;调用流程分析&#xff1a;栈水印检查触发跳转&#xff1a;当线程执行方法返回前的安全点轮询时&#xff08;MacroAssembler::safepoint_poll 中 at_returntrue…

Linux怎么查看服务器开放和启用的端口

在 Linux 系统中&#xff0c;可以通过以下方法查看 服务器开放和启用的端口。以下是详细的步骤和工具&#xff0c;适用于不同场景。1. 使用 ss 查看开放的端口ss 是一个现代化工具&#xff0c;用于显示网络连接和监听的端口。1.1 查看正在监听的端口运行以下命令&#xff1a;ba…

XF 306-2025 阻燃耐火电线电缆检测

近几年随着我国经济快速的发展&#xff0c;电气火灾呈现高发趋势&#xff0c;鉴于电线电缆火灾的危险性&#xff0c;国家制定了阻燃&#xff0c;耐火电线电缆的标准&#xff0c;为企业&#xff0c;建设方&#xff0c;施工方等的生产&#xff0c;选材提供了指引。XF 306-2025 阻…

【Java|第二十篇】面向对象(十)——枚举类

目录 &#xff08;四&#xff09;面向对象&#xff1a; 12、枚举类&#xff1a; &#xff08;1&#xff09;概述&#xff1a; &#xff08;2&#xff09;枚举类的定义格式&#xff1a; &#xff08;3&#xff09;编译与反编译&#xff1a; &#xff08;4&#xff09;Enum类…

第二十一天-OLED显示实验

一、OLED显示原理1、OLED名词解释OLED可以自发光&#xff0c;无需背光光源。2、正点原子OLED模块模块总体概述模块接口模式选择MCU与模块外部连接8080并口读写过程OLED显存因为要进行显示&#xff0c;所以需要有显存。显存容量为128 x 8 byte&#xff0c;一个点用一位表示。SSD…

会议系统核心流程详解:创建、加入与消息交互

一、系统架构概览 会议系统采用"主进程线程池进程池"的分层架构&#xff0c;实现高并发与业务隔离&#xff1a; #mermaid-svg-fDJ5Ja5L3rqPkby0 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-fDJ5Ja5L3r…

Spring 创建 Bean 的 8 种主要方式

Spring&#xff08;尤其是 Spring Boot&#xff09;提供了多种方式来让容器创建和管理 Bean。Component、Configuration Bean、EnableConfigurationProperties 都是常见方式。 下面我为你系统地梳理 Spring 创建 Bean 的所有主要方式&#xff0c;并说明它们的使用场景和区别。…

React 第七十节 Router中matchRoutes的使用详解及注意事项

前言 matchRoutes 是 React Router v6 提供的一个核心工具函数&#xff0c;主要用于匹配路由配置与当前路径。它在服务端渲染&#xff08;SSR&#xff09;、数据预加载、权限校验等场景中非常实用。下面详细解析其用法、注意事项和案例分析&#xff1a; 1、基本用法 import { m…

iSCSI服务配置全指南(含服务器与客户端)

iSCSI服务配置全指南&#xff08;含服务器与客户端&#xff09;一、iSCSI简介 1. 概念 互联网小型计算机系统接口&#xff08;Internet Small Computer System Interface&#xff0c;简称iSCSI&#xff09;是一种基于TCP/IP的协议&#xff0c;其核心功能是通过IP网络仿真SCSI高…