excelList) {
+ try (FileOutputStream fos = new FileOutputStream(targetFile)) {
+ exportToStream(fos, excelList);
+ } catch (IOException e) {
+ log.error("导出 ZIP 到文件失败: {}", targetFile.getAbsolutePath(), e);
+ throw new RuntimeException("导出失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 导出 ZIP 压缩包到任意 OutputStream
+ *
+ * 实现原理:先将每个 Excel 写入临时字节数组,再用内存 ZIP 打包,避免磁盘 IO。
+ *
+ * @param os 目标输出流
+ * @param excelList 要导出的 Excel 数据集合
+ */
+ public static void exportToStream(OutputStream os, List excelList) throws IOException {
+ if (excelList == null || excelList.isEmpty()) {
+ log.warn("导出数据为空,跳过 ZIP 生成");
+ return;
+ }
+
+ // 延迟创建 ZIP,累计所有 Excel 字节数组
+ ByteArrayOutputStream zipBaos = new ByteArrayOutputStream();
+ try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(zipBaos)) {
+ for (ExcelData excelData : excelList) {
+ if (excelData.getSheets() == null || excelData.getSheets().isEmpty()) {
+ continue;
+ }
+ byte[] excelBytes = workbookToBytes(createWorkbook(excelData));
+ String entryName = sanitizeFileName(excelData.getFileName()) + ".xlsx";
+
+ java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(entryName);
+ zos.putNextEntry(entry);
+ zos.write(excelBytes);
+ zos.closeEntry();
+ }
+ zos.finish();
+ }
+
+ os.write(zipBaos.toByteArray());
+ }
+
+ // ======================== Excel 生成方法 ========================
+
+ /**
+ * 根据 ExcelData 创建一个完整的 Excel Workbook(含所有 Sheet)
+ *
+ * 使用 SXSSFWorkbook(流式写入),支持大数据量导出,内存占用可控。
+ *
+ * @param excelData 单个 Excel 的数据定义
+ * @return 填充完毕的 Workbook(调用方负责关闭以释放临时文件)
+ */
+ public static Workbook createWorkbook(ExcelData excelData) {
+ SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 内存中保留 100 行,其余写临时文件
+ CellStyle headerStyle = createHeaderStyle(workbook);
+ CellStyle dataStyle = createDataStyle(workbook);
+
+ for (SheetData sheetData : excelData.getSheets()) {
+ if (sheetData == null) {
+ continue;
+ }
+ fillSheet(workbook, sheetData, headerStyle, dataStyle);
+ }
+
+ return workbook;
+ }
+
+ /**
+ * 填充单个 Sheet 页的数据
+ *
+ * @param workbook 目标 Workbook
+ * @param sheetData Sheet 数据定义
+ * @param headerStyle 表头样式
+ * @param dataStyle 数据行样式
+ */
+ public static void fillSheet(Workbook workbook, SheetData sheetData, CellStyle headerStyle, CellStyle dataStyle) {
+ String name = safeName(sheetData.getSheetName());
+ Sheet sheet = workbook.createSheet(name);
+
+ // SXSSFSheet 必须在写入数据前开启列追踪,否则 autoSizeColumn 会报错
+ if (sheet instanceof SXSSFSheet) {
+ ((SXSSFSheet) sheet).trackAllColumnsForAutoSizing();
+ }
+
+ List headers = sheetData.getHeaders();
+ List> rows = sheetData.getRows();
+ int colCount = headers != null ? headers.size() : 0;
+
+ // 设置列宽默认值(后续在数据写入后统一自动调整)
+ int currentRow = 0;
+
+ // ---- 写表头 ----
+ if (headers != null && !headers.isEmpty()) {
+ Row headerRow = sheet.createRow(currentRow++);
+ headerRow.setHeightInPoints(22);
+ for (int i = 0; i < colCount; i++) {
+ Cell cell = headerRow.createCell(i);
+ cell.setCellValue(headers.get(i));
+ cell.setCellStyle(headerStyle);
+ }
+ }
+
+ // ---- 写数据行 ----
+ if (rows != null) {
+ for (List