gaoluyang
2 天以前 90b3ad820eea45d021835197eac5bc3ad55b12ed
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package cn.iocoder.yudao.framework.excel.core.util;
 
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcelFactory;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.converters.longconverter.LongStringConverter;
import cn.idev.excel.enums.CellExtraTypeEnum;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.CellExtra;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
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.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
/**
 * Excel 工具类
 *
 * @author 超级管理员
 */
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");
    }
 
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
        try (InputStream inputStream = file.getInputStream()) {
            return FastExcelFactory.read(inputStream, head, null)
                    .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                    .doReadAllSync();
        }
    }
 
    /**
     * 读取 Excel,并把「纵向合并单元格」的值填充到合并区域内的每一行,即把合并单元格拉平成普通表格。
     *
     * 例如 A2:A5 合并、值为 100 时,读取结果中第 2~5 行的该列均为 100。
     * 因为 Excel 只在合并区域左上角保存值,直接读取时其余行为空,会导致「同一张单的多行明细」无法识别。
     * 注意:只处理单列的纵向合并;表头行的合并、跨列的横向合并不处理。
     *
     * @param file 文件
     * @param head Excel head 头
     * @return 数据列表
     */
    public static <T> List<T> readFlat(MultipartFile file, Class<T> head) throws IOException {
        MergedCellFillReadListener<T> listener = new MergedCellFillReadListener<>(head);
        // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
        try (InputStream inputStream = file.getInputStream()) {
            FastExcelFactory.read(inputStream, head, listener)
                    .extraRead(CellExtraTypeEnum.MERGE) // 额外读取合并单元格信息
                    .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                    .doReadAllSync();
        }
        listener.fillMergedCell();
        return listener.getDataList();
    }
 
    /**
     * 合并单元格填充的读取监听器:读取过程中收集表头、数据行与合并区域,读取完成后把合并单元格的值填充到合并区域内的每一行
     */
    private static class MergedCellFillReadListener<T> extends AnalysisEventListener<T> {
 
        /**
         * 表头:key 为 sheet 编号,value 为「列号 -> 表头文本」
         */
        private final Map<Integer, Map<Integer, String>> sheetHeaderMap = new HashMap<>();
        /**
         * 数据行:key 为「sheet 编号 - 行号」,value 为数据行
         */
        private final Map<String, T> rowDataMap = new HashMap<>();
        /**
         * 合并区域:key 为 sheet 编号
         */
        private final Map<Integer, List<CellExtra>> sheetMergedCellMap = new HashMap<>();
        /**
         * 「表头文本 -> 字段」,用于把合并单元格的值填充回字段
         */
        private final Map<String, Field> headerFieldMap = new HashMap<>();
        /**
         * 重复出现的表头文本,无法确定填充到哪个字段,忽略
         */
        private final Set<String> ambiguousHeaderSet = new HashSet<>();
        /**
         * 数据列表,即读取结果
         */
        private final List<T> dataList = new ArrayList<>();
 
        private MergedCellFillReadListener(Class<T> head) {
            for (Field field : head.getDeclaredFields()) {
                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
                if (excelProperty == null || excelProperty.value().length == 0
                        || excelProperty.value()[0].trim().isEmpty()) {
                    continue;
                }
                String header = excelProperty.value()[0].trim();
                if (headerFieldMap.containsKey(header)) {
                    ambiguousHeaderSet.add(header);
                    continue;
                }
                field.setAccessible(true);
                headerFieldMap.put(header, field);
            }
        }
 
        @Override
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            sheetHeaderMap.put(context.readSheetHolder().getSheetNo(), headMap);
        }
 
        @Override
        public void invoke(T data, AnalysisContext context) {
            dataList.add(data);
            rowDataMap.put(buildRowKey(context.readSheetHolder().getSheetNo(),
                    context.readRowHolder().getRowIndex()), data);
        }
 
        @Override
        public void extra(CellExtra extra, AnalysisContext context) {
            if (extra.getType() == CellExtraTypeEnum.MERGE) {
                sheetMergedCellMap.computeIfAbsent(context.readSheetHolder().getSheetNo(), k -> new ArrayList<>())
                        .add(extra);
            }
        }
 
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
        }
 
        /**
         * 把合并单元格的值,填充到合并区域内的每一行
         */
        public void fillMergedCell() {
            sheetMergedCellMap.forEach((sheetNo, mergedCellList) -> {
                Map<Integer, String> headerMap = sheetHeaderMap.get(sheetNo);
                if (headerMap == null) {
                    return;
                }
                mergedCellList.forEach(extra -> fillMergedCell(sheetNo, headerMap, extra));
            });
        }
 
        private void fillMergedCell(Integer sheetNo, Map<Integer, String> headerMap, CellExtra extra) {
            // 只处理单列的纵向合并;跨列的横向合并(如表头合并)不处理
            if (extra.getFirstColumnIndex() == null || !extra.getFirstColumnIndex().equals(extra.getLastColumnIndex())) {
                return;
            }
            // 表头行的合并不处理
            if (extra.getFirstRowIndex() == null || extra.getFirstRowIndex() < 1) {
                return;
            }
            String header = headerMap.get(extra.getFirstColumnIndex());
            Field field = header == null ? null : headerFieldMap.get(header);
            if (field == null || ambiguousHeaderSet.contains(header)) {
                return;
            }
            T sourceData = rowDataMap.get(buildRowKey(sheetNo, extra.getFirstRowIndex()));
            if (sourceData == null) {
                return;
            }
            try {
                Object value = field.get(sourceData);
                if (value == null) {
                    return;
                }
                for (int rowIndex = extra.getFirstRowIndex() + 1; rowIndex <= extra.getLastRowIndex(); rowIndex++) {
                    T targetData = rowDataMap.get(buildRowKey(sheetNo, rowIndex));
                    if (targetData != null) {
                        field.set(targetData, value);
                    }
                }
            } catch (IllegalAccessException e) {
                // 字段已 setAccessible,理论上不会发生;即使发生也忽略该合并单元格,不影响其它数据
            }
        }
 
        private String buildRowKey(Integer sheetNo, Integer rowIndex) {
            return sheetNo + "-" + rowIndex;
        }
 
        public List<T> getDataList() {
            return dataList;
        }
 
    }
 
}