diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java index ffa49e024..2f586a3d5 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java @@ -1,8 +1,11 @@ package org.dromara.common.excel.utils; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.io.resource.ClassPathResource; +import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ZipUtil; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import lombok.AccessLevel; @@ -20,14 +23,17 @@ import org.dromara.common.excel.convert.ExcelBigNumberConvert; import org.dromara.common.excel.core.*; import org.dromara.common.excel.handler.DataWriteHandler; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; /** * Excel相关处理 @@ -93,6 +99,138 @@ public class ExcelUtil { } } + /** + * 大数据量Excel导出 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param clazz 实体类 + */ + public static void exportExcelZip(List list, String sheetName, Class clazz, HttpServletResponse response) { + exportExcelZip(list, sheetName, clazz, response, 999); + } + + /** + * 大数据量Excel导出 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param clazz 实体类 + * @param pageSize 每页条数 + */ + public static void exportExcelZip(List list, String sheetName, Class clazz, HttpServletResponse response, int pageSize) { + // 数据分页 + List> pageList = ListUtil.partition(list, pageSize); + // 只有一页,直接导出普通Excel + if (pageList.size() <= 1) { + exportSingleExcel(list, sheetName, clazz, response); + return; + } + // 多线程生成所有Excel文件(字节数组) + Map excelMap = buildExcelZipData(pageList, sheetName, clazz); + // 写入ZIP并下载 + writeExcelZipResponse(sheetName, response, excelMap); + } + + /** + * 导出【单文件】Excel + */ + private static void exportSingleExcel(List list, String sheetName, Class clazz, HttpServletResponse response) { + try { + resetResponse(sheetName, response); + ServletOutputStream os = response.getOutputStream(); + exportExcel(list, sheetName, clazz, false, os, null); + } catch (IOException e) { + throw new RuntimeException("导出Excel异常", e); + } + } + + /** + * 多线程并行生成多个Excel文件 + * 使用虚拟线程,高并发、低资源占用 + * + * @return Map<文件名, 文件字节数组> + */ + private static Map buildExcelZipData(List> pageList, String sheetName, Class clazz) { + // 有序Map,保证文件按页码顺序打包 + Map excelMap = new LinkedHashMap<>(pageList.size()); + List>> futures = new ArrayList<>(pageList.size()); + + // 使用虚拟线程池执行导出任务 + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + // 1. 提交所有分页导出任务 + for (int i = 0; i < pageList.size(); i++) { + int pageNum = i + 1; + List pageData = pageList.get(i); + futures.add(executor.submit(() -> buildExcelZipEntry(pageData, sheetName, clazz, pageNum))); + } + // 2. 获取所有线程执行结果 + for (Future> future : futures) { + Map.Entry excel = getExcelZipEntry(future); + excelMap.put(excel.getKey(), excel.getValue()); + } + } + return excelMap; + } + + /** + * 单页Excel生成任务(线程执行单元) + * + * @param pageData 当前页数据 + * @param pageNum 当前页码 + * @return 文件名 + 文件字节 + */ + private static Map.Entry buildExcelZipEntry(List pageData, String sheetName, Class clazz, int pageNum) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + String exportSheetName = sheetName + "_第" + pageNum + "页"; + exportExcel(pageData, exportSheetName, clazz, false, bos, null); + return Map.entry(exportSheetName + ".xlsx", bos.toByteArray()); + } catch (Exception e) { + throw new RuntimeException("第" + pageNum + "页Excel生成失败", e); + } + } + + /** + * 安全获取异步任务结果 + * 处理中断异常、执行异常,保证任务稳定 + */ + private static Map.Entry getExcelZipEntry(Future> future) { + try { + return future.get(); + } catch (InterruptedException e) { + // 恢复中断标志 + Thread.currentThread().interrupt(); + throw new RuntimeException("Excel导出线程被中断", e); + } catch (ExecutionException e) { + throw new RuntimeException("Excel导出失败", e.getCause()); + } + } + + /** + * 将多个Excel文件打包成ZIP并输出到浏览器下载 + * + * @param excelMap 文件名 -> 文件字节 + */ + private static void writeExcelZipResponse(String sheetName, HttpServletResponse response, Map excelMap) { + try { + // 设置ZIP下载响应头 + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", + "attachment;filename*=UTF-8''" + URLEncoder.encode(sheetName, StandardCharsets.UTF_8) + ".zip"); + + // 压缩写入多个Excel文件 + try (ZipOutputStream zipOut = ZipUtil.getZipOutputStream(response.getOutputStream(), CharsetUtil.CHARSET_UTF_8)) { + for (Map.Entry entry : excelMap.entrySet()) { + zipOut.putNextEntry(new ZipEntry(entry.getKey())); + zipOut.write(entry.getValue()); + zipOut.closeEntry(); + } + } + } catch (IOException e) { + throw new RuntimeException("导出Zip异常", e); + } + } + /** * 导出excel *