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 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 泛型,保证 head 和 data 类型的一致性 * @throws IOException 写入失败的情况 */ public static void write(HttpServletResponse response, String filename, String sheetName, Class head, List 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 类型丢失精度 .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 模板(日期按文本渲染) * *

与 {@link #write} 的区别:额外注册了 {@link DateAsTextCellWriteHandler}, * 把 LocalDate / LocalDateTime 写成 yyyy-MM-dd 文本,而不是 Excel 日期序列号。 * 仅用于「下载导入模板」,导入解析仍走 {@link #read},不受影响。

*/ public static void writeTemplate(HttpServletResponse response, String filename, String sheetName, Class head, List 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()) .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 文件 * *

所有导入接口都经过这里,因此在这里统一把「坏文件 / 空文件 / 模板不匹配 / 单元格格式错」 * 等各种底层异常,转换成可读的中文提示(ServiceException)。 * 这样业务接口不必各自写 try-catch,也不会再出现含糊的「系统异常」500。

* * @param file Excel 文件 * @param head 表头对应的 VO 类 * @return 解析结果 */ public static List read(MultipartFile file, Class head) { // 1. 文件本身为空 if (file == null || file.isEmpty()) { throw exception(EXCEL_FILE_EMPTY); } // 2. 解析(逐类识别异常,给出可读提示) List 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; } }