EasyExcel: 结合springboot实现表格导收支(单/多sheet), 全字段校验,批次等

[复制链接]
发表于 2024-11-28 00:04:47 | 显示全部楼层 |阅读模式

1.前言简介

[size=3.5]
ps: 如您有更好的方案或发现错误,请不吝见教,感激不尽啦~~~

   利用了easyExcel实现导入操作, 全手动封装, 灵活利用, 为了满意部门业务需求, 也做了升级
  

  • 全字段进行校验, 利用注解与正则表达式, 校验到每一行参数
  • 报错信息明白, 精确到每一行, 某个字段不正确的报错
  • 多个sheet导入的excel, 提示出 sheet名下的第几行报错
  • 增长xid同批次报错回滚, 有点类似分布式事件, 也就是一行报错,全部批次数据清除
  • 增长拓展性, 制作监听器,样式封装等, 利用接口特性, 方便多工程利用拓展
  • 在特殊类型(如list等类型)导入时, 出现了报错, 进行了兼容操作
  • 增长了数据库插入批次新增, 防止推数据库的数据量过大, 业务才略微麻烦
  1.1 链接传送门

1.1.1 easyExcel传送门

⇒ EasyExcel文档链接
⇒ EasyExcel-Plus尽情期待~~~

2. Excel表格导入过程

   实现功能请看1 前言简介, 里面有详细阐明
  2.1 easyExcel的利用预备工作

2.1.1 导入maven依赖

   <alibaba.easyexcel.version>3.3.4</alibaba.easyexcel.version>
  1. <!-- easyExcel -->
  2.         <dependency>
  3.             <groupId>com.alibaba</groupId>
  4.             <artifactId>easyexcel</artifactId>
  5.             <version>${alibaba.easyexcel.version}</version>
  6.         </dependency>
复制代码
2.1.2 创建一个util包

里面专门放置全部的excel操作, 如图所示
   

  • realDto 里面就是具体导入业务dto
  • testGroup是自行测试代码
  • 其他类均为焦点逻辑
    - readme.md 是利用阐明, 防止反面人不知道如何利用
  下面从2.1.3开始

2.1.3 ExcelUtils统一功能封装(单/多sheet导入)

   跳转链接: 解释 @Accessors(chain = true) 与 easyExcel不兼容
  1. import com.alibaba.excel.EasyExcel;
  2. import com.alibaba.excel.ExcelReader;
  3. import com.alibaba.excel.ExcelWriter;
  4. import com.alibaba.excel.read.listener.ReadListener;
  5. import com.alibaba.excel.read.metadata.ReadSheet;
  6. import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
  7. import com.alibaba.excel.write.handler.WriteHandler;
  8. import com.alibaba.excel.write.metadata.WriteSheet;
  9. import com.google.common.collect.Lists;
  10. import lombok.extern.slf4j.Slf4j;
  11. import javax.servlet.http.HttpServletResponse;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.io.UnsupportedEncodingException;
  15. import java.net.URLEncoder;
  16. import java.rmi.ServerException;
  17. import java.util.List;
  18. /**
  19. * Excel相关操作(简易)
  20. * 文章一: 解释 @Accessors(chain = true) 与 easyExcel不兼容
  21. * -> https://blog.csdn.net/qq_36268103/article/details/134954322
  22. *
  23. * @author pzy
  24. * @version 1.1.0
  25. * @description ok
  26. */
  27. @Slf4j
  28. public class ExcelUtils {
  29.     /**
  30.      * 方法1.1: 读取excel(单sheet)
  31.      *
  32.      * @param inputStream 输入流
  33.      * @param dataClass   任意类型
  34.      * @param listener    监听
  35.      * @param sheetNo     sheet编号
  36.      * @param <T>         传入类型
  37.      */
  38.     public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, int sheetNo) {
  39.         try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {
  40.             // 构建一个sheet 这里可以指定名字或者no
  41.             ReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();
  42.             // 读取一个sheet
  43.             excelReader.read(readSheet);
  44.         }
  45.     }
  46.     /**
  47.      * 方法2.1: 读取excel(多sheet)
  48.      *
  49.      * @param inputStream 输入流
  50.      * @param dataClass   任意类型
  51.      * @param listener    监听
  52.      * @param sheetNoList sheet编号
  53.      * @param <T>         传入类型
  54.      */
  55.     public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, List<Integer> sheetNoList) {
  56.         try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {
  57.             List<ReadSheet> readSheetList = Lists.newArrayList();
  58.             sheetNoList.forEach(sheetNo -> {
  59.                 // 构建一个sheet 这里可以指定名字或者no
  60.                 ReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();
  61.                 readSheetList.add(readSheet);
  62.             });
  63.             // 读取一个sheet
  64.             excelReader.read(readSheetList);
  65.         }
  66.     }
  67.     /**
  68.      * 单sheet excel下载
  69.      *
  70.      * @param httpServletResponse 响应对象
  71.      * @param fileName            excel文件名字
  72.      * @param dataClass           class类型(转换)
  73.      * @param sheetName           sheet位置1的名字
  74.      * @param dataList            传入的数据
  75.      * @param writeHandlers       写处理器们 可变参数 (样式)
  76.      * @param <T>                 泛型
  77.      */
  78.     public static <T> void easyDownload(HttpServletResponse httpServletResponse,
  79.                                         String fileName,
  80.                                         Class<T> dataClass,
  81.                                         String sheetName,
  82.                                         List<T> dataList,
  83.                                         WriteHandler... writeHandlers
  84.     ) throws IOException {
  85.         //对响应值进行处理
  86.         getExcelServletResponse(httpServletResponse, fileName);
  87.         ExcelWriterSheetBuilder builder =
  88.                 EasyExcel.write(httpServletResponse.getOutputStream(), dataClass)
  89.                         .sheet(sheetName);
  90. //
  91. //        builder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
  92. //                .registerWriteHandler(ExcelStyleTool.getStyleStrategy());
  93.         /*样式处理器*/
  94.         if (writeHandlers.length > 0) {
  95.             for (WriteHandler writeHandler : writeHandlers) {
  96.                 builder.registerWriteHandler(writeHandler);
  97.             }
  98.         }
  99.         builder.doWrite(dataList);
  100.     }
  101.     /**
  102.      * 复杂 excel下载
  103.      * 1. 多个sheet
  104.      * 2. 多个处理器
  105.      *
  106.      * @param httpServletResponse 响应对象
  107.      * @param fileName            excel文件名字
  108.      * @param dataClass           class类型(转换)
  109.      * @param sheetNameList       多sheet的名字数据
  110.      * @param sheetDataList        多sheet的实际数据
  111.      * @param writeHandlers       写处理器们 可变参数 (样式)
  112.      * @param <T>                 泛型
  113.      */
  114.     public static <T> void complexDownload(HttpServletResponse httpServletResponse,
  115.                                            String fileName,
  116.                                            Class<T> dataClass,
  117.                                            List<String> sheetNameList,
  118.                                            List<List<T>> sheetDataList,
  119.                                            WriteHandler... writeHandlers) throws IOException {
  120.         if (sheetNameList.size() != sheetDataList.size()) {
  121.             throw new ServerException("抱歉,名字与列表长度不符~");
  122.         }
  123.         //对响应值进行处理
  124.         getExcelServletResponse(httpServletResponse, fileName);
  125.         try (ExcelWriter excelWriter = EasyExcel.write(httpServletResponse.getOutputStream()).build()) {
  126.             // 去调用写入, 这里最终会写到多个sheet里面
  127.             for (int i = 0; i < sheetNameList.size(); i++) {
  128.                 ExcelWriterSheetBuilder builder = EasyExcel.writerSheet(i, sheetNameList.get(i)).head(dataClass);
  129.                 if (writeHandlers.length > 0) {
  130.                     for (WriteHandler writeHandler : writeHandlers) {
  131.                         builder.registerWriteHandler(writeHandler);
  132.                     }
  133.                 }
  134.                 WriteSheet writeSheet = builder.build();
  135.                 excelWriter.write(sheetDataList.get(i), writeSheet);
  136.             }
  137.         }
  138.     }
  139.     /**
  140.      * 获取excel的响应对象
  141.      *
  142.      * @param httpServletResponse response
  143.      * @param fileName            文件名
  144.      * @throws UnsupportedEncodingException 不支持编码异常
  145.      */
  146.     private static void getExcelServletResponse(HttpServletResponse httpServletResponse, String fileName) throws UnsupportedEncodingException {
  147.         // 设置URLEncoder.encode可以防止中文乱码,和easyexcel没有关系
  148.         fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
  149.         httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  150.         httpServletResponse.setCharacterEncoding("utf-8");
  151.         httpServletResponse.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
  152.         httpServletResponse.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  153.     }
复制代码
2.1.4 ExcelDataListener数据监听器

   读取excel表格数据 一条一条读取出来
ps: ResultResponse就是返回值封装类 随便都行200或500
  1. import com.alibaba.excel.context.AnalysisContext;
  2. import com.alibaba.excel.read.listener.ReadListener;
  3. import com.alibaba.excel.util.ListUtils;
  4. import com.alibaba.fastjson.JSON;
  5. import com.alibaba.fastjson.JSONObject;
  6. import com.google.common.collect.Lists;
  7. import lombok.extern.slf4j.Slf4j;
  8. import java.util.HashMap;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.concurrent.ConcurrentHashMap;
  12. import java.util.concurrent.atomic.AtomicInteger;
  13. /**
  14. * 官方提供转换listener
  15. * ps: 有个很重要的点 ExcelDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
  16. *
  17. * @author pzy
  18. * @version 0.1.0
  19. * @description ok
  20. */
  21. //@Component
  22. @Slf4j
  23. public class ExcelDataListener<T> implements ReadListener<T> {
  24.     /**
  25.      * 每隔5条存储数据库,实际使用中可以300条,然后清理list ,方便内存回收
  26.      */
  27.     private static final int BATCH_COUNT = 300;
  28.     private final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
  29.     /**
  30.      * 缓存的数据
  31.      */
  32. //    private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
  33.     private final List<T> cachedDataList = Lists.newCopyOnWriteArrayList();
  34.     /**
  35.      * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
  36.      */
  37.     private final ExcelDataService excelDataService;
  38.     /**
  39.      * 自行定义的功能类型 1配件(库存) 2供应商 3客户(假)资料
  40.      */
  41.     private final Integer functionType;
  42.     /**
  43.      * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
  44.      */
  45.     public ExcelDataListener(ExcelDataService excelDataService1, Integer functionType) {
  46.         this.excelDataService = excelDataService1;
  47.         this.functionType = functionType;
  48.     }
  49.     /**
  50.      * 这个每一条数据解析都会来调用
  51.      *
  52.      * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
  53.      */
  54.     @Override
  55.     public void invoke(T data, AnalysisContext context) {
  56. //        String threadName = Thread.currentThread().getName();
  57. //        System.out.println(threadName);
  58.         log.info("解析到一条数据:{}", JSON.toJSONString(data));
  59.         String sheetName = context.readSheetHolder().getSheetName();
  60.         //ps: 慢换LongAdder
  61. //        if (!map.containsKey(sheetName)) {
  62. //            map.put(sheetName, new AtomicInteger(0));
  63. //        } else {
  64. //            map.put(sheetName, new AtomicInteger(map.get(sheetName).incrementAndGet()));
  65. //        }
  66.         int sheetDataCounts = map.computeIfAbsent(sheetName, k -> new AtomicInteger(0)).incrementAndGet();
  67.         log.info("当前sheet的数据是: {}, 数量是第: {}个", sheetName, sheetDataCounts);
  68.         if (data != null) {
  69.             JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(data));
  70.             jsonObject.put("sheetName", sheetName);
  71.             jsonObject.put("sheetDataNo", sheetDataCounts);//放入sheet数据编号(如果仅一个sheet
  72.             cachedDataList.add((T) jsonObject);//类型明确(不增加通配符边界了 增加使用难度)
  73.         }
  74.         // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
  75.         if (cachedDataList.size() >= BATCH_COUNT) {
  76.             saveData();
  77.             // 存储完成清理 list
  78. //            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
  79.             cachedDataList.clear();//这块需要测试看看效果
  80.         }
  81.     }
  82.     /**
  83.      * 所有数据解析完成了 都会来调用
  84.      */
  85.     @Override
  86.     public void doAfterAllAnalysed(AnalysisContext context) {
  87.         // 这里也要保存数据,确保最后遗留的数据也存储到数据库
  88.         log.info("{}条数据,开始存储数据库!", cachedDataList.size());
  89.         saveData();
  90.         cachedDataList.clear();
  91.         log.info("所有数据解析完成!");
  92.     }
  93.     /**
  94.      * 加上存储数据库
  95.      */
  96.     private void saveData() {
  97.         log.info("{}条数据,开始存储数据库!", cachedDataList.size());
  98. //        excelDataService.saveUser((T) new SystemUser());
  99.         ResultResponse response = excelDataService.saveExcelData(functionType, cachedDataList);
  100.         if (ResponseHelper.judgeResp(response)) {
  101.             log.info("存储数据库成功!");
  102.         }
  103.     }
  104. }
复制代码
2.1.5 ResponseHelper相应值处置处罚

   ResultResponse返回值封装类 任意即可
  1. import com.alibaba.fastjson.TypeReference;
  2. import org.springframework.web.context.request.RequestAttributes;
  3. import org.springframework.web.context.request.RequestContextHolder;
  4. import javax.servlet.http.HttpServletRequest;
  5. import java.util.Objects;
  6. /**
  7. * 响应 工具类
  8. *
  9. * @author pzy
  10. * @version 0.1.0
  11. * @description ok
  12. */
  13. public class ResponseHelper<T> {
  14.     /**
  15.      * 响应成功失败校验器
  16.      * 疑似存在bug(未进行测试)
  17.      */
  18.     @Deprecated
  19.     public T retBool(ResultResponse response) {
  20.         /*1. 如果接口返回值返回的不是200 抛出异常*/
  21.         if (response.getCode() != 200) {
  22.             throw new ServiceException(response.getCode(), response.getMsg());
  23.         }
  24.         return response.getData(new TypeReference<T>() {
  25.         });
  26.     }
  27.     /**
  28.      * 请求响应值校验器(ResultResponse对象)
  29.      */
  30.     public static void retBoolResp(ResultResponse response) {
  31.         if (response == null) {
  32.             throw new NullPointerException("服务响应异常!");
  33.         }
  34.         
  35.         /*1. 如果接口返回值返回的不是200 抛出异常*/
  36.         if (!Objects.equals(response.getCode(), 200)) {
  37.             throw new ServiceException(response.getCode(), response.getMsg());
  38.         }
  39.     }
  40.     /**
  41.      * 判定响应返回值
  42.      * <p>
  43.      * true 表示200 服务通畅
  44.      * false 表示500 服务不通畅(
  45.      */
  46.     public static boolean judgeResp(ResultResponse response) {
  47.         // 1. 如果接口返回值返回的不是200 返回false
  48.         return response != null && Objects.equals(response.getCode(), 200);
  49.     }
  50.     /**
  51.      * 通过上下文对象获取请求头的token值
  52.      * RequestHelper.getHeaderToken()
  53.      */
  54.     @Deprecated
  55.     public static String getHeaderToken() {
  56.         //请求上下文对象获取 线程
  57.         RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  58.         assert requestAttributes != null;
  59.         HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
  60.         assert request != null;
  61.         return request.getHeader("token");
  62.     }
  63. }
复制代码
2.1.6 MyConverter类-自定义转换器

   @ExcelProperty(converter = MyConverter.class) 利用自定义转换器 针对list等类型进行操作
  1. import com.alibaba.excel.converters.Converter;
  2. import com.alibaba.excel.converters.ReadConverterContext;
  3. import com.alibaba.excel.converters.WriteConverterContext;
  4. import com.alibaba.excel.enums.CellDataTypeEnum;
  5. import com.alibaba.excel.metadata.data.WriteCellData;
  6. import java.util.Collections;
  7. import java.util.List;
  8. import java.util.StringJoiner;
  9. /**
  10. * list类型使用 自定义转换器(补充功能 beta版)
  11. * @author pzy
  12. * @version 0.1.0
  13. * @description ok
  14. */
  15. public class MyConverter implements Converter<List> {
  16.     @Override
  17.     public Class<?> supportJavaTypeKey() {
  18.         return List.class;
  19.     }
  20.     @Override
  21.     public CellDataTypeEnum supportExcelTypeKey() {
  22.         return CellDataTypeEnum.STRING;
  23.     }
  24.     /**
  25.      * 读(导入)数据时调用
  26.      */
  27.     @Override
  28.     public List convertToJavaData(ReadConverterContext<?> context) {
  29.         //当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用
  30.         //context.getReadCellData().getStringValue()会获取excel表格中该字段对应的String数据
  31.         //这里可以对数据进行额外的加工处理
  32.         String stringValue = context.getReadCellData().getStringValue();
  33.         //将数据转换为List类型然后返回给实体类对象DTO
  34.         return Collections.singletonList(stringValue);
  35.     }
  36.     /**
  37.      * 写(导出)数据时调用
  38.      */
  39.     @Override
  40.     public WriteCellData<?> convertToExcelData(WriteConverterContext<List> context) {
  41.         //当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用
  42.         //context.getValue()会获取对应字段的List类型数据
  43.         //这里是将List<String>转换为String类型数据,根据自己的数据进行处理
  44.         StringJoiner joiner = new StringJoiner(",");
  45.         for (Object data : context.getValue()) {
  46.             joiner.add((CharSequence) data);
  47.         }
  48.         //然后将转换后的String类型数据写入到Excel表格对应字段当中
  49.         return new WriteCellData<>(joiner.toString());
  50.     }
  51. }
复制代码
2.1.7 ExcelDataService

   数据处置处罚行为接口(多工程拓展)
  1. import java.util.List;
  2. /**
  3. * 数据处理service
  4. *
  5. * @author pzy
  6. * @version 0.1.0
  7. * @description ok
  8. */
  9. @FunctionalInterface
  10. public interface ExcelDataService {
  11.     /**
  12.      * 保存导入的数据
  13.      * 分批进入 防止数据过大 - 栈溢出
  14.      *
  15.      * @param t 保存的数据类型
  16.      */
  17.     <T> ResultResponse saveExcelData(Integer functionType, List<T> t);
  18. }
复制代码
2.1.8 ExcelReqDTO 统一哀求dto

   业务必要, 生成的文件名 sheet的名称 功能类型等信息
其中Lists.newArrayList() 没有的直接换成new ArrayList()即可 效果雷同
  1. import com.google.common.collect.Lists;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import lombok.experimental.Accessors;
  6. import java.util.List;
  7. /**
  8. * excel统一请求dto
  9. * <p>
  10. * 传入需要的参数, 生成对应的excel表格
  11. *
  12. * @author pzy
  13. * @version 0.1.0
  14. * @description ok
  15. */
  16. @Data
  17. @NoArgsConstructor
  18. @AllArgsConstructor
  19. @Accessors(chain = true)
  20. public class ExcelReqDTO {
  21.     /**
  22.      * 功能类型 例: 1用户 2其他业务
  23.      */
  24.     private Integer functionType;
  25.     /**
  26.      * excel类型 1单sheet 2多sheet
  27.      */
  28.     private Integer excelType;
  29.     /**
  30.      * 文件名称
  31.      */
  32.     private String fileName;
  33.     /**
  34.      * sheet名称
  35.      */
  36.     private String sheetName;
  37.     /**
  38.      * sheet名称组
  39.      */
  40.     private List<String> sheetNames = Lists.newArrayList();
  41. }
复制代码
2.1.9 上传文件校验

   文件巨细校验可在设置文件内添加, 效果更好
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.web.multipart.MultipartFile;
  3. import java.math.BigDecimal;
  4. import java.math.RoundingMode;
  5. import java.util.Arrays;
  6. import java.util.Locale;
  7. /**
  8. * 文件上传校验的公共方法
  9. * 严格校验
  10. *
  11. * @author pzy
  12. * @version 1.0.0
  13. */
  14. @Slf4j
  15. public class UploadCheckUtils {
  16.     //20MB
  17.     private static final Integer maxUpLoadSize = 20;
  18.     /**
  19.      * 只支持文件格式
  20.      */
  21.     public static final String[] YES_FILE_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv"};
  22.     /**
  23.      * 全部文件(普通文件,图片, 视频,音频)后缀 支持的类型
  24.      */
  25.     private static final String[] FILE_SUFFIX_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv",
  26.             ".jpg", ".jpeg", ".png", ".mp4", ".avi", ".mp3"};
  27.     /**
  28.      * 文件名字 需要排除的字符
  29.      * 废弃:  "(", ")","",".", "——", "_","-"
  30.      */
  31.     private static final String[] FILE_NAME_EXCLUDE = {
  32.             "`", "!", "@", "#", "$", "%", "^", "&", "*", "=", "+",
  33.             "~", "·", "!", "¥", "……", "(", ")",
  34.             "?", ",", "<", ">", ":", ";", "[", "]", "{", "}", "/", "\", "|",
  35.             "?", ",", "。", "《", "》", ":", ";", "【", "】", "、"
  36.     };
  37.     /**
  38.      * 多文件上传
  39.      * 校验+大图片压缩
  40.      */
  41.     public MultipartFile[] uploadVerify(MultipartFile[] multipartFile) {
  42.         /*校验1: 没有文件时,报错提示*/
  43.         if (multipartFile == null || multipartFile.length <= 0) {
  44.             throw new ServiceException(500, "上传文件不能为空");
  45.         }
  46.         /*总文件大于: ?Mb时, 拦截*/
  47.         long sumSize = 0;
  48.         for (MultipartFile file : multipartFile) {
  49.             sumSize += file.getSize();
  50.         }
  51.         // 总文件超过100mb 直接拦截 beta功能 不正式使用
  52.         if (sumSize > (100 * 1024 * 1024L)) {
  53.             log.warn("(上传总空间)大于100MB, 文件上传过大!");
  54. //            throw new ThirdServiceException(ResponseEnum.T160007, "(上传总空间)100");
  55.         }
  56.         /*校验2: 上传文件的长度小于等于1 就一个直接校验*/
  57.         if (multipartFile.length <= 1) {
  58.             MultipartFile[] files = new MultipartFile[1];
  59.             files[0] = uploadVerify(multipartFile[0]);
  60.             return files;
  61.         }
  62.         /*校验3: 多个文件直接校验 需要更换新的file */
  63.         for (int i = 0; i < multipartFile.length; i++) {
  64.             multipartFile[i] = uploadVerify(multipartFile[i]);
  65.         }
  66.         return multipartFile;
  67.     }
  68.     /**
  69.      * 上传文件校验大小、名字、后缀
  70.      *
  71.      * @param multipartFile multipartFile
  72.      */
  73.     public static MultipartFile uploadVerify(MultipartFile multipartFile) {
  74.         // 校验文件是否为空
  75.         if (multipartFile == null) {
  76.             throw new ServiceException(500, "上传文件不能为空呦~");
  77.         }
  78.         /*大小校验*/
  79.         log.info("上传文件的大小的是: {} MB", new BigDecimal(multipartFile.getSize()).divide(BigDecimal.valueOf(1024 * 1024), CommonConstants.FINANCE_SCALE_LENGTH, RoundingMode.HALF_UP));
  80.         log.info("上传限制的文件大小是: {} MB", maxUpLoadSize);
  81.         if (multipartFile.getSize() > (maxUpLoadSize * 1024 * 1024L)) {
  82.             throw new ServiceException(500, String.format("上传文件不得大于 %s MB", maxUpLoadSize));
  83.         }
  84.         // 校验文件名字
  85.         String originalFilename = multipartFile.getOriginalFilename();
  86.         if (originalFilename == null) {
  87.             throw new ServiceException(500, "上传文件名字不能为空呦~");
  88.         }
  89.         for (String realKey : FILE_NAME_EXCLUDE) {
  90.             if (originalFilename.contains(realKey)) {
  91.                 throw new ServiceException(500, String.format("文件名字不允许出现 '%s' 关键字呦~", realKey));
  92.             }
  93.         }
  94.         // 校验文件后缀
  95.         if (!originalFilename.contains(".")) {
  96.             throw new ServiceException(500, "文件不能没有后缀呦~");
  97.         }
  98.         String suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));
  99.         /*校验: 文件格式是否符合要求*/
  100.         if (!Arrays.asList(FILE_SUFFIX_SUPPORT).contains(suffix.toLowerCase(Locale.ROOT))) {
  101.             //throw new RuntimeException("文件格式' " + realFormat + " '不支持,请更换后重试!");
  102.             throw new ServiceException(500, "文件格式不支持呦~");
  103.         }
  104.         return multipartFile;
  105.     }
  106. }
复制代码
2.1.10 最后写个readme.md(阐明利用方式)

   这里写不写都行, 如有错误,请指出,谢谢啦~
  1. # excel工具类使用说明
  2. ## 1.本功能支持
  3. 1. excel导入
  4. 2. excel导出
  5. 3. 样式调整
  6. 4. 类型转换器
  7. ## 2. 使用技术介绍
  8. - 使用alibaba的easyExcel 3.3.4版本
  9. - 官网地址: [=> easyExcel新手必读 ](https://easyexcel.opensource.alibaba.com/docs/current)
  10. ## 3. 功能说明
  11. 1. ExcelUtils 统一工具类 封装了单/多sheet的导入与导出 任意类型传入 只需`.class`即可
  12. 2. ExcelStyleTool excel表格导出风格自定义
  13. 3. MyConverter: 对于list类型转换存在问题, 手写新的转换器(beta版)
  14. 4. ExcelDataListener 数据监听器, 在这里处理接收的数据
  15. 5. ExcelDataService 数据处理服务接口(封装统一的功能要求, 同时满足拓展性)
  16. 6. testGroup中 全部均为演示demo(请在需要的工程中使用)
  17. ## 4. 功能的演示
  18. 1. upload.html 前端简易测试功能页面(测试功能)
  19. ## 5. 版本说明
  20. 1. beta版(1.0.1), 测试中
  21. 2. 可能有更好的方法解决本次业务需求
  22. 3. 导出的样式仅仅是简易能用, 跟美观没啥关系
  23. ## 6. 特别注意
  24. 1. 生成的excel的实体类均需要新写(或者看6-2)
  25. 2. @Accessors不可使用: 源码位置-> (ModelBuildEventListener的buildUserModel)中的BeanMap.create(resultModel).putAll(map);
  26. > [不能使用@Accessors(chain = true) 注解原因: ](https://blog.csdn.net/zmx729618/article/details/78363191)
  27. >
  28. ## 7. 本文作者
  29. > @author: pzy
复制代码
2.2 easyExcel工具包(全)利用方式

   testGroup组演示
  2.2.1 UserExcelDTO 生成用户excel数据

   跟随业务随意, 用啥字段就加啥, @ExcelIgnore //体现忽略此字段
  1. import com.alibaba.excel.annotation.ExcelIgnore;
  2. import com.alibaba.excel.annotation.ExcelProperty;
  3. import com.alibaba.excel.annotation.write.style.ColumnWidth;
  4. import com.alibaba.excel.annotation.write.style.ContentRowHeight;
  5. import com.alibaba.excel.annotation.write.style.HeadRowHeight;
  6. import lombok.AllArgsConstructor;
  7. import lombok.Data;
  8. import lombok.NoArgsConstructor;
  9. /**
  10. * excel表格示例demo
  11. * ps: 不能用accessors
  12. *
  13. * @author pzy
  14. * @version 0.1.0
  15. * @description ok
  16. */
  17. @ContentRowHeight(20)
  18. @HeadRowHeight(30)
  19. @ColumnWidth(25)
  20. @NoArgsConstructor
  21. @AllArgsConstructor
  22. //@Accessors(chain = true)
  23. @Data
  24. public class UserExcelDTO {
  25.     /**
  26.      * 用户ID
  27.      */
  28. //    @ExcelIgnore //忽略
  29.     @ColumnWidth(20)
  30.     @ExcelProperty(value = "用户编号")
  31.     private Long userId;
  32.     @ColumnWidth(50)
  33.     @ExcelProperty(value = "真实姓名")
  34.     private String realName;
  35.     @ColumnWidth(50)
  36.     @ExcelProperty(value = "手机号")
  37.     private String phone;
  38.     /**
  39.      * 用户邮箱
  40.      */
  41.     @ColumnWidth(50)
  42.     //@ExcelProperty(value = "邮箱",converter = MyConverter.class)
  43.     @ExcelProperty(value = "邮箱")
  44.     private String email;
  45. }
复制代码
2.2.2 ExcelDataServiceImpl实现类(工程一)

   模仿一下数据库行为操作, 反面有现实操作呦~
  1. import java.util.List;
  2. /**
  3. * 实现类 demo实现方式 (此处不可注入bean) 示例文档
  4. *
  5. * @author pzy
  6. * @version 0.1.0
  7. * @description ok
  8. */
  9. //@Slf4j
  10. //@RequiredArgsConstructor
  11. //@Service
  12. public class ExcelDataServiceImpl implements ExcelDataService {
  13.     /**
  14.      * 保存导入的数据
  15.      * 分批进入 防止数据过大 - 栈溢出
  16.      *
  17.      * @param t 保存的数据类型
  18.      */
  19.     @Override
  20.     public <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {
  21.         //测试演示(添加数据库)
  22.         return ResultResponse.booleanToResponse(true);
  23.     }
  24. //
  25. //    /**
  26. //     * 获取数据并导出到excel表格中
  27. //     *
  28. //     * @param t 传入对象
  29. //     * @return t类型集合
  30. //     */
  31. //    @Override
  32. //    public <T> List<T> getExcelData(T t) {
  33. //        //测试演示
  34. //        return null;
  35. //    }
  36. }
复制代码
2.2.3 upload.html测试页面

   网上找的前端页面, 改了改, 自行测试, 我这里没有token传入位置,

    解决方案一: 后端放行一下, 测试后关闭即可
解决方案二: 让前端直接连, 用前端写过的页面
等等
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>EasyExcel</title>
  6. </head>
  7. <body>
  8. <div class="app">
  9.     <input type="file" id="fileInput" accept=".xlsx, .xls, .csv">
  10.     <button onclick="upload()">单sheet上传</button>
  11.     <br>
  12.     <br>
  13.     <input type="file" id="fileInput1" accept=".xlsx, .xls, .csv">
  14.     <button onclick="upload1()">多sheet上传</button>
  15. </div>
  16. <br>
  17. <div>
  18.     <button onclick="download()">单sheet导出</button>&nbsp;
  19.     <button onclick="download1()">多sheet导出</button>
  20. </div>
  21. </body>
  22. <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  23. <script>
  24.     const upload = () => {
  25.         // 获取文件输入元素
  26.         const fileInput = document.getElementById('fileInput')
  27.         // 获取选中的文件
  28.         const file = fileInput.files[0]
  29.         if (!file) {
  30.             alert('请选择一个文件')
  31.             return
  32.         }
  33.         // 创建 FormData 对象
  34.         const formData = new FormData()
  35.         // 将文件添加到 FormData 对象
  36.         formData.append('file', file)
  37.         // 发送 POST 请求到后端
  38.         axios.post('http://localhost:8001/system/excel/upload?functionType=1', formData, {
  39.             headers: {
  40.                 'Content-Type': 'multipart/form-data' // 设置正确的 Content-Type
  41.             }
  42.         }).then(response => {
  43.             alert('文件上传成功')
  44.             console.log('文件上传成功:', response.data)
  45.         }).catch(error => {
  46.             console.error('文件上传失败:', error)
  47.         });
  48.     }
  49.     const upload1 = () => {
  50.         // 获取文件输入元素
  51.         const fileInput = document.getElementById('fileInput1')
  52.         // 获取选中的文件
  53.         const file = fileInput.files[0]
  54.         if (!file) {
  55.             alert('请选择一个文件')
  56.             return
  57.         }
  58.         // 创建 FormData 对象
  59.         const formData = new FormData()
  60.         // 将文件添加到 FormData 对象
  61.         formData.append('file', file)
  62.         // 发送 POST 请求到后端
  63.         axios.post('http://localhost:8001/system/excel/upload1?functionType=2', formData, {
  64.             headers: {
  65.                 'Content-Type': 'multipart/form-data', // 设置正确的 Content-Type
  66.                 'token': ''
  67.             }
  68.         }).then(response => {
  69.             alert('文件上传成功')
  70.             console.log('文件上传成功:', response.data)
  71.         }).catch(error => {
  72.             console.error('文件上传失败:', error)
  73.         });
  74.     }
  75.     const headers = {
  76.         // 'Content-Type': 'application/json', // 设置请求头部的Content-Type为application/json
  77.         // token: '', // 设置请求头部的Authorization为Bearer your_token
  78.         // 'Token4545': '1', // 设置请求头部的Authorization为Bearer your_token
  79.         // 'responseType': 'blob', // 设置响应类型为blob(二进制大对象)
  80.     };
  81.     const download = () => {
  82.         const url = 'http://192.168.1.254:8001/system/excel/download?fileName=单S文件&functionType=1'
  83.         axios.get(url, {
  84.             responseType: 'blob'
  85.         }).then(response => {
  86.             // 从Content-Disposition头部中获取文件名
  87.             const contentDisposition = response.headers['content-disposition']
  88.             console.log(response)
  89.             console.log(contentDisposition)
  90.             const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)
  91.             console.log(matches)
  92.             let filename = 'downloaded.xlsx'
  93.             if (matches != null && matches[2] != null) {
  94.                 console.log(matches[2])
  95.                 // 解码RFC 5987编码的文件名
  96.                 filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))
  97.             } else {
  98.                 // 如果没有filename*,尝试使用filename
  99.                 const filenameMatch = /filename="(.*)"/.exec(contentDisposition);
  100.                 console.log(71)
  101.                 if (filenameMatch != null && filenameMatch[1] != null) {
  102.                     filename = filenameMatch[1]
  103.                     console.log(74)
  104.                 }
  105.             }
  106.             // 创建一个a标签用于下载
  107.             const a = document.createElement('a')
  108.             // 创建一个URL对象,指向下载的文件
  109.             const url = window.URL.createObjectURL(new Blob([response.data]))
  110.             a.href = url
  111.             a.download = filename // 设置文件名
  112.             document.body.appendChild(a)
  113.             a.click()
  114.             document.body.removeChild(a)
  115.             window.URL.revokeObjectURL(url)
  116.         }).catch(error => {
  117.             console.error('下载文件时出错:', error)
  118.         })
  119.     }
  120.     const download1 = () => {
  121.         const url = 'http://192.168.1.254:8001/system/excel/test2'
  122.         axios.get(url, {
  123.             responseType: 'blob', // 设置响应类型为blob(二进制大对象)
  124.         }).then(response => {
  125.             // 从Content-Disposition头部中获取文件名
  126.             const contentDisposition = response.headers['content-disposition']
  127.             console.log(response)
  128.             console.log(contentDisposition)
  129.             const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)
  130.             console.log(matches)
  131.             let filename = 'downloaded.xlsx'
  132.             if (matches != null && matches[2] != null) {
  133.                 console.log(matches[2])
  134.                 // 解码RFC 5987编码的文件名
  135.                 filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))
  136.             } else {
  137.                 // 如果没有filename*,尝试使用filename
  138.                 const filenameMatch = /filename="(.*)"/.exec(contentDisposition);
  139.                 console.log(71)
  140.                 if (filenameMatch != null && filenameMatch[1] != null) {
  141.                     filename = filenameMatch[1]
  142.                     console.log(74)
  143.                 }
  144.             }
  145.             // 创建一个a标签用于下载
  146.             const a = document.createElement('a')
  147.             // 创建一个URL对象,指向下载的文件
  148.             const url = window.URL.createObjectURL(new Blob([response.data]))
  149.             a.href = url
  150.             a.download = filename // 设置文件名
  151.             document.body.appendChild(a)
  152.             a.click()
  153.             document.body.removeChild(a)
  154.             window.URL.revokeObjectURL(url)
  155.         }).catch(error => {
  156.             console.error('下载文件时出错:', error)
  157.         })
  158.     }
  159. </script>
  160. </html>
复制代码
3.业务实战方式与效果(可跳过2.2)焦点

前言: 2.2介绍的是简朴的demo, 根据谁人进行拓展
   业务需求
  

  • 客户点击- 生成模板, 生成空的excel模板
  • 根据阐明填写具体信息
  • 导入后, 如果数据正常,导入成功
  • 导入异常, 则明白告知数据问题在哪
  • 本次导入的数据均不见效
  • 面对多sheet导入异常, 明白指出 sheet名内的第*条数据,什么问题, 其他上同
  操作方式:
  

  • 设置批次导入(发放唯一批次号)
  • 同批次的一组报错全部回滚
  • 导入时生成批次, 整个线程利用一个批次
  • 全字段自定义校验, 准确定位错误数据,给出精准提示
  3.1 业务工具类

3.1.1 ThreadLocalUtils工具类(批次号)

   写个底子的set和get , 通过当火线程转达xid号,
  1. import java.util.Map;
  2. /**
  3. * threadLocal使用工具方法
  4. * <p>
  5. * ps: jdk建议将 ThreadLocal 定义为 private static
  6. * 避免: 有弱引用,内存泄漏的问题了
  7. *
  8. * @author pzy
  9. * @description TODO beta01测试中
  10. * @version 1.0.1
  11. */
  12. public class ThreadLocalUtils {
  13.     private static final ThreadLocal<Map<String, Object>> mapThreadLocal = new ThreadLocal<>();
  14.     //获取当前线程的存的变量
  15.     public static Map<String, Object> get() {
  16.         return mapThreadLocal.get();
  17.     }
  18.     //设置当前线程的存的变量
  19.     public static void set(Map<String, Object> map) {
  20.         mapThreadLocal.set(map);
  21.     }
  22.     //移除当前线程的存的变量
  23.     public static void remove() {
  24.         mapThreadLocal.remove();
  25.     }
  26. }
复制代码
3.1.2 自定义字段校验(注解)

-> 3.1.2_1 创建校验注解@DataCheck

   如有更过细的校验, 请自行添加
  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. /**
  6. * 实体类-数据校验注解
  7. * <p>
  8. * ps: 第一版
  9. * 校验方式
  10. * 1. 数据为空
  11. * 2. 最大长度
  12. * 3. 正则表达式
  13. * 4. 报错信息
  14. * <p>
  15. * 其中功能校验在 ValidatorUtils 中
  16. *
  17. * @author pzy
  18. * @version 1.0.1
  19. * @description ok
  20. */
  21. @Target(ElementType.FIELD)
  22. @Retention(RetentionPolicy.RUNTIME)
  23. public @interface DataCheck {
  24.     /**
  25.      * 校验不能为空 true开启  false关闭
  26.      */
  27.     boolean notBank() default false;
  28.     /**
  29.      * 长度
  30.      */
  31.     int maxLength() default -1;
  32.     /**
  33.      * 正则表达式
  34.      */
  35.     String value() default "";
  36.     /**
  37.      * 报错信息
  38.      */
  39.     String message() default "";
  40. }
复制代码
-> 3.1.2_2 注解实现类ValidatorUtils(校验逻辑)

   对@DataCheck校验逻辑进行支持, 其中异常条数和异常sheet名称(多sheet必要)必要转达
这里先不管这俩参数
方法一: 单sheet
方法二: 多sheet
  1. import com.alibaba.fastjson.JSON;
  2. import lombok.SneakyThrows;
  3. import lombok.extern.slf4j.Slf4j;
  4. import java.lang.reflect.Field;
  5. /**
  6. * 校验器工具类
  7. */
  8. @Slf4j
  9. public class ValidatorUtils {
  10.     /**
  11.      * DataCheck注册-正则校验器1
  12.      */
  13.     @SneakyThrows
  14.     public static ResultResponse validate(Object obj, Integer errorCounts) {
  15.         return validate(obj, errorCounts, null);
  16.     }
  17.     /**
  18.      * DataCheck注册-正则校验器2
  19.      */
  20.     @SneakyThrows
  21.     public static ResultResponse validate(Object obj, Integer errorCounts, String sheetName) {
  22.         Field[] fields = obj.getClass().getDeclaredFields();
  23.         for (Field field : fields) {
  24.             if (field.isAnnotationPresent(DataCheck.class)) {
  25.                 DataCheck annotation = field.getAnnotation(DataCheck.class);
  26.                 field.setAccessible(true);
  27.                 Object value = field.get(obj);//实体类参数
  28.                 int maxLength = annotation.maxLength(); //长度
  29.                 String message = "";
  30.                 if (StringUtils.isNotBlank(sheetName)) {
  31.                     message = String.format("可能是: 品类: %s ,第 %s 条,要求: %s", sheetName, errorCounts, annotation.message()); //报错信息
  32.                 } else {
  33.                     message = String.format("可能是: 第 %s 条,要求: %s", errorCounts, annotation.message()); //报错信息
  34.                 }
  35.                 String matchValue = annotation.value();//正则表达式
  36.                 /*校验1: 开启校验 且参数是空的 */
  37.                 if (annotation.notBank() && (value == null || value == "")) {
  38.                     log.warn("Field :[" + field.getName() + "] is null");
  39.                     log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
  40. //                    throw new IllegalArgumentException("数据为空呦, " + message);
  41.                     return ResultResponse.error("数据为空呦, " + message);
  42.                 }
  43.                 /*校验2: 长度字段大于0 并且长度大于*/
  44.                 if (maxLength > 0) {
  45.                     if (maxLength < String.valueOf(value).length()) {
  46.                         log.warn("Field :[" + field.getName() + " ] is out of range");
  47.                         log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
  48. //                        throw new IllegalArgumentException("数据超范围了呦, " + message);
  49.                         return ResultResponse.error("数据超范围了呦, " + message);
  50.                     }
  51.                 }
  52.                 /*校验3: 正则不匹配 则刨除异常*/
  53.                 if (StringUtils.isNotBlank(matchValue) && value != null && !value.toString().matches(matchValue)) {
  54.                     log.warn("Field :[" + field.getName() + "] is not match");
  55.                     log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
  56. //                    throw new IllegalArgumentException("数据格式不对呦, " + message);
  57.                     return ResultResponse.error("数据格式不对呦, " + message);
  58.                 }
  59.             }
  60.         }
  61.         return ResultResponse.ok();
  62.     }
  63. }
复制代码
3.2 工程内业务利用

3.2.0 创建上传或下载对象dto

   添加校验注解 excel注册等, 不可利用@Accessors注解
  1. /**
  2. * 临时客户dto
  3. *
  4. * @author pzy
  5. * @version 0.1.0
  6. * @description ok
  7. */
  8. @Data
  9. @NoArgsConstructor
  10. @AllArgsConstructor
  11. public class UserTempDTO {
  12.     @DataCheck(notBank = true, maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "(非空)用户姓名支持中文,英文,数字,'-' 和'_', 长度255位")
  13.     @ExcelProperty(value = "真实姓名")
  14.     private String realname;
  15.     @DataCheck(maxLength = 2, message = "性别请填写: 男,女,未知")
  16.     @ExcelProperty(value = "性别")
  17.     private String gender;
  18.     @DataCheck(notBank = true,maxLength = 255, value = "0?(13|14|15|18|17)[0-9]{9}", message = "(非空)手机号需纯数字且长度11位")
  19.     @ExcelProperty(value = "电话号")
  20.     private String phone;
  21. //    @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "地址信息名称支持中文,英文,数字,'-' 和'_', 长度255位")
  22.     @DataCheck(maxLength = 255, message = "地址信息名称长度255位")
  23.     @ExcelProperty(value = "地址信息")
  24.     private String familyAddr;
  25. //    @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "头像链接地址,长度255位")
  26.     @DataCheck(maxLength = 255, message = "头像链接地址,长度255位")
  27.     @ExcelProperty(value = "头像")
  28.     private String avatarUrl;
  29.     //---------------------------------------->
  30.     @ExcelIgnore
  31.     @ExcelProperty(value = "备用电话号")
  32.     private String sparePhone;
  33.     @ExcelIgnore
  34.     @ExcelProperty(value = "昵称")
  35.     private String nickname;
  36.     @ExcelIgnore
  37.     @ApiModelProperty(value = "excel的sheet名称")
  38.     private String sheetName;
  39.     @ExcelIgnore
  40.     @ApiModelProperty(value = "excel的sheet名称对应行号,用于报错行数")
  41.     private String sheetDataNo;
  42.     @ExcelIgnore
  43.     @ApiModelProperty(value = "xid号")
  44.     private String xid;
  45.   }
复制代码
  测试校验是否见效
  1.     public static void main(String[] args) {
  2.         UserTempDTO userTempDTO = new UserTempDTO();
  3.         userTempDTO.setRealname("");
  4.         userTempDTO.setGender("男");
  5.         userTempDTO.setPhone("14788888888");
  6.         userTempDTO.setFamilyAddr("");
  7.         userTempDTO.setAvatarUrl("");
  8.         ValidatorUtils.validate(userTempDTO,10);
  9.     }
复制代码
3.2.1 创建controller

   业务的入口
  1. @Slf4j
  2. @RequiredArgsConstructor
  3. @RestController
  4. @RequestMapping("/excel/test")
  5. public class SystemExcelController {
  6.     private final SystemExcelService systemExcelService;
  7.     @PostMapping("/upload")
  8.     public ResultResponse upload(MultipartFile file, ExcelReqDTO excelReqDTO) throws IOException {
  9.         log.info("===> excel文件上传 <===");
  10.         //文件校验
  11.         UploadCheckUtils.uploadVerify(file);
  12.         try {
  13.             Map<String, Object> map = new HashMap<>();
  14.             long snowId = IdGenerater.getInstance().nextId();
  15.             log.info("excel导入e_xid===> {}",snowId);
  16.             map.put("e_xid", snowId);
  17.             //存入threadLocal
  18.             ThreadLocalUtils.set(map);
  19.             systemExcelService.upload(file, excelReqDTO);
  20.         } finally {
  21.             ThreadLocalUtils.remove();
  22.         }
  23.         return ResultResponse.ok("操作成功");
  24.     }
  25.     @GetMapping("/download")
  26.     public void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) throws IOException {
  27.         log.info("===> excel文件下载 <===");
  28.         systemExcelService.download(httpServletResponse, excelReqDTO);
  29.     }
  30. }
复制代码
3.2.2 接口SystemExcelService

  1. /**
  2. * excel表格实现类
  3. * @author pzy
  4. * @version 0.1.0
  5. * @description ok
  6. */
  7. public interface SystemExcelService {
  8.     /**
  9.      * 上传excel文件
  10.      * @param file 文件
  11.      * @param excelReqDTO 请求参数
  12.      */
  13.     void upload(MultipartFile file, ExcelReqDTO excelReqDTO);
  14.     void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO);
  15. }
复制代码
3.2.3 实现类SystemExcelServiceImpl(需根业务自行调整)

   这里面就是具体业务了
利用了ExcelUtils方法 实现多/单sheet导入与导出
导入ps: 在利用excelUtils方法时, 必要注入ExcelDataService接口来实现数据库存储操作
导出ps: 查询数据库数据, 处置处罚 传入Lists.newArrayList() 这个位置即可
  1. /**
  2. * excel表格实现类
  3. *
  4. * @author pzy
  5. * @version 0.1.0
  6. * @description ok
  7. */
  8. @Service
  9. @Slf4j
  10. @RequiredArgsConstructor
  11. public class SystemExcelServiceImpl implements SystemExcelService {
  12.     private final ExcelDataService excelDataService;
  13.    
  14.     /**
  15.      * 上传excel功能文件
  16.      *
  17.      * @param file        文件
  18.      * @param excelReqDTO 请求参数
  19.      */
  20.     @SneakyThrows
  21.     @Override
  22.     public void upload(MultipartFile file, ExcelReqDTO excelReqDTO) {
  23.         //功能类型 1 2 3
  24.         Integer functionType = excelReqDTO.getFunctionType();
  25.         if (Objects.equals(functionType, 1)) {//多sheet
  26.             ExcelUtils.readExcel(file.getInputStream(),
  27.                     *.class,
  28.                     new ExcelDataListener<>(excelDataService, functionType),
  29.                     MathUtils.getIntRangeToList(0, 8)
  30.             );
  31.         } else if (Objects.equals(functionType, 2)) {//
  32.             //单sheet
  33.             ExcelUtils.readExcel(file.getInputStream(),
  34.                     *.class,
  35.                     new ExcelDataListener<>(excelDataService, functionType), 0
  36.             );
  37.         } else if (Objects.equals(functionType, 3)) {//
  38.             //单sheet
  39.             ExcelUtils.readExcel(file.getInputStream(),
  40.                     *.class,
  41.                     new ExcelDataListener<>(excelDataService, functionType), 0
  42.             );
  43.         } else {
  44.             throw new ServiceException(ResponseEnum.E30001);
  45.         }
  46.     }
  47.     @SneakyThrows
  48.     @Override
  49.     public void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) {
  50.         String fileName = excelReqDTO.getFileName();
  51.         if (StringUtils.isBlank(fileName) || fileName.length() > 6) {
  52.             throw new ServiceException("抱歉名称长度需大于0且不能超过6呦~");
  53.         }
  54.         //功能类型 1 2 3
  55.         Integer functionType = excelReqDTO.getFunctionType();
  56.         if (Objects.equals(functionType, 1)) {
  57.             //sheet名字
  58.             List<String> sheetNameList = ?;
  59.             List<List<*>> sheetDataList = Lists.newArrayList();
  60.             sheetNameList.forEach(sheetDto->sheetDataList.add(Lists.newArrayList()));
  61.             ExcelUtils.complexDownload(httpServletResponse, fileName,
  62.                     ShopOfflineListDTO.class, sheetNameList,
  63.                     sheetDataList,
  64.                     new LongestMatchColumnWidthStyleStrategy(),
  65.                     ExcelStyleTool.getStyleStrategy()
  66.             );
  67.         } else if (Objects.equals(functionType, 2)) {//
  68.             //写出excel核心代码
  69.             ExcelUtils.easyDownload(httpServletResponse,
  70.                     fileName,
  71.                     *.class,
  72.                     "模板1",
  73.                     Lists.newArrayList(),//需要数据就传入 不需要就传递空集合
  74.                     new LongestMatchColumnWidthStyleStrategy(),
  75.                     ExcelStyleTool.getStyleStrategy()
  76.             );
  77.         } else if (Objects.equals(functionType, 3)) {
  78.             //写出excel核心代码
  79.             ExcelUtils.easyDownload(httpServletResponse,
  80.                     fileName,
  81.                     *.class,
  82.                     "模板1",
  83.                     Lists.newArrayList(),
  84.                     new LongestMatchColumnWidthStyleStrategy(),
  85.                     ExcelStyleTool.getStyleStrategy()
  86.             );
  87.         } else {
  88.             throw new ServiceException(ResponseEnum.E30001);
  89.         }
  90.     }
  91. }
复制代码
3.2.4 寻找ExcelDataService的实现类

选择自己工程下的实现类, 写3.2.3的具体业务
   如遇问题请提出

    实现类重写saveExcelData()方法, 这里就枚举其中的两种利用方式, 业务代码跳过
  1.     /**
  2.      * 保存导入的数据
  3.      * 分批进入 防止数据过大 - 栈溢出
  4.      *
  5.      * @param t 保存的数据类型
  6.      */
  7. //    @Transactional
  8.     @Override
  9.     public <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {
  10.         MemberResponseVo user = AuthServerConstant.loginUser.get();
  11.         int companyId = user.getCompanyId();
  12.         log.info("需要保存的数据: {}", JSON.toJSONString(t));
  13.         //获取当前xid号-批次号(数据安全)
  14.         String eXid = String.valueOf(ThreadLocalUtils.get().get("e_xid"));
  15.         log.info("业务中: e_xid号=========================> {}", eXid);
  16.         //功能类型 1配件(库存) 2供应商 3客户(假)资料
  17.         if (Objects.equals(functionType, 1)) {//1
  18.             return upload111Data(t, companyId, eXid);
  19.         } else if (Objects.equals(functionType, 2)) {//2
  20.             return upload222Data(t, companyId, eXid);
  21.         } else if (Objects.equals(functionType, 3)) {//3
  22.             return upload333Data(t, companyId, eXid);
  23.         } else {
  24.             throw new ServiceException(ResponseEnum.E30001);
  25.         }
  26.     }
  27.     /**
  28.      * 1. 上传配件数据
  29.      *
  30.      * @param t         传入数据
  31.      * @param companyId 公司id
  32.      * @param eXid      eXid
  33.      * @return ResultResponse对象
  34.      */
  35.     private <T> ResultResponse uploadPartsData(List<T> t, Integer companyId, String eXid) {
  36.         List<***> a1List;
  37.         try {
  38.             a1ListList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<***>>() {
  39.             });
  40.         } catch (Exception e) {
  41.             e.printStackTrace();
  42.             return ResultResponse.error("类型不匹配,请先检查金额字段,必须是纯数字的整数或小数哟~");
  43.         }
  44.         if (CollectionUtils.isEmpty(a1List)) {
  45.             return ResultResponse.ok("无数据需要导入~");
  46.         }
  47.         //数据处理
  48.         a1List.forEach(a1DTO -> {
  49.             //数据校验
  50.             ResultResponse response = ValidatorUtils.validate(a1DTO, Integer.valueOf(a1.getSheetDataNo()), a1.getSheetName());
  51.             if (!ResponseHelper.judgeResp(response)) {
  52.                 //执行回滚操作
  53.                 if (!ResponseHelper.judgeResp(productFeignService.rollBackPartsData(eXid))) {
  54.                     log.error("======> 数据eXid: {} 回滚失败了 ", eXid);
  55.                 }
  56.                 throw new IllegalArgumentException(response.getMsg());
  57.             }
  58.             a1.setSourceType(1);
  59.             a1.setXid(eXid);
  60.             //根据品类名称 转换成品类id
  61.             a1.setTypeId(changeTypeNameToId(a1.getSheetName()));
  62.         });
  63.         //远程调用 即使出现问题也不会滚 业务内直接删除数据重新传递
  64.         return ***.saveBatch(a1List);
  65.     }
复制代码
  客户导入, 这个保留业务代码 方便查察具体利用方式
  1.    /**
  2.      * 3. 上传客户临时数据
  3.      *
  4.      * @param t         传入数据
  5.      * @param companyId 公司id
  6.      * @param eXid      eXid
  7.      * @return ResultResponse对象
  8.      */
  9.     private <T> ResultResponse uploadUserTempData(List<T> t, Integer companyId, String eXid) {
  10.         List<UserTempDTO> userTempList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<UserTempDTO>>() {
  11.         });
  12.         if (CollectionUtils.isEmpty(userTempList)) {
  13.             return ResultResponse.ok("无数据需要导入呦~");
  14.         }
  15.         List<AxUserTemp> axUserTempList = userTempList.stream().map(userTempDTO -> {
  16.             //数据校验(包含回滚)
  17.             ResultResponse response = ValidatorUtils.validate(userTempDTO, Integer.valueOf(userTempDTO.getSheetDataNo()));
  18.             if (!ResponseHelper.judgeResp(response)) {
  19.                 rollBackAxUserTemp(eXid);
  20.                 throw new IllegalArgumentException(response.getMsg());
  21.             }
  22.             AxUserTemp axUserTemp = new AxUserTemp();
  23.             BeanUtils.copyProperties(userTempDTO, axUserTemp);
  24.             axUserTemp.setId(IdGenerater.getInstance().nextId())
  25.                     .setUserRole(UserRoleEnum.CONSUMER.getCode()).setCompanyId(companyId)
  26.                     .setCreateTime(DateUtils.getNowDate()).setDelFlag(1).setXid(eXid);
  27.             return axUserTemp;
  28.         }).collect(Collectors.toList());
  29.         try {
  30.             if (!SqlHelper.retBool(axUserTempMapper.insertBatchSomeColumn(axUserTempList))) {
  31.                 rollBackAxUserTemp(eXid);
  32.                 throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入失败", axUserTempList.size()));
  33.             }
  34.         } catch (DuplicateKeyException e) {
  35.             e.printStackTrace();
  36.             rollBackAxUserTemp(eXid);
  37.             throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据重复,请检查(可能重复提交)", axUserTempList.size()));
  38.         } catch (Exception e) {
  39.             e.printStackTrace();
  40.             rollBackAxUserTemp(eXid);
  41.             throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入异常", axUserTempList.size()));
  42.         }
  43.         return ResultResponse.ok();
  44.     }
  45.    
复制代码
  其中rollbackAxUserTemp()方法如下, 手动提交事件
第一步: 注入事件管理器
  1.         /**
  2.      * 事务管理器
  3.      */
  4.     private final PlatformTransactionManager platformTransactionManager;
  5.     /**
  6.      * 事务的一些基础信息,如超时时间、隔离级别、传播属性等
  7.      */
  8.     private final TransactionDefinition transactionDefinition;
复制代码
  第二步: 根据xid号进行删除数据代表回滚, 添加代码 (其中可以添加一些参数 我这直接默认了)
  1. /**
  2.      * 回滚临时用户数据(调用-事务不看结果直接提交)
  3.      *
  4.      * @param eXid xid号
  5.      */
  6.     private void rollBackAxUserTemp(String eXid) {
  7.         TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);//TransactionStatus : 事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚
  8.         try {
  9.             axUserTempMapper.delete(Wrappers.<AxUserTemp>lambdaQuery().eq(AxUserTemp::getXid, eXid));
  10.             platformTransactionManager.commit(transaction);
  11.         }  catch (Exception e) {
  12.             // 回滚事务
  13.             platformTransactionManager.rollback(transaction);
  14.             throw e;
  15.         }
  16.     }
复制代码
3.3 程序测试实行结果及报错解决

3.3.1 实行结果

   前端接入, 可以根据上面testGroup里面html的进行调整
后端部署, 测试, 效果如下

  3.3.2 报错解决

   emm, 代码太长了, 遇到,想用的话批评或私信吧, 遇到的问题太多了, 挑几个重点的
  3.3.2_1 CROS跨域问题



  • 生产环境跨域, 署理一下,设置nginx
  • 开发环境: 当地开跨域只能解决其中一种问题, 下个插件cros就行了 , 有更好的办法(后端)欢迎批评哈~
3.3.2_2 excel表格导出是空

   去掉@Accessors(chain = true)即可
  3.3.2_3 导入dto中有list报错

   利用注解 @ExcelProperty(value = “”,converter = MyConverter.class)
试一下, 欠好用批评区发一下
  3.3.2_4 导出模板/sheet的名字不正确

   根本是前端的问题了, 按照html里去改即可
  3.3.2_5 待续未完…

   想不起来还遇到哪些问题了, 业务层面的不包含, 多线程测试也正常, 等遇到问题在调整本文
如遇到部门类没有, 可根据上下文行为自行更改或批评区指出
渐渐在这里添加
  4. 文章的总结与预告

4.1 本文总结

easyExcel实现具体操作, 遇到问题请看 3.3

4.2 下文预告

暂无


@author: pingzhuyan
@description: ok
@year: 2024


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

© 2001-2025 Discuz! Team. Powered by Discuz! X3.5

GMT+8, 2025-7-12 23:27 , Processed in 0.080473 second(s), 29 queries 手机版|qidao123.com技术社区-IT企服评测▪应用市场 ( 浙ICP备20004199 )|网站地图

快速回复 返回顶部 返回列表