package cn.iocoder.yudao.framework.excel.core.util;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.exceptions.ExceptionUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.idev.excel.ExcelWriter;
|
import cn.idev.excel.FastExcelFactory;
|
import cn.idev.excel.converters.longconverter.LongStringConverter;
|
import cn.idev.excel.exception.ExcelAnalysisException;
|
import cn.idev.excel.exception.ExcelCommonException;
|
import cn.idev.excel.exception.ExcelDataConvertException;
|
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
import cn.iocoder.yudao.framework.excel.core.convert.ListJsonStringConverter;
|
import cn.iocoder.yudao.framework.excel.core.convert.LocalTimeStringConverter;
|
import lombok.extern.slf4j.Slf4j;
|
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
|
import cn.iocoder.yudao.framework.excel.core.handler.DateAsTextCellWriteHandler;
|
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
|
import jakarta.servlet.http.HttpServletResponse;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.util.List;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.excel.core.enums.ExcelErrorCodeConstants.*;
|
|
/**
|
* Excel 工具类
|
*
|
* @author 超级管理员
|
*/
|
@Slf4j
|
public class ExcelUtils {
|
|
/**
|
* 将列表以 Excel 响应给前端
|
*
|
* @param response 响应
|
* @param filename 文件名
|
* @param sheetName Excel sheet 名
|
* @param head Excel head 头
|
* @param data 数据列表哦
|
* @param <T> 泛型,保证 head 和 data 类型的一致性
|
* @throws IOException 写入失败的情况
|
*/
|
public static <T> void write(HttpServletResponse response, String filename, String sheetName,
|
Class<T> head, List<T> data) throws IOException {
|
// 输出 Excel
|
ExcelWriter excelWriter = FastExcelFactory.write(response.getOutputStream(), head)
|
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
|
.registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
|
.registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
|
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
|
.registerConverter(new LocalTimeStringConverter()) // FastExcel 内置无 LocalTime,缺少时会抛 Can not find 'Converter'
|
.registerConverter(new ListJsonStringConverter()) // FastExcel 内置无 List,缺少时会抛 Can not find 'Converter'
|
.build();
|
try {
|
excelWriter.write(data, FastExcelFactory.writerSheet(sheetName).build());
|
} finally {
|
// finish() 将 workbook 写入到 response 输出流
|
excelWriter.finish();
|
// close() 触发 dispose() 清理临时文件;
|
// POI SXSSFWorkbook.dispose() 可能输出 WARN "Failed to dispose sheet",
|
// 这是 POI 已知问题(临时文件清理时 BufferedWriter 已关闭),不影响导出结果
|
excelWriter.close();
|
}
|
// 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
|
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
|
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
|
}
|
|
/**
|
* 输出 Excel 模板(日期按文本渲染)
|
*
|
* <p>与 {@link #write} 的区别:额外注册了 {@link DateAsTextCellWriteHandler},
|
* 把 LocalDate / LocalDateTime 写成 yyyy-MM-dd 文本,而不是 Excel 日期序列号。
|
* 仅用于「下载导入模板」,导入解析仍走 {@link #read},不受影响。</p>
|
*/
|
public static <T> void writeTemplate(HttpServletResponse response, String filename, String sheetName,
|
Class<T> head, List<T> data) throws IOException {
|
ExcelWriter excelWriter = FastExcelFactory.write(response.getOutputStream(), head)
|
.autoCloseStream(false)
|
.registerWriteHandler(new ColumnWidthMatchStyleStrategy())
|
.registerWriteHandler(new SelectSheetWriteHandler(head))
|
.registerWriteHandler(new DateAsTextCellWriteHandler())
|
.registerConverter(new LongStringConverter())
|
.registerConverter(new LocalTimeStringConverter())
|
.registerConverter(new ListJsonStringConverter())
|
.build();
|
try {
|
excelWriter.write(data, FastExcelFactory.writerSheet(sheetName).build());
|
} finally {
|
excelWriter.finish();
|
excelWriter.close();
|
}
|
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
|
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
|
}
|
|
/**
|
* 读取 Excel 文件
|
*
|
* <p>所有导入接口都经过这里,因此在这里统一把「坏文件 / 空文件 / 模板不匹配 / 单元格格式错」
|
* 等各种底层异常,转换成可读的中文提示(ServiceException)。
|
* 这样业务接口不必各自写 try-catch,也不会再出现含糊的「系统异常」500。</p>
|
*
|
* @param file Excel 文件
|
* @param head 表头对应的 VO 类
|
* @return 解析结果
|
*/
|
public static <T> List<T> read(MultipartFile file, Class<T> head) {
|
// 1. 文件本身为空
|
if (file == null || file.isEmpty()) {
|
throw exception(EXCEL_FILE_EMPTY);
|
}
|
// 2. 解析(逐类识别异常,给出可读提示)
|
List<T> list;
|
// 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
|
try (InputStream inputStream = file.getInputStream()) {
|
list = FastExcelFactory.read(inputStream, head, null)
|
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
|
.doReadAllSync();
|
} catch (IOException ex) {
|
log.warn("[read][Excel 文件读取失败,文件名({})]", file.getOriginalFilename(), ex);
|
throw exception(EXCEL_FILE_READ_FAILED);
|
} catch (ExcelDataConvertException ex) {
|
// 单元格数据格式不对,例如文本列填了数字、日期列填了乱码
|
String detail = StrUtil.blankToDefault(ex.getMessage(), "内容与列类型不匹配");
|
log.warn("[read][Excel 单元格数据格式错误,行({}) 列({})]", ex.getRowIndex(), ex.getColumnIndex(), ex);
|
throw exception(EXCEL_DATA_CONVERT_FAILED,
|
(ex.getRowIndex() == null ? 0 : ex.getRowIndex()) + 1,
|
(ex.getColumnIndex() == null ? 0 : ex.getColumnIndex()) + 1,
|
StrUtil.maxLength(detail, 60));
|
} catch (ExcelAnalysisException ex) {
|
// 文件损坏、加密、不是真正的 Excel、表头与模板对不上等
|
String rootMsg = StrUtil.blankToDefault(ExceptionUtil.getRootCauseMessage(ex), "");
|
log.warn("[read][Excel 解析失败,文件名({}),原因({})]", file.getOriginalFilename(), rootMsg, ex);
|
if (StrUtil.containsAnyIgnoreCase(rootMsg, "head", "Can not find", "autoTrim", "Convert")) {
|
throw exception(EXCEL_HEAD_NOT_MATCH);
|
}
|
throw exception(EXCEL_TEMPLATE_NOT_MATCH);
|
} catch (ExcelCommonException ex) {
|
log.warn("[read][Excel 通用异常,文件名({})]", file.getOriginalFilename(), ex);
|
throw exception(EXCEL_FILE_READ_FAILED);
|
} catch (Exception ex) {
|
// 兜底:任何未预料的解析异常都转成可读提示,避免直接 500「系统异常」
|
log.warn("[read][Excel 解析出现未预期异常,文件名({})]", file.getOriginalFilename(), ex);
|
throw exception(EXCEL_FILE_READ_FAILED);
|
}
|
// 3. 解析出来是空表(只有表头、没有数据行)
|
if (CollUtil.isEmpty(list)) {
|
throw exception(EXCEL_FILE_EMPTY_CONTENT);
|
}
|
return list;
|
}
|
|
}
|