1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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 <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 类型丢失精度
                .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())
                .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;
    }
 
}