fix: 增加综合导出工具类
This commit is contained in:
parent
deac23c517
commit
0b8ca9063c
521
backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java
Normal file
521
backend/src/main/java/com/yfd/platform/utils/ExportZipUtil.java
Normal file
@ -0,0 +1,521 @@
|
||||
package com.yfd.platform.utils;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 综合导出工具类 — 将多维度数据导出为 ZIP 压缩包,内含若干 Excel (.xlsx) 文件,每个 Excel 支持多 Sheet 页。
|
||||
*
|
||||
* <h3>使用示例</h3>
|
||||
* <pre>{@code
|
||||
* // 1. 构建 Sheet 数据
|
||||
* SheetData stationSheet = new SheetData("电站基础信息",
|
||||
* Arrays.asList("测站编码", "测站名称", "经度", "纬度"),
|
||||
* stationRows); // List<List<Object>>
|
||||
*
|
||||
* SheetData monitorSheet = new SheetData("监测数据",
|
||||
* Arrays.asList("时间", "水位", "流量"),
|
||||
* monitorRows);
|
||||
*
|
||||
* // 2. 构建 Excel 数据(一个 Excel = 多个 Sheet)
|
||||
* ExcelData excel1 = new ExcelData("电站数据", Arrays.asList(stationSheet, monitorSheet));
|
||||
* ExcelData excel2 = new ExcelData("流域数据", Arrays.asList(basinSheet));
|
||||
*
|
||||
* // 3. 导出到 HttpServletResponse
|
||||
* ExportZipUtil.exportToResponse(response, "数据导出_20260731.zip", Arrays.asList(excel1, excel2));
|
||||
*
|
||||
* // 或导出到文件
|
||||
* ExportZipUtil.exportToFile(new File("D:/export.zip"), Arrays.asList(excel1, excel2));
|
||||
*
|
||||
* // 类型安全的构建方式
|
||||
* SheetData sheet = ExportZipUtil.fromTypedData("电站信息",
|
||||
* Arrays.asList("编码", "名称", "经度"),
|
||||
* Arrays.asList(Station::getStcd, Station::getStnm, Station::getLgtd),
|
||||
* stationList);
|
||||
* }</pre>
|
||||
*
|
||||
* @author system
|
||||
* @since 2026-07-31
|
||||
*/
|
||||
public class ExportZipUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExportZipUtil.class);
|
||||
|
||||
private ExportZipUtil() {
|
||||
// 工具类,禁止实例化
|
||||
}
|
||||
|
||||
// ======================== 数据模型 ========================
|
||||
|
||||
/**
|
||||
* 单个 Sheet 页数据定义
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SheetData {
|
||||
/** Sheet 页名称 */
|
||||
private String sheetName;
|
||||
/** 表头列名集合 */
|
||||
private List<String> headers;
|
||||
/** 数据行集合(每行是一个 Object 列表,与 headers 一一对应) */
|
||||
private List<List<Object>> rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个 Excel 文件数据定义(一个 Excel = 多个 Sheet)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ExcelData {
|
||||
/** Excel 文件名(不含 .xlsx 扩展名,在 ZIP 中自动添加) */
|
||||
private String fileName;
|
||||
/** 该 Excel 包含的所有 Sheet 页 */
|
||||
private List<SheetData> sheets;
|
||||
}
|
||||
|
||||
// ======================== 核心导出方法 ========================
|
||||
|
||||
/**
|
||||
* 导出 ZIP 压缩包到 HttpServletResponse(浏览器下载)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param zipFileName 下载文件名(含 .zip 扩展名)
|
||||
* @param excelList 要导出的 Excel 数据集合
|
||||
*/
|
||||
public static void exportToResponse(HttpServletResponse response, String zipFileName, List<ExcelData> excelList) {
|
||||
response.setContentType("application/zip");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
try {
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
response.setHeader("Content-Disposition", "attachment; filename=export.zip");
|
||||
}
|
||||
|
||||
try (ServletOutputStream os = response.getOutputStream()) {
|
||||
exportToStream(os, excelList);
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
log.error("导出 ZIP 到 Response 失败", e);
|
||||
throw new RuntimeException("导出失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 ZIP 压缩包到本地文件
|
||||
*
|
||||
* @param targetFile 目标文件
|
||||
* @param excelList 要导出的 Excel 数据集合
|
||||
*/
|
||||
public static void exportToFile(File targetFile, List<ExcelData> 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
|
||||
* <p>
|
||||
* 实现原理:先将每个 Excel 写入临时字节数组,再用内存 ZIP 打包,避免磁盘 IO。
|
||||
*
|
||||
* @param os 目标输出流
|
||||
* @param excelList 要导出的 Excel 数据集合
|
||||
*/
|
||||
public static void exportToStream(OutputStream os, List<ExcelData> 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)
|
||||
* <p>
|
||||
* 使用 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<String> headers = sheetData.getHeaders();
|
||||
List<List<Object>> 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<Object> rowData : rows) {
|
||||
Row dataRow = sheet.createRow(currentRow++);
|
||||
for (int i = 0; i < Math.min(rowData.size(), colCount); i++) {
|
||||
Cell cell = dataRow.createCell(i);
|
||||
Object value = rowData.get(i);
|
||||
setCellValue(cell, value);
|
||||
cell.setCellStyle(dataStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 自动调整列宽 ----
|
||||
autoSizeColumns(sheet, colCount);
|
||||
|
||||
// 冻结首行(表头固定)
|
||||
if (currentRow > 1) {
|
||||
sheet.createFreezePane(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 样式定义 ========================
|
||||
|
||||
/**
|
||||
* 创建表头样式(蓝色背景、白色加粗字体、居中对齐、带边框)
|
||||
*/
|
||||
public static CellStyle createHeaderStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
|
||||
// 背景色:深蓝
|
||||
style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
// 字体:白色加粗
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
font.setColor(IndexedColors.WHITE.getIndex());
|
||||
font.setFontHeightInPoints((short) 11);
|
||||
style.setFont(font);
|
||||
|
||||
// 对齐:居中
|
||||
style.setAlignment(HorizontalAlignment.CENTER);
|
||||
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
// 边框
|
||||
setThinBorder(style);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据行样式(无背景、正常字体、左对齐、带边框)
|
||||
*/
|
||||
public static CellStyle createDataStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
|
||||
style.setAlignment(HorizontalAlignment.LEFT);
|
||||
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
// 自动换行
|
||||
style.setWrapText(true);
|
||||
|
||||
setThinBorder(style);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置细线边框
|
||||
*/
|
||||
private static void setThinBorder(CellStyle style) {
|
||||
style.setBorderTop(BorderStyle.THIN);
|
||||
style.setBorderRight(BorderStyle.THIN);
|
||||
style.setBorderBottom(BorderStyle.THIN);
|
||||
style.setBorderLeft(BorderStyle.THIN);
|
||||
}
|
||||
|
||||
// ======================== 列宽自动调整 ========================
|
||||
|
||||
/**
|
||||
* 自动调整列宽(根据内容宽度)
|
||||
* <p>
|
||||
* 遍历每一列,取表头和所有数据行中最宽的内容作为列宽基准。
|
||||
*
|
||||
* @param sheet Sheet 对象
|
||||
* @param colCount 列数
|
||||
*/
|
||||
public static void autoSizeColumns(Sheet sheet, int colCount) {
|
||||
for (int i = 0; i < colCount; i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
// 自动列宽后留一点余量,避免内容紧贴边框
|
||||
int currentWidth = sheet.getColumnWidth(i);
|
||||
if (currentWidth > 0) {
|
||||
sheet.setColumnWidth(i, Math.min(currentWidth + 512, 255 * 256)); // 最大 255 字符宽
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 辅助方法 ========================
|
||||
|
||||
/**
|
||||
* 类型安全方式构建 SheetData
|
||||
* <p>
|
||||
* 适用于已有实体类 List 的场景,通过方法引用指定字段取值,避免手动逐字段提取。
|
||||
*
|
||||
* <pre>{@code
|
||||
* SheetData data = ExportZipUtil.fromTypedData("电站信息",
|
||||
* Arrays.asList("编码", "名称", "经度", "纬度"),
|
||||
* Arrays.asList(Station::getStcd, Station::getStnm, Station::getLgtd, Station::getLttd),
|
||||
* stationList);
|
||||
* }</pre>
|
||||
*
|
||||
* @param sheetName Sheet 名称
|
||||
* @param headers 表头列表
|
||||
* @param extractors 字段取值函数(与 headers 一一对应)
|
||||
* @param data 实体数据集合
|
||||
* @param <T> 实体类型
|
||||
* @return 构建好的 SheetData
|
||||
*/
|
||||
public static <T> SheetData fromTypedData(String sheetName,
|
||||
List<String> headers,
|
||||
List<Function<T, Object>> extractors,
|
||||
List<T> data) {
|
||||
List<List<Object>> rows = data.stream()
|
||||
.map(entity -> extractors.stream()
|
||||
.map(ext -> ext.apply(entity))
|
||||
.collect(Collectors.toList()))
|
||||
.collect(Collectors.toList());
|
||||
return new SheetData(sheetName, headers, rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Workbook 写入字节数组并关闭 Workbook
|
||||
*/
|
||||
private static byte[] workbookToBytes(Workbook workbook) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
workbook.write(baos);
|
||||
} finally {
|
||||
workbook.close();
|
||||
// SXSSFWorkbook 会清理临时文件
|
||||
if (workbook instanceof SXSSFWorkbook) {
|
||||
((SXSSFWorkbook) workbook).dispose();
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件名非法字符过滤(Windows + 通用)
|
||||
*/
|
||||
private static String sanitizeFileName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return "sheet";
|
||||
}
|
||||
return fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sheet 名称安全处理(Excel Sheet 名最长 31 字符)
|
||||
*/
|
||||
private static String safeName(String name) {
|
||||
if (name == null || name.isEmpty()) {
|
||||
return "Sheet1";
|
||||
}
|
||||
// Excel Sheet 名禁止包含 [ ] : * ? / \
|
||||
String safe = name.replaceAll("[\\[\\]:*?/\\\\]", "_");
|
||||
return safe.length() > 31 ? safe.substring(0, 31) : safe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单元格值(自动适配类型)
|
||||
*/
|
||||
private static void setCellValue(Cell cell, Object value) {
|
||||
if (value == null) {
|
||||
cell.setCellValue("");
|
||||
} else if (value instanceof Number) {
|
||||
cell.setCellValue(((Number) value).doubleValue());
|
||||
} else if (value instanceof Boolean) {
|
||||
cell.setCellValue((Boolean) value);
|
||||
} else if (value instanceof java.util.Date) {
|
||||
cell.setCellValue((java.util.Date) value);
|
||||
} else {
|
||||
cell.setCellValue(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 测试方法 ========================
|
||||
|
||||
/**
|
||||
* 本地运行测试,生成模拟数据 ZIP 文件到 D:/export_sample.zip
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// ---------- Excel 1:电站基础数据(1个Sheet:基础信息) ----------
|
||||
List<List<Object>> stationRows = new ArrayList<>();
|
||||
for (int i = 1; i <= 50; i++) {
|
||||
stationRows.add(Arrays.asList(
|
||||
"STCD" + String.format("%05d", i), // 测站编码
|
||||
"XX水电站" + i + "号", // 测站名称
|
||||
"长江流域", // 所在流域
|
||||
110.123456 + Math.random() * 10, // 经度
|
||||
30.654321 + Math.random() * 10, // 纬度
|
||||
"坝式水电站", // 工程类型
|
||||
Math.random() > 0.5 ? "运行中" : "在建", // 建设状态
|
||||
"省级", // 级别
|
||||
2000 + (i % 24) // 建成时间
|
||||
));
|
||||
}
|
||||
SheetData stationSheet = new SheetData("电站基础信息",
|
||||
Arrays.asList("测站编码", "测站名称", "所在流域", "经度", "纬度",
|
||||
"工程类型", "建设状态", "级别", "建成时间"),
|
||||
stationRows);
|
||||
|
||||
// ---------- Sheet 2:水位监测数据 ----------
|
||||
List<List<Object>> monitorRows = new ArrayList<>();
|
||||
String[] months = {"2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"};
|
||||
for (int i = 1; i <= 30; i++) {
|
||||
for (String month : months) {
|
||||
monitorRows.add(Arrays.asList(
|
||||
"STCD" + String.format("%05d", i),
|
||||
"XX水电站" + i + "号",
|
||||
month,
|
||||
String.format("%.2f", 100 + Math.random() * 50), // 水位(m)
|
||||
String.format("%.2f", 500 + Math.random() * 2000), // 流量(m³/s)
|
||||
String.format("%.1f", 15 + Math.random() * 20), // 水温(℃)
|
||||
Math.random() > 0.8 ? "超标" : "正常" // 水质状态
|
||||
));
|
||||
}
|
||||
}
|
||||
SheetData monitorSheet = new SheetData("水位监测数据",
|
||||
Arrays.asList("测站编码", "测站名称", "监测月份", "水位(m)", "流量(m³/s)",
|
||||
"水温(℃)", "水质状态"),
|
||||
monitorRows);
|
||||
|
||||
// ---------- Excel 2:流域概况数据 ----------
|
||||
List<List<Object>> basinRows = new ArrayList<>();
|
||||
String[] basinNames = {"长江流域", "黄河流域", "珠江流域", "淮河流域", "松花江流域", "海河流域"};
|
||||
String[] provinces = {"湖北/湖南/四川", "河南/山东/陕西", "广东/广西", "安徽/江苏", "黑龙江/吉林", "河北/天津"};
|
||||
for (int i = 0; i < basinNames.length; i++) {
|
||||
basinRows.add(Arrays.asList(
|
||||
"RV" + String.format("%03d", i + 1), // 流域编码
|
||||
basinNames[i], // 流域名称
|
||||
i == 0 ? "一级" : "二级", // 级别
|
||||
provinces[i], // 覆盖省份
|
||||
100 + Math.random() * 900, // 流域面积(万km²)
|
||||
500 + Math.random() * 5000, // 年径流量(亿m³)
|
||||
(int) (1000 + Math.random() * 2000), // 电站数量
|
||||
"良好" // 生态状态
|
||||
));
|
||||
}
|
||||
SheetData basinSheet = new SheetData("流域概况",
|
||||
Arrays.asList("流域编码", "流域名称", "级别", "覆盖省份", "流域面积(万km²)",
|
||||
"年径流量(亿m³)", "电站数量(个)", "生态状态"),
|
||||
basinRows);
|
||||
|
||||
// ---------- Sheet 4:鱼道设施数据 ----------
|
||||
List<List<Object>> fishwayRows = new ArrayList<>();
|
||||
String[] types = {"竖缝式鱼道", "池堰式鱼道", "升鱼机", "集运鱼船"};
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
fishwayRows.add(Arrays.asList(
|
||||
"STCD" + String.format("%05d", i),
|
||||
"XX水电站" + i + "号",
|
||||
types[i % types.length], // 鱼道类型
|
||||
String.format("%.1f", 5 + Math.random() * 95), // 过鱼效率(%)
|
||||
String.format("%.2f", 50 + Math.random() * 200), // 鱼道长度(m)
|
||||
String.format("%.1f", 2 + Math.random() * 8), // 鱼道宽度(m)
|
||||
Math.random() > 0.3 ? "运行中" : "检修中", // 运行状态
|
||||
"2025-0" + (1 + i % 9) + "-01" // 投运日期
|
||||
));
|
||||
}
|
||||
SheetData fishwaySheet = new SheetData("鱼道设施",
|
||||
Arrays.asList("电站编码", "电站名称", "鱼道类型", "过鱼效率(%)",
|
||||
"鱼道长度(m)", "鱼道宽度(m)", "运行状态", "投运日期"),
|
||||
fishwayRows);
|
||||
|
||||
// ---------- 组装导出 ----------
|
||||
ExcelData excel1 = new ExcelData("电站综合数据", Arrays.asList(stationSheet, monitorSheet));
|
||||
ExcelData excel2 = new ExcelData("流域与鱼道数据", Arrays.asList(basinSheet, fishwaySheet));
|
||||
|
||||
String outputPath = "D:/export_sample.zip";
|
||||
try {
|
||||
log.info("开始生成测试数据...");
|
||||
exportToFile(new File(outputPath), Arrays.asList(excel1, excel2));
|
||||
log.info("导出成功!文件路径: {}", outputPath);
|
||||
System.out.println("导出成功!文件路径: " + outputPath);
|
||||
} catch (Exception e) {
|
||||
log.error("导出失败", e);
|
||||
System.err.println("导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user