001package top.cenze.utils;
002
003import cn.hutool.core.collection.CollectionUtil;
004import cn.hutool.core.convert.Convert;
005import cn.hutool.core.date.DateUtil;
006import cn.hutool.core.io.FileUtil;
007import cn.hutool.core.util.ObjectUtil;
008import cn.hutool.core.util.StrUtil;
009import cn.hutool.core.util.XmlUtil;
010import com.alibaba.fastjson.JSON;
011import com.alibaba.fastjson.JSONArray;
012import com.alibaba.fastjson.JSONObject;
013import lombok.SneakyThrows;
014import lombok.extern.slf4j.Slf4j;
015import org.apache.commons.fileupload.FileItem;
016import org.apache.commons.fileupload.FileItemFactory;
017import org.apache.commons.fileupload.disk.DiskFileItemFactory;
018import org.apache.commons.jexl3.JexlBuilder;
019import org.apache.commons.lang.NullArgumentException;
020import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
021import org.apache.poi.hssf.usermodel.HSSFDateUtil;
022import org.apache.poi.hssf.usermodel.HSSFRichTextString;
023import org.apache.poi.hssf.usermodel.HSSFWorkbook;
024import org.apache.poi.ss.usermodel.*;
025import org.apache.poi.xssf.usermodel.*;
026import org.jxls.common.Context;
027import org.jxls.expression.JexlExpressionEvaluator;
028import org.jxls.reader.ReaderBuilder;
029import org.jxls.reader.ReaderConfig;
030import org.jxls.reader.XLSReadStatus;
031import org.jxls.reader.XLSReader;
032import org.jxls.transform.Transformer;
033import org.jxls.transform.poi.PoiTransformer;
034import org.jxls.transform.poi.WritableCellValue;
035import org.jxls.transform.poi.WritableHyperlink;
036import org.jxls.util.JxlsHelper;
037import org.springframework.web.multipart.MultipartFile;
038import org.w3c.dom.Document;
039import org.w3c.dom.Element;
040import top.cenze.utils.component.ExcelDateConverter;
041import top.cenze.utils.enums.ExcelTypeEnum;
042import top.cenze.utils.file.CZFileUtil;
043import top.cenze.utils.file.MultipartFileUtil;
044import top.cenze.utils.file.XMLUtil;
045import top.cenze.utils.pojo.*;
046
047import javax.servlet.ServletOutputStream;
048import javax.servlet.http.HttpServletRequest;
049import javax.servlet.http.HttpServletResponse;
050import java.io.*;
051import java.text.DecimalFormat;
052import java.text.SimpleDateFormat;
053import java.util.*;
054import java.util.regex.Matcher;
055import java.util.regex.Pattern;
056import java.util.stream.Collectors;
057
058/**
059 * @desc: JxlsExcel工具类
060 * @author: chengze
061 * @createByDate: 2023/10/9 10:24
062 */
063@Slf4j
064public class JxlsExcelUtil {
065
066    /** ========================================= delete  ========================================= */
067
068    /**
069     * 删除行(从指定起始行开始,删除排除行集合之外的所有行数据)
070     * @param file
071     * @param sheetIndex
072     * @param startRow
073     * @param lstExclusionRow
074     * @return
075     */
076    @SneakyThrows
077    public static MultipartFile delWithExclusionRow(MultipartFile file,
078                                       Integer sheetIndex,
079                                       Integer startRow,
080                                       List<Integer> lstExclusionRow) {
081        if (ObjectUtil.isNull(file) ||
082                ObjectUtil.isNull(sheetIndex) ||
083                CollectionUtil.isEmpty(lstExclusionRow)) {
084            return null;
085        }
086
087        Workbook workbook = null;
088        if (file.getOriginalFilename().endsWith(".xls")) {
089            workbook = new HSSFWorkbook(file.getInputStream());
090        }
091        if (file.getOriginalFilename().endsWith(".xlsx")) {
092            workbook = new XSSFWorkbook(file.getInputStream());
093        }
094
095        // worksheet
096        Sheet sheet = workbook.getSheetAt(sheetIndex);
097        int lastRowNum = sheet.getLastRowNum();
098
099        for (int i = startRow; i < lastRowNum; i++) {
100            Row row = sheet.getRow(i);
101            if (!lstExclusionRow.contains(i) && ObjectUtil.isNull(row)) {
102                sheet.removeRow(row);
103            }
104        }
105
106        FileItemFactory factory = new DiskFileItemFactory(16, null);
107        FileItem fileItem = factory.createItem("textField", "text/plain", true, file.getName());
108        OutputStream os = fileItem.getOutputStream();
109        workbook.write(os);
110
111        return MultipartFileUtil.getMultipartFile(fileItem);
112    }
113
114    /**
115     * 删除指定行
116     * @param file
117     * @param sheetIndex
118     * @param lstRow
119     * @return
120     */
121    @SneakyThrows
122    public static MultipartFile delRow(MultipartFile file,
123                                       Integer sheetIndex,
124                                       List<Integer> lstRow) {
125        if (ObjectUtil.isNull(file) ||
126                ObjectUtil.isNull(sheetIndex) ||
127                CollectionUtil.isEmpty(lstRow)) {
128            return null;
129        }
130
131        Workbook workbook = null;
132        if (file.getOriginalFilename().endsWith(".xls")) {
133            workbook = new HSSFWorkbook(file.getInputStream());
134        }
135        if (file.getOriginalFilename().endsWith(".xlsx")) {
136            workbook = new XSSFWorkbook(file.getInputStream());
137        }
138
139        // worksheet
140        Sheet sheet = workbook.getSheetAt(sheetIndex);
141//        // 创建样式
142//        CellStyle cellStyle = workbook.createCellStyle();
143//        cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
144//        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
145
146        for (Integer rowIdx : lstRow) {
147            Row row = sheet.getRow(rowIdx);
148            if (ObjectUtil.isNull(row)) {
149                sheet.removeRow(row);
150            }
151//            for (int i = 0; i < maxCol; i++) {
152//                Cell cell = row.getCell(i);
153//                cell.setCellStyle(cellStyle);
154//            }
155        }
156
157        FileItemFactory factory = new DiskFileItemFactory(16, null);
158        FileItem fileItem = factory.createItem("textField", "text/plain", true, file.getName());
159        OutputStream os = fileItem.getOutputStream();
160        workbook.write(os);
161
162        return MultipartFileUtil.getMultipartFile(fileItem);
163    }
164
165    /** ========================================= import  ========================================= */
166
167    /**
168     * 导入Excel
169     * @param file                  待导入数据文件(Excel)
170     * @param templateFilePath      导入模板文件路径
171     * @param items                 输出对象集合
172     * @param skipErrs              是否跳过异常
173     * @return
174     */
175    @SneakyThrows
176    public static XLSReadStatus importExcel(MultipartFile file,
177                                            String templateFilePath,
178                                            Object items,
179                                            Boolean skipErrs) {
180        // 载入导入模板流
181        InputStream isImportXmlTemplate = CZFileUtil.readFile(templateFilePath);
182
183        // 读取待导入数据文件(Excel)
184        InputStream isImportFile = file.getInputStream();
185
186        return importExcel(isImportFile, isImportXmlTemplate, items, skipErrs);
187    }
188
189    /**
190     * 导入Excel
191     * @param file                  待导入数据文件(Excel)
192     * @param templateFile          导入模板二进制文件(如从数据库中读取的文件)
193     * @param items                 输出对象集合
194     * @param skipErrs              是否跳过异常
195     * @return
196     */
197    @SneakyThrows
198    public static XLSReadStatus importExcel(MultipartFile file,
199                                            byte[] templateFile,
200                                            Object items,
201                                            Boolean skipErrs) {
202        // 载入导入模板流
203        InputStream isImportXmlTemplate = new ByteArrayInputStream(templateFile);
204
205        // 读取待导入数据文件(Excel)
206        InputStream isImportFile = file.getInputStream();
207
208        return importExcel(isImportFile, isImportXmlTemplate, items, skipErrs);
209    }
210
211    /**
212     * 导入Excel
213     * @param importFilePath        待导入数据文件路径(Excel)
214     * @param templateFilePath      导入模板文件路径
215     * @param items                 输出对象集合
216     * @param skipErrs              是否跳过异常
217     * @return
218     */
219    @SneakyThrows
220    public static XLSReadStatus importExcel(String importFilePath,
221                                            String templateFilePath,
222                                            Object items,
223                                            Boolean skipErrs) {
224        // 载入导入模板流
225        InputStream isImportXmlTemplate = CZFileUtil.readFile(templateFilePath);
226
227        // 读取待导入数据文件(Excel)
228        InputStream isImportFile = CZFileUtil.readFile(importFilePath);
229
230        return importExcel(isImportFile, isImportXmlTemplate, items, skipErrs);
231    }
232
233    /**
234     * 导入Excel
235     * @param importFilePath    待导入数据文件路径(Excel)
236     * @param templateFile      导入模板二进制文件(如从数据库中读取的文件)
237     * @param items                 输出对象集合
238     * @param skipErrs              是否跳过异常
239     * @return
240     */
241    @SneakyThrows
242    public static XLSReadStatus importExcel(String importFilePath,
243                                            byte[] templateFile,
244                                            Object items,
245                                            Boolean skipErrs) {
246        // 载入导入模板流
247        InputStream isImportXmlTemplate = new ByteArrayInputStream(templateFile);
248
249        // 读取待导入数据文件(Excel)
250        InputStream isImportFile = CZFileUtil.readFile(importFilePath);
251
252        return importExcel(isImportFile, isImportXmlTemplate, items, skipErrs);
253    }
254
255    /**
256     * 导入Excel
257     * @param isImportFile          待导入数据文件流(Excel)
258     * @param isImportXmlTemplate   载入的导入模板流
259     * @param items                 输出对象集合
260     * @param skipErrs              是否跳过异常
261     * @return
262     */
263    @SneakyThrows
264    public static XLSReadStatus importExcel(InputStream isImportFile,
265                                            InputStream isImportXmlTemplate,
266                                            Object items,
267                                            Boolean skipErrs) {
268        // 默认不跳过异常
269        if (ObjectUtil.isNull(skipErrs)) {
270            skipErrs = false;
271        }
272
273        log.info("importExcel items: {}", JSON.toJSONString(items));
274        if (ObjectUtil.isNull(isImportFile)) {
275            throw new NullArgumentException("待导入数据文件流不能为空");
276        }
277        if (ObjectUtil.isNull(isImportXmlTemplate)) {
278            throw new NullArgumentException("导入模板流不能为空");
279        }
280        if (ObjectUtil.isNull(items)) {
281            throw new NullArgumentException("输出对象集合不能为空");
282        }
283
284        if (skipErrs) {
285            // 跳过异常
286            ReaderConfig.getInstance().setSkipErrors(true);
287        }
288
289        // 开启读取偏好设置
290        ReaderConfig.getInstance().setUseDefaultValuesForPrimitiveTypes(true);
291
292                // 创建读取器
293        XLSReader reader = ReaderBuilder.buildFromXML(isImportXmlTemplate);
294        reader.getConvertUtilsBeanProvider().getConvertUtilsBean().register(new ExcelDateConverter(), java.lang.String.class);
295//        reader.getConvertUtilsBeanProvider().getConvertUtilsBean().register(new ZonedDateTimeConverter(), java.time.ZonedDateTime.class);
296//        reader.getConvertUtilsBeanProvider().getConvertUtilsBean().register(new LocalDateConverter(), java.time.LocalDate.class);
297//        reader.getConvertUtilsBeanProvider().getConvertUtilsBean().register(new InstantConverter(), java.time.Instant.class);
298//        reader.getConvertUtilsBeanProvider().getConvertUtilsBean().register(new StringConverter(), java.lang.String.class);
299
300        // 创建映射对象集合
301        Map beans = new HashMap<>();
302        beans.put("items", items);
303
304        return reader.read(isImportFile, beans);
305    }
306
307    /**
308     * 解释Excel读取映射异常信息
309     * @param xlsReadStatus
310     * @return
311     */
312    public static List<ExcelReadException> getReadExceptions(XLSReadStatus xlsReadStatus) {
313        // 正则表达式(N个大写字母 + N个数字)
314        String regex = "[A-Z]+[0-9]+";
315        Pattern pattern = Pattern.compile(regex);
316
317        // 映射失败信息处理
318        if (ObjectUtil.isNotNull(xlsReadStatus.getReadMessages())) {
319            List<ExcelReadException> lstReadException = new ArrayList<>();
320            JSONArray jsonArray = JSONArray.parseArray(JSON.toJSONString(xlsReadStatus.getReadMessages()));
321            for (int i = 0; i < jsonArray.size(); i++) {
322                JSONObject jsonObject = jsonArray.getJSONObject(i);
323                if (ObjectUtil.isNull(jsonObject)) {
324                    continue;
325                }
326
327                ExcelReadException readException = new ExcelReadException();
328                // 映射失败单元格提示
329                String errCell = jsonObject.getString("message");
330                log.info("getReadExceptions errCell: {}", errCell);
331                if (StrUtil.isNotEmpty(errCell)) {
332                    // 使用正则表达式提取单元格名称
333                    Matcher matcher = pattern.matcher(errCell);
334                    while (matcher.find()) {
335                        readException.setExCell(matcher.group());
336                    }
337                }
338                JSONObject exception = jsonObject.getJSONObject("exception");
339                if (ObjectUtil.isNotNull(exception)) {
340                    // 映射失败原因
341                    readException.setExMessage(exception.getString("message"));
342                }
343
344                lstReadException.add(readException);
345            }
346
347            return lstReadException;
348        }
349
350        return null;
351    }
352
353    /** ========================================= export  ========================================= */
354
355    /**
356     * 导出Excel(输出到数据流)
357     * @param templateFilePath  Excel模板文件路径
358     * @param mapperObject     待导出对象数据(单一对象Object)
359     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
360     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
361     * @param response
362     * @param request
363     */
364    @SneakyThrows
365    public static void exportExcel(String templateFilePath,
366                                   Object mapperObject,
367                                   Object mapperLoop,
368                                   Object mapperFunc,
369                                   HttpServletResponse response,
370                                   HttpServletRequest request) {
371        // 载入模板文件
372        File file = new File(templateFilePath);
373        FileInputStream fis = new FileInputStream(file);
374        // 强转成int类型大小的数组
375        byte[] bytes = new byte[(int) file.length()];
376        // 将pdf内容放到数组当中
377        fis.read(bytes);
378        // 关闭文件流
379        fis.close();
380
381        exportExcel(bytes, mapperObject, mapperLoop, mapperFunc, response, request);
382
383    }
384
385    /**
386     * 导出Excel(输出到数据流)
387     * @param templateFile  Excel模板二进制文件(从数据库中读取的)
388     * @param mapperObject     待导出对象数据(单一对象Object)
389     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
390     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
391     * @param response
392     * @param request
393     */
394    @SneakyThrows
395    public static void exportExcel(byte[] templateFile,
396                                   Object mapperObject,
397                                   Object mapperLoop,
398                                   Object mapperFunc,
399                                   HttpServletResponse response,
400                                   HttpServletRequest request) {
401        byte[] bytes = exportExcel(templateFile, mapperObject, mapperLoop, mapperFunc);
402
403        ServletOutputStream outputStream = response.getOutputStream();
404        outputStream.write(bytes);
405        outputStream.flush();
406        outputStream.close();
407    }
408
409    /**
410     * 导出Excel(输出到本地)
411     * @param templateFilePath  Excel模板文件路径: xx/xxx/xxx.xlsx
412     * @param mapperObject     待导出对象数据(单一对象Object)
413     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
414     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
415     */
416    @SneakyThrows
417    public static byte[] exportExcel(String templateFilePath,
418                                     Object mapperObject,
419                                     Object mapperLoop,
420                                     Object mapperFunc) {
421        // 载入模板文件
422        File file = new File(templateFilePath);
423        FileInputStream fis = new FileInputStream(file);
424        // 强转成int类型大小的数组
425        byte[] bytes = new byte[(int) file.length()];
426        // 将pdf内容放到数组当中
427        fis.read(bytes);
428        // 关闭文件流
429        fis.close();
430
431        return exportExcel(bytes, mapperObject, mapperLoop, mapperFunc);
432    }
433
434    /**
435     * 导出Excel(输出到本地)
436     * @param templateFile      Excel模板二进制文件(从数据库中读取的)
437     * @param mapperObject     待导出对象数据(单一对象Object)
438     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
439     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
440     */
441    @SneakyThrows
442    public static byte[] exportExcel(byte[] templateFile,
443                                     Object mapperObject,
444                                     Object mapperLoop,
445                                     Object mapperFunc) {
446        String templateFileName = UUID.randomUUID().toString().replaceAll("-", "") + ExcelTypeEnum.XLSX.getExtname();
447        File tmpFile = CZFileUtil.mkFileToResource(null, templateFileName);
448        CZFileUtil.writeFileToResource(tmpFile, templateFile);
449
450
451        String exportFileName = UUID.randomUUID().toString().replaceAll("-", "") + ExcelTypeEnum.XLSX.getExtname();
452        File expFile = CZFileUtil.mkFileToResource(null, exportFileName);
453
454        return exportExcel(tmpFile, expFile, mapperObject, mapperLoop, mapperFunc);
455    }
456
457    /**
458     * 导出Excel
459     * @param templateFile          Excel模板文件
460     * @param exportFile            输出文件
461     * @param mapperObject     待导出对象数据(单一对象Object)
462     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
463     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
464     */
465    @SneakyThrows
466    public static byte[] exportExcel(File templateFile,
467                                     File exportFile,
468                                     Object mapperObject,
469                                     Object mapperLoop,
470                                     Object mapperFunc) {
471        MultipartFile file = exportExcelToMultiPartFile(templateFile, exportFile, mapperObject, mapperLoop, mapperFunc);
472        if (ObjectUtil.isNotNull(file)) {
473            return file.getBytes();
474        }
475
476        return null;
477    }
478
479    /**
480     * 导出Excel
481     * @param templateFile      Excel模板二进制文件(从数据库中读取的)
482     * @param mapperObject     待导出对象数据(单一对象Object)
483     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
484     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
485     */
486    @SneakyThrows
487    public static MultipartFile exportExcelToMultiPartFile(byte[] templateFile,
488                                                           Object mapperObject,
489                                                           Object mapperLoop,
490                                                           Object mapperFunc) {
491        String templateFileName = UUID.randomUUID().toString().replaceAll("-", "") + ExcelTypeEnum.XLSX.getExtname();
492        File tmpFile = CZFileUtil.mkFileToResource(null, templateFileName);
493        CZFileUtil.writeFileToResource(tmpFile, templateFile);
494
495
496        String exportFileName = UUID.randomUUID().toString().replaceAll("-", "") + ExcelTypeEnum.XLSX.getExtname();
497        File expFile = CZFileUtil.mkFileToResource(null, exportFileName);
498
499        return exportExcelToMultiPartFile(tmpFile, expFile, mapperObject, mapperLoop, mapperFunc);
500    }
501
502    /**
503     * 导出Excel
504     * @param templateFile          Excel模板文件
505     * @param exportFile            输出文件
506     * @param mapperObject     待导出对象数据(单一对象Object)
507     * @param mapperLoop       待导出的循环对象数据(对象Object数组)
508     * @param mapperFunc       待载入的自定义对象(用于Excel中引用其对象中的方法)(单一对象Object)
509     */
510    @SneakyThrows
511    public static MultipartFile exportExcelToMultiPartFile(File templateFile,
512                                                           File exportFile,
513                                                           Object mapperObject,
514                                                           Object mapperLoop,
515                                                           Object mapperFunc) {
516        if (!FileUtil.exist(templateFile) || !FileUtil.exist(exportFile)) {
517            throw new Exception("模板或导出文件不存在");
518        }
519
520        FileInputStream fis = null;
521        FileOutputStream fos = null;
522        try {
523            // 载入模板文件
524            fis = new FileInputStream(templateFile);
525            // 创建输出文件流
526            fos = new FileOutputStream(exportFile);
527
528            JxlsHelper jxlsHelper = JxlsHelper.getInstance();
529            Transformer trans = jxlsHelper.createTransformer(fis, fos);
530
531            // 设置自定义方法(用于Excel模板内引用)
532            if (ObjectUtil.isNotNull(mapperFunc)) {
533                Map<String , Object> mapFunc = new HashMap<>();
534                mapFunc.put("funcs", mapperFunc);
535                // 启动新的jxls-api加载自定义方法
536                JexlExpressionEvaluator evaluator = (JexlExpressionEvaluator) trans.getTransformationConfig().getExpressionEvaluator();
537                evaluator.setJexlEngine(new JexlBuilder().namespaces(mapFunc).create());
538            }
539
540            // 创建导出数据
541            Context context = PoiTransformer.createInitialContext();
542            // 设置导出对象数据
543            if (ObjectUtil.isNotNull(mapperObject)) {
544                context.putVar("object", mapperObject);
545            }
546            // 设置导出循环对象数据
547            if (ObjectUtil.isNotNull(mapperLoop)) {
548                context.putVar("items", mapperLoop);
549            }
550
551            //导出
552            jxlsHelper.setUseFastFormulaProcessor(false)  // 必须要这个,否者表格函数统计会错乱
553                    .processTemplate(context, trans);
554
555            return MultipartFileUtil.getMultipartFile(exportFile);
556        } catch (FileNotFoundException fe) {
557            log.error("export file not found err: {}", fe.getMessage());
558            throw new Exception(fe.getMessage());
559        } catch (IOException ie) {
560            log.error("export io err: {}", ie.getMessage());
561            throw new Exception(ie.getMessage());
562        } finally {
563            try {
564                if (ObjectUtil.isNotNull(fos)) {
565                    fos.flush();
566                    fos.close();
567                }
568                if (ObjectUtil.isNotNull(fis)) {
569                    fis.close();
570                }
571
572                FileUtil.del(templateFile);
573                FileUtil.del(exportFile);
574            } catch (IOException e) {
575                log.error("export io err: {}", e.getMessage());
576                throw new Exception(e.getMessage());
577            }
578        }
579    }
580
581    /** ========================================= create export template (xml)  ========================================= */
582
583    /**
584     * 给Cell添加批注
585     *
586     * @param cell 单元格
587     * @param value 批注内容
588     * @param excelType 扩展名
589     */
590    private static void addComment(Cell cell, String value, ExcelTypeEnum excelType) {
591        if (ObjectUtil.isNull(excelType)) {
592            excelType = ExcelTypeEnum.XLSX;
593        }
594
595        Sheet sheet = cell.getSheet();
596        cell.removeCellComment();
597        if (ExcelTypeEnum.XLS.equals(excelType)) {
598            ClientAnchor anchor = new HSSFClientAnchor();
599            // 关键修改
600            anchor.setDx1(0);
601            anchor.setDx2(0);
602            anchor.setDy1(0);
603            anchor.setDy2(0);
604            anchor.setCol1(cell.getColumnIndex());
605            anchor.setRow1(cell.getRowIndex());
606            anchor.setCol2(cell.getColumnIndex());
607            anchor.setRow2(cell.getRowIndex());
608            // 结束
609            Drawing drawing = sheet.createDrawingPatriarch();
610            Comment comment = drawing.createCellComment(anchor);
611            // 输入批注信息
612            comment.setString(new HSSFRichTextString(value));
613            // 将批注添加到单元格对象中
614            cell.setCellComment(comment);
615        } else if (ExcelTypeEnum.XLSX.equals(excelType)) {
616            ClientAnchor anchor = new XSSFClientAnchor();
617            // 关键修改
618            anchor.setDx1(0);
619            anchor.setDx2(0);
620            anchor.setDy1(0);
621            anchor.setDy2(0);
622            anchor.setCol1(cell.getColumnIndex());
623            anchor.setRow1(cell.getRowIndex());
624            anchor.setCol2(cell.getColumnIndex());
625            anchor.setRow2(cell.getRowIndex());
626            // 结束
627            Drawing drawing = sheet.createDrawingPatriarch();
628            Comment comment = drawing.createCellComment(anchor);
629            // 输入批注信息
630            comment.setString(new XSSFRichTextString(value));
631            // 将批注添加到单元格对象中
632            cell.setCellComment(comment);
633        }
634    }
635
636    /**
637     * 创建导出模板
638     * @param mapping
639     * @return
640     */
641    @SneakyThrows
642    public static MultipartFile createExportTemplate(ExcelMapping mapping) {
643        XSSFWorkbook workbook = new XSSFWorkbook();
644        Sheet sheet = workbook.createSheet("Sheet1");
645        if (ObjectUtil.isNull(sheet)) {
646            throw new NullPointerException("创建sheet失败");
647        }
648
649        // 获取所有单元格样式
650        Map<Integer, Map<Integer, CellStyle>> mapAllStyle = new HashMap<>();
651        Map<Integer, Short> mapAllHeight = new HashMap<>();
652        Map<Integer, Integer> mapAllWidth = new HashMap<>();
653        getSheetStyle(sheet, mapAllStyle, mapAllHeight, mapAllWidth);
654
655        // 创建所有行
656        Map<Integer, Row> allRow = createAllRow(sheet, mapping);
657        log.info("createExportTemplate allRow size: {}", allRow.size());
658        if (ObjectUtil.isNull(allRow.get(0))) {
659            throw new NullPointerException("创建单元格失败");
660        }
661
662        // A1单元格
663        Cell a1 = allRow.get(0).getCell(0);
664        CellStyle a1Style = null;
665        if (ObjectUtil.isNull(a1)) {
666            allRow.get(0).createCell(0, CellType.STRING);
667            a1 = allRow.get(0).getCell(0);
668        }
669        // 创建所有列及映射
670        Map<Integer, Map<Integer, Cell>> allCell = createAllCell(allRow, mapping);
671
672        // 设置A1批注
673        String a1Rich = ConvertUtil.numberToLetter(mapping.getMaxCol()) + (mapping.getMaxRow() + 1);
674        addComment(a1, "jx:area(lastCell=\"" + a1Rich + "\")", null);
675        if (ObjectUtil.isNotNull(a1) && StrUtil.isNotEmpty(a1.getStringCellValue())) {
676            a1.setCellValue(a1.getStringCellValue());
677        }
678
679        // 设置工作表行高和列宽
680        // 读取Luckysheet插件编辑的表格
681        setSheetWidthHeight(sheet, mapping.getLstRowHeight(), mapping.getLstColWidth());
682        // 读取excel软件编辑的表格
683//        setSheetWidthHeight(sheet, mapAllHeight, mapAllWidth);
684
685        // 设置工作表样式
686        setSheetStyle(sheet, mapAllStyle);
687
688        // 创建临时模板文件
689        File tmpFile = CZFileUtil.createExcelTemplateFile();
690        FileOutputStream fos = new FileOutputStream(tmpFile);
691        workbook.write(fos);
692        workbook.close();
693        fos.flush();
694        fos.close();
695
696//        // 文件转byte[]
697//        byte[] bytes = CZFileUtil.file2Bytes(tmpFile);
698        // 文件转multipartFile
699        MultipartFile multipartFile = MultipartFileUtil.getMultipartFile(tmpFile);
700        // 删除临时模板文件
701        FileUtil.del(tmpFile);
702
703        return multipartFile;
704    }
705
706    /**
707     * 创建导出模板
708     * @param templateFile      模板excel文件(主要取其样式,在原样式上生成新的模板)
709     * @param mapping           映射数据
710     * @return
711     */
712    @SneakyThrows
713    public static MultipartFile createExportTemplate(MultipartFile templateFile, ExcelMapping mapping) {
714        log.info("createExportTemplate mapping: {}", JSON.toJSONString(mapping));
715
716        Workbook workbook = null;
717        if (templateFile.getOriginalFilename().endsWith(".xls")) {
718            workbook = new HSSFWorkbook(templateFile.getInputStream());
719        }
720        if (templateFile.getOriginalFilename().endsWith(".xlsx")) {
721            workbook = new XSSFWorkbook(templateFile.getInputStream());
722        }
723
724        // worksheet
725        Sheet sheet = workbook.getSheetAt(0);
726        if (ObjectUtil.isNull(sheet)) {
727            throw new NullPointerException("加载sheet失败");
728        }
729
730        // 获取所有单元格样式
731        Map<Integer, Map<Integer, CellStyle>> mapAllStyle = new HashMap<>();
732        Map<Integer, Short> mapAllHeight = new HashMap<>();
733        Map<Integer, Integer> mapAllWidth = new HashMap<>();
734        getSheetStyle(sheet, mapAllStyle, mapAllHeight, mapAllWidth);
735
736        // 创建所有行
737        Map<Integer, Row> allRow = createAllRow(sheet, mapping);
738        log.info("createExportTemplate allRow size: {}", allRow.size());
739        if (ObjectUtil.isNull(allRow.get(0))) {
740            throw new NullPointerException("创建单元格失败");
741        }
742
743        // A1单元格
744        Cell a1 = allRow.get(0).getCell(0);
745        CellStyle a1Style = null;
746        if (ObjectUtil.isNull(a1)) {
747            allRow.get(0).createCell(0, CellType.STRING);
748            a1 = allRow.get(0).getCell(0);
749        }
750        // 创建所有列及映射
751        Map<Integer, Map<Integer, Cell>> allCell = createAllCell(allRow, mapping);
752
753        // 设置A1批注
754        String a1Rich = ConvertUtil.numberToLetter(mapping.getMaxCol()) + (mapping.getMaxRow() + 1);
755        addComment(a1, "jx:area(lastCell=\"" + a1Rich + "\")", null);
756        if (ObjectUtil.isNotNull(a1) && StrUtil.isNotEmpty(a1.getStringCellValue())) {
757            a1.setCellValue(a1.getStringCellValue());
758        }
759
760        // 设置工作表行高和列宽
761        // 读取Luckysheet插件编辑的表格
762        setSheetWidthHeight(sheet, mapping.getLstRowHeight(), mapping.getLstColWidth());
763        // 读取excel软件编辑的表格
764//        setSheetWidthHeight(sheet, mapAllHeight, mapAllWidth);
765
766        // 设置工作表样式
767        setSheetStyle(sheet, mapAllStyle);
768
769        // 创建临时模板文件
770        File tmpFile = CZFileUtil.createExcelTemplateFile();
771        FileOutputStream fos = new FileOutputStream(tmpFile);
772        workbook.write(fos);
773        workbook.close();
774        fos.flush();
775        fos.close();
776
777//        // 文件转byte[]
778//        byte[] bytes = CZFileUtil.file2Bytes(tmpFile);
779        // 文件转multipartFile
780        MultipartFile multipartFile = MultipartFileUtil.getMultipartFile(tmpFile);
781        // 删除临时模板文件
782        FileUtil.del(tmpFile);
783
784        return multipartFile;
785    }
786
787    @SneakyThrows
788    public static ExcelMapping getTemplateFileMaxRowAndCol(MultipartFile templateFile) {
789        Workbook workbook = null;
790        if (templateFile.getOriginalFilename().endsWith(".xls")) {
791            workbook = new HSSFWorkbook(templateFile.getInputStream());
792        }
793        if (templateFile.getOriginalFilename().endsWith(".xlsx")) {
794            workbook = new XSSFWorkbook(templateFile.getInputStream());
795        }
796
797        // worksheet
798        Sheet sheet = workbook.getSheetAt(0);
799        if (ObjectUtil.isNull(sheet)) {
800            throw new NullPointerException("加载sheet失败");
801        }
802
803        int maxRow = sheet.getLastRowNum();
804        int maxCol = 0;
805        for (int i = 0; i < maxRow + 1; i++) {
806            Row row = sheet.getRow(i);
807            if (ObjectUtil.isNotNull(row)) {
808                short lastCellNum = row.getLastCellNum();
809                if (lastCellNum > maxCol) {
810                    maxCol = lastCellNum;
811                }
812            }
813        }
814
815        ExcelMapping mapping = new ExcelMapping();
816        mapping.setMaxRow(maxRow);
817        mapping.setMaxCol(maxCol);
818
819        return mapping;
820    }
821
822    /**
823     * 获取所有单元格样式
824     * @param sheet
825     * @return
826     */
827    private static void getSheetStyle(Sheet sheet,
828                                    Map<Integer, Map<Integer, CellStyle>> mapAllStyle,
829                                    Map<Integer, Short> mapAllHeight,
830                                    Map<Integer, Integer> mapAllWidth) {
831        int maxRow = sheet.getLastRowNum();
832        int maxCol = 0;
833        log.info("getSheetStyle maxRow: {}", maxRow);
834        if (maxRow < 0) {
835            return;
836        }
837
838        // 遍历行
839        for (int r = 0; r < maxRow + 1; r++) {
840            Row row = sheet.getRow(r);
841            // 行高
842            if (ObjectUtil.isNull(row)) {
843                row = sheet.createRow(r);
844            }
845            mapAllHeight.put(r, row.getHeight());
846
847            // 获取当前行列数
848            int cols = row.getLastCellNum();
849            log.info("getSheetStyle cols: {}", cols);
850            if (cols > maxCol) {
851                maxCol = cols;
852            }
853
854            // 遍历列
855            Map<Integer, CellStyle> mapStyle = new HashMap<>();
856            for (int c = 0; c < cols + 1; c++) {
857                Cell cell = row.getCell(c);
858                if (ObjectUtil.isNull(cell)) {
859                    continue;
860                }
861
862                CellStyle cellStyle = cell.getCellStyle();
863                if (ObjectUtil.isNotNull(cellStyle)) {
864                    mapStyle.put(c, cellStyle);
865                }
866            }
867
868            // 样式
869            if (CollectionUtil.isNotEmpty(mapStyle)) {
870                mapAllStyle.put(r, mapStyle);
871            }
872        }
873
874        log.info("getSheetStyle maxCol: {}", maxCol);
875        // 遍历列
876        for (int c = 0; c < maxCol + 1; c++) {
877            if (ObjectUtil.isNull(sheet.getRow(0).getCell(c))) {
878                sheet.getRow(0).createCell(c, CellType.STRING);
879            }
880
881            mapAllWidth.put(c, sheet.getColumnWidth(c));
882        }
883
884        log.info("getSheetStyle allHeight: {}", JSON.toJSONString(mapAllHeight));
885        log.info("getSheetStyle allWidth: {}", JSON.toJSONString(mapAllWidth));
886    }
887
888    /**
889     * 设置工作表样式
890     * @param sheet
891     * @param mapAllStyle
892     */
893    private static void setSheetStyle(Sheet sheet, Map<Integer, Map<Integer, CellStyle>> mapAllStyle) {
894        if (ObjectUtil.isNull(sheet) || CollectionUtil.isEmpty(mapAllStyle)) {
895            return;
896        }
897
898        for (Map.Entry<Integer, Map<Integer, CellStyle>> entry : mapAllStyle.entrySet()) {
899            Integer r = entry.getKey();
900            Map<Integer, CellStyle> mapStyle = entry.getValue();
901            if (CollectionUtil.isEmpty(mapStyle)) {
902                continue;
903            }
904
905            for (Map.Entry<Integer, CellStyle> e : mapStyle.entrySet()) {
906                Integer c = e.getKey();
907                CellStyle style = e.getValue();
908                if (ObjectUtil.isNull(style)) {
909                    continue;
910                }
911
912                sheet.getRow(r).getCell(c).setCellStyle(style);
913            }
914        }
915    }
916
917    /**
918     * 设置工作表行高和列宽
919     * https://zhidao.baidu.com/question/721783003339700525.html
920     * @param sheet
921     */
922    private static void setSheetWidthHeight(Sheet sheet,
923                                            Map<Integer, Short> mapAllHeight,
924                                            Map<Integer, Integer> mapAllWidth) {
925        if (ObjectUtil.isNull(sheet)) {
926            return;
927        }
928
929        // 设置行高
930        if (CollectionUtil.isNotEmpty(mapAllHeight)) {
931            for (Map.Entry<Integer, Short> entry : mapAllHeight.entrySet()) {
932                if (ObjectUtil.isNull(entry)) {
933                    continue;
934                }
935
936                // 行高转换计算,可直接等于获取到的列宽值
937                // 实际行高 = 获取getWidth / 20
938//                BigDecimal h = Convert.toBigDecimal(entry.getValue()).divide(new BigDecimal("20.00"), 3, RoundingMode.HALF_UP);
939//                h.multiply(new BigDecimal("20.00")).shortValue();
940                sheet.getRow(entry.getKey()).setHeight(entry.getValue());
941            }
942        }
943
944        // 设置列宽
945        if (CollectionUtil.isNotEmpty(mapAllWidth)) {
946            for (Map.Entry<Integer, Integer> entry : mapAllWidth.entrySet()) {
947                if (ObjectUtil.isNull(entry)) {
948                    continue;
949                }
950
951                // 列宽转换计算,可直接等于获取到的列宽值
952                // 实际列宽 = 获取getHeitht / 264
953//                BigDecimal w = Convert.toBigDecimal(entry.getValue()).divide(new BigDecimal("264.00"), 3, RoundingMode.HALF_UP);
954//                log.info("setSheetWidthHeight w: {}", w);
955//                w.multiply(new BigDecimal("264.00")).intValue();
956                sheet.setColumnWidth(entry.getKey(), entry.getValue());
957            }
958        }
959    }
960
961    /**
962     * 设置工作表行高和列宽
963     * https://zhidao.baidu.com/question/721783003339700525.html
964     * @param sheet
965     * @param lstRowHeight
966     * @param lstColWidth
967     */
968    private static void setSheetWidthHeight(Sheet sheet, List<Short> lstRowHeight, List<Short> lstColWidth) {
969        if (ObjectUtil.isNull(sheet)) {
970            return;
971        }
972
973        // 设置行高
974        if (CollectionUtil.isNotEmpty(lstRowHeight)) {
975            short pre = 0;
976            for (int r = 0; r < lstRowHeight.size(); r ++) {
977                Row row = sheet.getRow(r);
978                if (ObjectUtil.isNull(row)) {
979                    row = sheet.createRow(r);
980                }
981
982                // 实际行高 = 所传高度 * 20
983                row.setHeight(Convert.toShort((lstRowHeight.get(r) - pre) * 20));
984                pre = lstRowHeight.get(r);
985            }
986        }
987
988        // 设置列宽
989        if (CollectionUtil.isNotEmpty(lstColWidth)) {
990            int pre = 0;
991            for (int c = 0; c < lstColWidth.size(); c ++) {
992                Cell cell = sheet.getRow(0).getCell(c);
993                if (ObjectUtil.isNull(cell)) {
994                    cell = sheet.getRow(0).createCell(c);
995                }
996
997                // 列宽转换计算
998                // 实际列宽 = 所传宽度 * 26.4
999                sheet.setColumnWidth(c, Convert.toInt((lstColWidth.get(c) - pre) * 26.4));
1000                pre = lstColWidth.get(c);
1001            }
1002        }
1003    }
1004
1005    /**
1006     * 初始化创建所有行
1007     * @param sheet
1008     * @param mapping
1009     * @return
1010     */
1011    private static Map<Integer, Row> createAllRow(Sheet sheet, ExcelMapping mapping) {
1012        if (ObjectUtil.isNull(sheet) || ObjectUtil.isNull(mapping)) {
1013            return null;
1014        }
1015
1016        Map<Integer, Row> mapAllRows = new HashMap<>();
1017        // 涉及的所有行索引
1018        Set<Integer> rows = new HashSet<>();
1019
1020        // 自定义涉及的行索引
1021        if (CollectionUtil.isNotEmpty(mapping.getLstCustom())) {
1022            Set<Integer> setRows = mapping.getLstCustom().stream().map(ExcelMappingCustom::getRow).collect(Collectors.toSet());
1023            if (CollectionUtil.isNotEmpty(setRows)) {
1024                rows.addAll(setRows);
1025            }
1026        }
1027
1028        // 对象涉及的行索引
1029        if (CollectionUtil.isNotEmpty(mapping.getLstObject())) {
1030            Set<Integer> setRows = mapping.getLstObject().stream().map(ExcelMappingObject::getRow).collect(Collectors.toSet());
1031            if (CollectionUtil.isNotEmpty(setRows)) {
1032                rows.addAll(setRows);
1033            }
1034        }
1035
1036        // 对象涉及的行索引
1037        if (ObjectUtil.isNotNull(mapping.getMappingLoop()) && CollectionUtil.isNotEmpty(mapping.getMappingLoop().getLstData())) {
1038            Set<Integer> setRows = mapping.getMappingLoop().getLstData().stream().map(ExcelMappingObject::getRow).collect(Collectors.toSet());
1039            if (CollectionUtil.isNotEmpty(setRows)) {
1040                rows.addAll(setRows);
1041            }
1042        }
1043
1044        // 如果为空或不存在第一行,则添加
1045        if (CollectionUtil.isEmpty(rows) || !rows.contains(0)) {
1046            rows.add(0);
1047        }
1048        // 获取或创建各行
1049        for (Integer row : rows) {
1050            Row excelRow = sheet.getRow(row);
1051            if (ObjectUtil.isNull(excelRow)) {
1052                excelRow = sheet.createRow(row);
1053            }
1054            if (ObjectUtil.isNotNull(excelRow)) {
1055                mapAllRows.put(row, excelRow);
1056            }
1057        }
1058
1059        return mapAllRows;
1060    }
1061
1062    /**
1063     * 初始化创建所有列
1064     * @param mapAllRows
1065     * @param mapping
1066     * @return
1067     */
1068    private static Map<Integer, Map<Integer, Cell>> createAllCell(Map<Integer, Row> mapAllRows, ExcelMapping mapping) {
1069        if (CollectionUtil.isEmpty(mapAllRows) || ObjectUtil.isNull(mapping)) {
1070            return null;
1071        }
1072
1073        Map<Integer, Map<Integer, Cell>> mapAllCells = new HashMap<>();
1074
1075        // 自定义数据(如:标题)
1076        if (CollectionUtil.isNotEmpty(mapping.getLstCustom())) {
1077            for (ExcelMappingCustom custom : mapping.getLstCustom()) {
1078                Row row = mapAllRows.get(custom.getRow());
1079                if (ObjectUtil.isNull(row)) {
1080                    throw new NullPointerException("行单元格模板未找到");
1081                }
1082
1083                Map<Integer, Cell> mapCells = mapAllCells.get(custom.getRow());
1084                if (CollectionUtil.isEmpty(mapCells)) {
1085                    mapCells = new HashMap<>();
1086                }
1087
1088                // 单元格内容
1089                Cell cell = row.getCell(custom.getCol());
1090                if (ObjectUtil.isNull(cell)) {
1091                    cell = row.createCell(custom.getCol(), CellType.STRING);
1092                }
1093
1094                // 单元格样式
1095                CellStyle cellStyle = cell.getCellStyle();
1096                cell.setCellValue(custom.getContent());
1097                // 设置样式
1098                if (ObjectUtil.isNotNull(cellStyle)) {
1099                    cell.setCellStyle(cellStyle);
1100                }
1101
1102                mapCells.put(custom.getCol(), cell);
1103                mapAllCells.put(custom.getRow(), mapCells);
1104            }
1105        }
1106
1107        // 对象数据
1108        if (CollectionUtil.isNotEmpty(mapping.getLstObject())) {
1109            for (ExcelMappingObject object : mapping.getLstObject()) {
1110                Row row = mapAllRows.get(object.getRow());
1111                if (ObjectUtil.isNull(row)) {
1112                    throw new NullPointerException("行单元格模板未找到");
1113                }
1114
1115                Map<Integer, Cell> mapCells = mapAllCells.get(object.getRow());
1116                if (CollectionUtil.isEmpty(mapCells)) {
1117                    mapCells = new HashMap<>();
1118                }
1119
1120                StringBuilder sb = new StringBuilder();
1121                sb.append("${object.").append(object.getObjectItemName()).append("}");
1122
1123                // 单元格内容
1124                Cell cell = row.getCell(object.getCol());
1125                if (ObjectUtil.isNull(cell)) {
1126                    cell = row.createCell(object.getCol(), CellType.STRING);
1127                }
1128
1129                // 单元格样式
1130                CellStyle cellStyle = cell.getCellStyle();
1131                cell.setCellValue(sb.toString());
1132                // 设置样式
1133                if (ObjectUtil.isNotNull(cellStyle)) {
1134                    cell.setCellStyle(cellStyle);
1135                }
1136
1137                mapCells.put(object.getCol(), cell);
1138                mapAllCells.put(object.getRow(), mapCells);
1139            }
1140        }
1141
1142        // 循环数据
1143        if (ObjectUtil.isNotNull(mapping.getMappingLoop()) && CollectionUtil.isNotEmpty(mapping.getMappingLoop().getLstData())) {
1144            List<ExcelMappingObject> lstData = mapping.getMappingLoop().getLstData();
1145            Integer minCol = lstData.stream().map(ExcelMappingObject::getCol).min((v1, v2) -> v1 - v2).get();
1146            Integer maxCol = lstData.stream().map(ExcelMappingObject::getCol).max((v1, v2) -> v1 - v2).get();
1147            if (ObjectUtil.isNull(minCol)) {
1148                minCol = 0;
1149            }
1150
1151            for (ExcelMappingObject object : lstData) {
1152//                log.info("createAllCell allRows key: {}, object row: {}", JSON.toJSONString(mapAllRows.keySet()), object.getRow());
1153                Row row = mapAllRows.get(object.getRow());
1154                if (ObjectUtil.isNull(row)) {
1155                    throw new NullPointerException("行单元格模板未找到");
1156                }
1157
1158                Map<Integer, Cell> mapCells = mapAllCells.get(object.getRow());
1159                if (CollectionUtil.isEmpty(mapCells)) {
1160                    mapCells = new HashMap<>();
1161                }
1162
1163                StringBuilder sb = new StringBuilder();
1164                sb.append("${item.").append(object.getObjectItemName()).append("}");
1165
1166                // 单元格内容
1167                Cell cell = row.getCell(object.getCol());
1168                if (ObjectUtil.isNull(cell)) {
1169                    cell = row.createCell(object.getCol(), CellType.STRING);
1170                }
1171
1172                // 单元格样式
1173                CellStyle cellStyle = cell.getCellStyle();
1174                cell.setCellValue(sb.toString());
1175                // 设置样式
1176                if (ObjectUtil.isNotNull(cellStyle)) {
1177                    cell.setCellStyle(cellStyle);
1178                }
1179
1180                mapCells.put(object.getCol(), cell);
1181                mapAllCells.put(object.getRow(), mapCells);
1182            }
1183
1184            // jx:each(items="consignees" var="consignee" lastCell="E4")
1185//            String loopRich = ConvertUtil.numberToLetter(maxCol) + (mapping.getMappingLoop().getMaxRow() + 1);
1186            // 循环体需占满最大列,否则循环体下面如果有统计数据是超过循环数据最大列的话,超出列的统计数据则会显示在设计时所以的行列位置,并不会显示最后行
1187            String loopRich = ConvertUtil.numberToLetter(mapping.getMaxCol()) + (mapping.getMappingLoop().getMaxRow() + 1);
1188            Cell firstLoopCell = mapAllCells.get(mapping.getMappingLoop().getMinRow()).get(minCol);
1189            if (ObjectUtil.isNull(firstLoopCell)) {
1190                Row row = mapAllRows.get(mapping.getMappingLoop().getMinRow());
1191                if (ObjectUtil.isNull(row)) {
1192                    throw new NullPointerException("行单元格模板未找到");
1193                }
1194                firstLoopCell = row.createCell(minCol, CellType.STRING);
1195            }
1196            addComment(firstLoopCell, "jx:each(items=\"items\" var=\"item\" lastCell=\"" + loopRich + "\")", null);
1197        }
1198
1199        return mapAllCells;
1200    }
1201
1202    /** ========================================= create import template (xml)  ========================================= */
1203
1204    /**
1205     * 创建导入xml映射文件
1206     * Excel行或列索引从0开始(即行或列行数据,第一行或第一列,索引为0)
1207     * @param sheetName         sheet名称
1208     * @param startRowIndex     真实数据起始行
1209     * @param lstMapper         数据映射(excel字段名和对应的对应属性名)
1210     * @param className         映射对象全类型
1211     * @param
1212     */
1213//    public static Document createImportXml(String sheetName, Integer startRowIndex,
1214//                                            List<ExcelImportMapper> lstMapper, String className) {
1215//        if (StrUtil.isEmpty(sheetName)) {
1216//            throw new NullArgumentException("必须指定导入的Excel表Sheet名称");
1217//        }
1218//        if (ObjectUtil.isNull(startRowIndex)) {
1219//            throw new NullArgumentException("必须指定真实数据所在起始行");
1220//        }
1221//
1222//        // 创建xml文档
1223//        Document xml = XmlUtil.createXml();
1224//
1225//        // workbook
1226//        Element workbook = xml.createElement("workbook");
1227//
1228//        // worksheet
1229//        Element worksheet = xml.createElement("worksheet");
1230//        if (StrUtil.isNotEmpty(sheetName)) {
1231//            worksheet.setAttribute("name", sheetName);
1232//        }
1233//
1234//        // 排除(跳过)真实数据起始行前面的行数(如果不跳过非真实数据行,则会出现脏数据,loop即会把非真实数据行读取进来)
1235//        // exclusionSection
1236//        Element sectionExclusion = xml.createElement("section");
1237//        sectionExclusion.setAttribute("startRow", "0");
1238//        if (startRowIndex > 0) {
1239//            sectionExclusion.setAttribute("endRow", Convert.toStr(startRowIndex - 1));
1240//        } else {
1241//            sectionExclusion.setAttribute("endRow", "0");
1242//        }
1243//
1244//        // loop
1245//        Element loop = xml.createElement("loop");
1246//        loop.setAttribute("startRow", Convert.toStr(startRowIndex));
1247//        loop.setAttribute("endRow", Convert.toStr(startRowIndex));
1248//        loop.setAttribute("items", "items");
1249//        loop.setAttribute("var", "item");
1250//        loop.setAttribute("varType", className);
1251//
1252//        // sectionData
1253//        Element sectionData = xml.createElement("section");
1254//        sectionData.setAttribute("startRow", Convert.toStr(startRowIndex));
1255//        sectionData.setAttribute("endRow", Convert.toStr(startRowIndex));
1256//
1257//        // mapping
1258//        for (ExcelImportMapper mapper : lstMapper) {
1259//            Element mapping = xml.createElement("mapping");
1260//            mapping.setAttribute("row", Convert.toStr(startRowIndex));
1261//            mapping.setAttribute("col", Convert.toStr(mapper.getColIndex()));
1262//            mapping.setTextContent("item." + mapper.getMapperField());
1263//
1264//            sectionData.appendChild(mapping);
1265//        }
1266//
1267//        // loopbreakcondition
1268//        Element loopbreakcondition = xml.createElement("loopbreakcondition");
1269//
1270//        // rowcheck
1271//        Element rowcheck = xml.createElement("rowcheck");
1272//        rowcheck.setAttribute("offset", "0");
1273//
1274//        // cellcheck
1275//        Element cellcheck = xml.createElement("cellcheck");
1276//        cellcheck.setAttribute("offset", "0");
1277//
1278//        rowcheck.appendChild(cellcheck);
1279//        loopbreakcondition.appendChild(rowcheck);
1280//        loop.appendChild(loopbreakcondition);
1281//        loop.appendChild(sectionData);
1282//        worksheet.appendChild(sectionExclusion);
1283//        worksheet.appendChild(loop);
1284//        workbook.appendChild(worksheet);
1285//        xml.appendChild(workbook);
1286//
1287//        return xml;
1288//    }
1289
1290    /**
1291     * 创建导入模板xml文档
1292     * @param mapping
1293     * @return
1294     */
1295    @SneakyThrows
1296    public static MultipartFile createImportXmlToMultipartFile(ExcelMapping mapping) {
1297        Document xml = createImportXml(mapping);
1298        if (ObjectUtil.isNull(xml)) {
1299            return null;
1300        }
1301
1302        File tmpFile = XMLUtil.toFile(xml);
1303
1304        MultipartFile multipartFile = MultipartFileUtil.getMultipartFile(tmpFile);
1305
1306        // 删除临时文件
1307        FileUtil.del(tmpFile);
1308
1309        return multipartFile;
1310    }
1311
1312    /**
1313     * 创建导入模板xml文档
1314     * @param mapping
1315     * @return
1316     */
1317    public static Document createImportXml(ExcelMapping mapping) {
1318        if (ObjectUtil.isNull(mapping) || ObjectUtil.isNull(mapping.getMappingLoop())) {
1319            throw new NullArgumentException("映射内容不能为空");
1320        }
1321
1322        // 创建xml文档
1323        Document xml = XmlUtil.createXml();
1324        // workbook
1325        Element workbook = xml.createElement("workbook");
1326
1327        // 设置工作Sheet索引(默认指定为第一个Sheet)
1328        if (ObjectUtil.isNull(mapping.getSheetIndex())) {
1329            mapping.setSheetIndex(0);
1330        }
1331
1332        // worksheet
1333        Element worksheet = xml.createElement("worksheet");
1334        worksheet.setAttribute("idx", Convert.toStr(mapping.getSheetIndex()));
1335
1336        // 创建占位元素
1337        Element exclusionSection = createExclusionSection(xml, 0, mapping.getMappingLoop().getMinRow());
1338        worksheet.appendChild(exclusionSection);
1339
1340        // 创建loop
1341        Element loop = createElementLoop(xml, mapping.getMappingLoop());
1342        worksheet.appendChild(loop);
1343
1344        workbook.appendChild(worksheet);
1345        xml.appendChild(workbook);
1346
1347        return xml;
1348    }
1349
1350    /**
1351     * 创建Loop元素
1352     * @param xml
1353     * @param mappingLoop
1354     * @return
1355     */
1356    private static Element createElementLoop(Document xml, ExcelMappingLoop mappingLoop) {
1357        // 找出loop的起始行和截止行索引
1358        Integer startRow = mappingLoop.getMinRow(); // lstLoop.stream().map(ExcelMappingObject::getRow).min((v1, v2) -> v1 - v2).get();
1359        Integer endRow= mappingLoop.getMaxRow();    // lstLoop.stream().map(ExcelMappingObject::getCol).max((v1, v2) -> v1 - v2).get();
1360        System.out.println("createElementLoop endRow: " + endRow);
1361
1362        String varType = mappingLoop.getVarType();
1363        if (StrUtil.isEmpty(varType)) {
1364            throw new NullArgumentException("必须指定映射对象的全类名");
1365        }
1366
1367        // loop
1368        Element loop = xml.createElement("loop");
1369        loop.setAttribute("startRow", Convert.toStr(startRow));
1370        loop.setAttribute("endRow", Convert.toStr(endRow));
1371        loop.setAttribute("items", "items");
1372        loop.setAttribute("var", "item");
1373        loop.setAttribute("varType", varType);
1374
1375        // sectionData
1376        Element sectionData = xml.createElement("section");
1377        sectionData.setAttribute("startRow", Convert.toStr(startRow));
1378        sectionData.setAttribute("endRow", Convert.toStr(endRow));
1379
1380        // mapping
1381        for (ExcelMappingObject item : mappingLoop.getLstData()) {
1382            Element mapping = xml.createElement("mapping");
1383            mapping.setAttribute("row", Convert.toStr(item.getRow()));
1384            mapping.setAttribute("col", Convert.toStr(item.getCol()));
1385            mapping.setTextContent("item." + item.getObjectItemName());
1386
1387            sectionData.appendChild(mapping);
1388        }
1389
1390        // loopbreakcondition
1391        Element loopbreakcondition = xml.createElement("loopbreakcondition");
1392
1393        // rowcheck
1394        Element rowcheck = xml.createElement("rowcheck");
1395        rowcheck.setAttribute("offset", "0");
1396
1397        // cellcheck
1398        for (ExcelMappingObject item : mappingLoop.getLstData()) {
1399            Element cellcheck = xml.createElement("cellcheck");
1400            cellcheck.setAttribute("offset", Convert.toStr(item.getCol()));
1401
1402            rowcheck.appendChild(cellcheck);
1403        }
1404
1405        loopbreakcondition.appendChild(rowcheck);
1406        loop.appendChild(loopbreakcondition);
1407        loop.appendChild(sectionData);
1408
1409        return loop;
1410    }
1411
1412    /**
1413     * 创建排除(占位)Section元素
1414     * 排除(跳过)真实数据起始行前面的行数(如果不跳过非真实数据行,则会出现脏数据,loop即会把非真实数据行读取进来)
1415     * @param xml
1416     * @param startRow
1417     * @param endRow
1418     * @return
1419     */
1420    private static Element createExclusionSection(Document xml, Integer startRow, Integer endRow) {
1421        // exclusionSection
1422        Element sectionExclusion = xml.createElement("section");
1423        sectionExclusion.setAttribute("startRow", Convert.toStr(startRow));
1424        if (ObjectUtil.isNotNull(endRow) && endRow > 0) {
1425            sectionExclusion.setAttribute("endRow", Convert.toStr(endRow - 1));
1426        } else {
1427            sectionExclusion.setAttribute("endRow", "0");
1428        }
1429
1430        return sectionExclusion;
1431    }
1432
1433
1434    /** ========================================= read  ========================================= */
1435
1436
1437    /**
1438     * 读取excel文件指定行的字段信息
1439     * @param file          excel文件
1440     * @param sheetIdx      sheet索引(0开始)
1441     * @param row           字段名所在行数
1442     * @return
1443     */
1444    @SneakyThrows
1445    public static List<ExcelFieldInfo> readExcelByRow(MultipartFile file, Integer sheetIdx, Integer row) {
1446        // 默认sheet0(第一个sheet)
1447        if (ObjectUtil.isNull(sheetIdx)) {
1448            sheetIdx = 0;
1449        }
1450        // 默认第一行
1451        if (ObjectUtil.isNull(row)) {
1452            row = 1;
1453        }
1454
1455        Workbook workbook = null;
1456        if (file.getOriginalFilename().endsWith(".xls")) {
1457            workbook = new HSSFWorkbook(file.getInputStream());
1458        }
1459        if (file.getOriginalFilename().endsWith(".xlsx")) {
1460            workbook = new XSSFWorkbook(file.getInputStream());
1461        }
1462
1463        // worksheet
1464        Sheet sheet = workbook.getSheetAt(sheetIdx);
1465        int lastRowNum = sheet.getLastRowNum();
1466        if (lastRowNum < 0) {
1467            throw new Exception("Excel文件Sheet不能为空");
1468        }
1469
1470        // 字段名所在行索引
1471        int rowIdx = row - 1;
1472        if (rowIdx > lastRowNum || rowIdx < 0) {
1473            throw new Exception("指定字段名行数不正确");
1474        }
1475
1476        // 获取指定行数据
1477        Row r = sheet.getRow(rowIdx);
1478        if (ObjectUtil.isNull(r)) {
1479            throw new Exception("指定字段名行数据不能为空");
1480        }
1481
1482        // 获取指定行列数(总索引总)
1483        short lastCellNum = r.getLastCellNum();
1484        if (lastCellNum < 0) {
1485            throw new Exception("指定字段名行数据列不能为空");
1486        }
1487
1488        List<ExcelFieldInfo> lstFields = new ArrayList<>();
1489        for (int i = 0; i < lastCellNum; i++) {
1490            Cell cell = r.getCell(i);
1491            String cellVal = getCellValueByCell(cell);
1492            if (StrUtil.isEmpty(cellVal)) {
1493                continue;
1494            }
1495
1496            ExcelFieldInfo fieldInfo = new ExcelFieldInfo();
1497            ConvertUtil.numberToLetter(i);
1498            fieldInfo.setCell(ConvertUtil.numberToLetter(i) + row);
1499            fieldInfo.setRowIdx(rowIdx);
1500            fieldInfo.setColIdx(i);
1501            fieldInfo.setFieldName(cellVal);
1502
1503            lstFields.add(fieldInfo);
1504        }
1505
1506        return lstFields;
1507    }
1508
1509    /**
1510     * 获取单元格各类型值,返回字符串类型
1511     *
1512     * @param cell cell
1513     * @return String
1514     */
1515    public static String getCellValueByCell(Cell cell) {
1516        //判断是否为null或空串
1517        if (cell == null || cell.toString().trim().equals("")) {
1518            return "";
1519        }
1520
1521        String cellValue = "";
1522        CellType cellType = cell.getCellType();
1523        switch (cellType) {
1524            case NUMERIC: // 数字
1525                short format = cell.getCellStyle().getDataFormat();
1526                if (HSSFDateUtil.isCellDateFormatted(cell)) { // 日期
1527                    SimpleDateFormat sdf;
1528                    String dtFormat = "yyyy-MM-dd HH:mm:ss";
1529                    if (format == 20 || format == 32) {
1530                        dtFormat = "HH:mm";
1531                    } else if (format == 14 || format == 31 || format == 57 || format == 58) {
1532                        dtFormat = "yyyy-MM-dd";
1533                    } else if (format == 179) {
1534                        dtFormat = "HH:mm:ss";
1535                    } else {
1536                        dtFormat = "yyyy-MM-dd HH:mm:ss";
1537                    }
1538                    cellValue = DateUtil.format(cell.getDateCellValue(), dtFormat);
1539                } else {
1540                    cell.setCellType(CellType.STRING);
1541                    cellValue = cell.getStringCellValue();
1542                }
1543                break;
1544            case STRING: // 字符串
1545                cellValue = cell.getStringCellValue();
1546                break;
1547            case BOOLEAN: // Boolean
1548                cellValue = cell.getBooleanCellValue() + "";
1549                break;
1550            case FORMULA: // 公式
1551                cell.setCellType(CellType.STRING);
1552                cellValue = cell.getStringCellValue();
1553                break;
1554            case BLANK: // 空值
1555                cellValue = "";
1556                break;
1557            case ERROR: // 故障
1558                cellValue = "ERROR VALUE";
1559                break;
1560            default:
1561                cellValue = "UNKNOWN VALUE";
1562                break;
1563        }
1564
1565        return cellValue;
1566    }
1567
1568
1569
1570
1571    /** ========================================= test function  ========================================= */
1572
1573    /**
1574     * 字符串转日期(double字符串转Date)
1575     * @param val
1576     * @return
1577     */
1578    public static String toDateDash(String val) {
1579        if (StrUtil.isNotEmpty(val)) {
1580            Double date = Convert.toDouble(val);
1581            System.out.println("toDate: " + date);
1582
1583            return DateUtil.format(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(date), "yyyy-MM-dd HH:mm:ss");
1584        }
1585
1586        return val;
1587    }
1588
1589    /**
1590     * 字符串转日期(double字符串转Date)
1591     * @param val
1592     * @return
1593     */
1594    public static String toDate(String val) {
1595//        System.out.println("toDate: " + val);
1596        if (StrUtil.isNotEmpty(val)) {
1597            if (StrUtil.contains(val, "-")) {
1598//                System.out.println("toDate1: " + val);
1599                return val;
1600            } else if (StrUtil.contains(val, "/")) {
1601//                System.out.println("toDate2: " + val);
1602                return val.replaceAll("/", "-");
1603            } else if (StrUtil.contains(val, "\\\\")) {
1604//                System.out.println("toDate3: " + val);
1605                return val.replaceAll("\\\\", "-");
1606            } else {
1607                Double date = Convert.toDouble(val);
1608//                System.out.println("toDate4: " + date);
1609
1610                return doubleToDate(date);
1611            }
1612        }
1613
1614        return val;
1615    }
1616
1617    /**
1618     * double to date
1619     * @param date
1620     * @return
1621     */
1622    public static String doubleToDate(Double date) {
1623//        System.out.println("当前时间:"+ new Date());
1624        // Calendar.getInstance() 获取Calendar实例,并获取系统默认的TimeZone
1625        Calendar calendar = Calendar.getInstance();
1626//        System.out.println("Calendar的系统默认TimeZone ID:" +
1627//                calendar.getTimeZone().getID());
1628//
1629        // 指定时区,例如意大利的罗马时区:Asia/Shanghai
1630        // 时区:https://blog.csdn.net/caicai1377/article/details/115873822
1631        // 罗马时区属于东1区,也就是UTC+1或GMT+1
1632        TimeZone itTimeZone = TimeZone.getTimeZone("Asia/Shanghai");
1633
1634        // Calendar指定罗马时区
1635        calendar.setTimeZone(itTimeZone);
1636//        System.out.println("Calendar指定TimeZone ID:" + itTimeZone.getID());
1637
1638        // 夏令时时间,比标准时间快1小时,即3600000毫秒,
1639        // 根据系统时间计算,如果不在夏令时生效范围内,则为0毫秒,反之为3600000毫秒
1640        int dstOffset = calendar.get(Calendar.DST_OFFSET);
1641        // 取得与GMT之间的时间偏移量,例如罗马属于东1区,则时间偏移量为3600000毫秒
1642        int zoneOffset = calendar.get(Calendar.ZONE_OFFSET);
1643
1644//        System.out.println("夏令时时间:"+dstOffset);
1645//        System.out.println("时间偏移量:"+zoneOffset);
1646        // 系统时区偏移 1900/1/1 到 1970/1/1 的 25569 天
1647//        int localOffset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET) / (60 * 1000);
1648//        int localOffset = zoneOffset + dstOffset;
1649        // 上海时区与GMT时间偏移8小时
1650        int localOffset = zoneOffset;
1651//        System.out.println("doubleToDate localOffset: " + localOffset);
1652        Date tDate = new Date();
1653        tDate.setTime((long) ((date - 25569) * 24 * 3600 * 1000 - localOffset));
1654
1655        return DateUtil.format(tDate, "yyyy-MM-dd HH:mm:ss");
1656    }
1657
1658    /**
1659     * 字符串转日期(double字符串转Date)
1660     * @param val
1661     * @return
1662     */
1663    public static String toDateSlash(String val) {
1664        if (StrUtil.isNotEmpty(val)) {
1665            Double date = Convert.toDouble(val);
1666            System.out.println("toDate: " + date);
1667
1668            return DateUtil.format(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(date), "yyyy/MM/dd HH:mm:ss");
1669        }
1670
1671        return val;
1672    }
1673
1674    // 格式化时间(自定义方法,可通过jxls-api加载后,在Excel内引用)
1675    public Object formatDate(Date dt){
1676        if(ObjectUtil.isNotNull(dt)){
1677            String dateStr = DateUtil.format(dt, "yyyy-MM-dd HH:mm:ss");
1678            return dateStr;
1679        }
1680
1681        return "--";
1682    }
1683
1684    // 时延-处理执行时间 ms -> s 且保留两位(自定义方法,可通过jxls-api加载后,在Excel内引用)
1685    public Object timeChange(Long time){
1686        if(time != null){
1687            DecimalFormat df = new DecimalFormat("0.00");
1688            String result = df.format((double)time / 1000);
1689            return result.equals("0.00") ? "--" : result + "s";
1690        }
1691        return "--";
1692    }
1693
1694    // 超链接方法(自定义方法,可通过jxls-api加载后,在Excel内引用)
1695    public WritableCellValue myLink(String address, String title) {
1696        return new WritableHyperlink(address, title);
1697    }
1698
1699    public String getTitle() {
1700        return "导出收货人";
1701    }
1702}