当前位置: 首页 > news >正文

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

全文目录,一步到位

  • 1.前言简介
    • 1.1 链接传送门
      • 1.1.1 easyExcel传送门
  • 2. Excel表格导入过程
    • 2.1 easyExcel的使用准备工作
      • 2.1.1 导入maven依赖
      • 2.1.2 建立一个util包
      • 2.1.3 ExcelUtils统一功能封装(单/多sheet导入)
      • 2.1.4 ExcelDataListener数据监听器
      • 2.1.5 ResponseHelper响应值处理
      • 2.1.6 MyConverter类-自定义转换器
      • 2.1.7 ExcelDataService
      • 2.1.8 ExcelReqDTO 统一请求dto
      • 2.1.9 上传文件校验
      • 2.1.10 最后写个readme.md(说明使用方式)
    • 2.2 easyExcel工具包(全)使用方式
      • 2.2.1 UserExcelDTO 生成用户excel数据
      • 2.2.2 ExcelDataServiceImpl实现类(工程一)
      • 2.2.3 upload.html测试页面
  • 3.业务实战方式与效果(`可跳过2.2`)核心
    • 3.1 业务工具类
      • 3.1.1 ThreadLocalUtils工具类(批次号)
      • 3.1.2 自定义字段校验(注解)
        • -> 3.1.2_1 创建校验注解`@DataCheck`
        • -> 3.1.2_2 注解实现类ValidatorUtils(校验逻辑)
    • 3.2 工程内业务使用
      • 3.2.0 创建上传或下载对象dto
      • 3.2.1 创建controller
      • 3.2.2 接口SystemExcelService
      • 3.2.3 实现类SystemExcelServiceImpl(需根业务自行调整)
      • 3.2.4 寻找ExcelDataService的实现类
    • 3.3 程序测试执行结果及报错解决
      • 3.3.1 执行结果
      • 3.3.2 报错解决
        • 3.3.2_1 CROS跨域问题
        • 3.3.2_2 excel表格导出是空
        • 3.3.2_3 导入dto中有list报错
        • 3.3.2_4 导出模板/sheet的名字不正确
        • 3.3.2_5 待续未完...
  • 4. 文章的总结与预告
    • 4.1 本文总结
    • 4.2 下文预告


1.前言简介


ps: 如您有更好的方案或发现错误,请不吝赐教,感激不尽啦~~~

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

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

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>

<!-- easyExcel --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${alibaba.easyexcel.version}</version></dependency>

2.1.2 建立一个util包

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

  • realDto 里面就是具体导入业务dto
  • testGroup是自行测试代码
  • 其他类均为核心逻辑
    - readme.md 是使用说明, 防止后面人不知道如何使用

下面从2.1.3开始
在这里插入图片描述

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

跳转链接: 解释 @Accessors(chain = true) 与 easyExcel不兼容

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.rmi.ServerException;
import java.util.List;/*** Excel相关操作(简易)* 文章一: 解释 @Accessors(chain = true) 与 easyExcel不兼容* -> https://blog.csdn.net/qq_36268103/article/details/134954322** @author pzy* @version 1.1.0* @description ok*/
@Slf4j
public class ExcelUtils {/*** 方法1.1: 读取excel(单sheet)** @param inputStream 输入流* @param dataClass   任意类型* @param listener    监听* @param sheetNo     sheet编号* @param <T>         传入类型*/public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, int sheetNo) {try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();// 读取一个sheetexcelReader.read(readSheet);}}/*** 方法2.1: 读取excel(多sheet)** @param inputStream 输入流* @param dataClass   任意类型* @param listener    监听* @param sheetNoList sheet编号* @param <T>         传入类型*/public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, List<Integer> sheetNoList) {try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {List<ReadSheet> readSheetList = Lists.newArrayList();sheetNoList.forEach(sheetNo -> {// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();readSheetList.add(readSheet);});// 读取一个sheetexcelReader.read(readSheetList);}}/*** 单sheet excel下载** @param httpServletResponse 响应对象* @param fileName            excel文件名字* @param dataClass           class类型(转换)* @param sheetName           sheet位置1的名字* @param dataList            传入的数据* @param writeHandlers       写处理器们 可变参数 (样式)* @param <T>                 泛型*/public static <T> void easyDownload(HttpServletResponse httpServletResponse,String fileName,Class<T> dataClass,String sheetName,List<T> dataList,WriteHandler... writeHandlers) throws IOException {//对响应值进行处理getExcelServletResponse(httpServletResponse, fileName);ExcelWriterSheetBuilder builder =EasyExcel.write(httpServletResponse.getOutputStream(), dataClass).sheet(sheetName);
//
//        builder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
//                .registerWriteHandler(ExcelStyleTool.getStyleStrategy());/*样式处理器*/if (writeHandlers.length > 0) {for (WriteHandler writeHandler : writeHandlers) {builder.registerWriteHandler(writeHandler);}}builder.doWrite(dataList);}/*** 复杂 excel下载* 1. 多个sheet* 2. 多个处理器** @param httpServletResponse 响应对象* @param fileName            excel文件名字* @param dataClass           class类型(转换)* @param sheetNameList       多sheet的名字数据* @param sheetDataList        多sheet的实际数据* @param writeHandlers       写处理器们 可变参数 (样式)* @param <T>                 泛型*/public static <T> void complexDownload(HttpServletResponse httpServletResponse,String fileName,Class<T> dataClass,List<String> sheetNameList,List<List<T>> sheetDataList,WriteHandler... writeHandlers) throws IOException {if (sheetNameList.size() != sheetDataList.size()) {throw new ServerException("抱歉,名字与列表长度不符~");}//对响应值进行处理getExcelServletResponse(httpServletResponse, fileName);try (ExcelWriter excelWriter = EasyExcel.write(httpServletResponse.getOutputStream()).build()) {// 去调用写入, 这里最终会写到多个sheet里面for (int i = 0; i < sheetNameList.size(); i++) {ExcelWriterSheetBuilder builder = EasyExcel.writerSheet(i, sheetNameList.get(i)).head(dataClass);if (writeHandlers.length > 0) {for (WriteHandler writeHandler : writeHandlers) {builder.registerWriteHandler(writeHandler);}}WriteSheet writeSheet = builder.build();excelWriter.write(sheetDataList.get(i), writeSheet);}}}/*** 获取excel的响应对象** @param httpServletResponse response* @param fileName            文件名* @throws UnsupportedEncodingException 不支持编码异常*/private static void getExcelServletResponse(HttpServletResponse httpServletResponse, String fileName) throws UnsupportedEncodingException {// 设置URLEncoder.encode可以防止中文乱码,和easyexcel没有关系fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");httpServletResponse.setCharacterEncoding("utf-8");httpServletResponse.addHeader("Access-Control-Expose-Headers", "Content-Disposition");httpServletResponse.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");}

2.1.4 ExcelDataListener数据监听器

读取excel表格数据 一条一条读取出来
ps: ResultResponse就是返回值封装类 随便都行200或500

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** 官方提供转换listener* ps: 有个很重要的点 ExcelDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去** @author pzy* @version 0.1.0* @description ok*/
//@Component
@Slf4j
public class ExcelDataListener<T> implements ReadListener<T> {/*** 每隔5条存储数据库,实际使用中可以300条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 300;private final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();/*** 缓存的数据*/
//    private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);private final List<T> cachedDataList = Lists.newCopyOnWriteArrayList();/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private final ExcelDataService excelDataService;/*** 自行定义的功能类型 1配件(库存) 2供应商 3客户(假)资料*/private final Integer functionType;/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来*/public ExcelDataListener(ExcelDataService excelDataService1, Integer functionType) {this.excelDataService = excelDataService1;this.functionType = functionType;}/*** 这个每一条数据解析都会来调用** @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}*/@Overridepublic void invoke(T data, AnalysisContext context) {//        String threadName = Thread.currentThread().getName();
//        System.out.println(threadName);log.info("解析到一条数据:{}", JSON.toJSONString(data));String sheetName = context.readSheetHolder().getSheetName();//ps: 慢换LongAdder
//        if (!map.containsKey(sheetName)) {
//            map.put(sheetName, new AtomicInteger(0));
//        } else {
//            map.put(sheetName, new AtomicInteger(map.get(sheetName).incrementAndGet()));
//        }int sheetDataCounts = map.computeIfAbsent(sheetName, k -> new AtomicInteger(0)).incrementAndGet();log.info("当前sheet的数据是: {}, 数量是第: {}个", sheetName, sheetDataCounts);if (data != null) {JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(data));jsonObject.put("sheetName", sheetName);jsonObject.put("sheetDataNo", sheetDataCounts);//放入sheet数据编号(如果仅一个sheetcachedDataList.add((T) jsonObject);//类型明确(不增加通配符边界了 增加使用难度)}// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {saveData();// 存储完成清理 list
//            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);cachedDataList.clear();//这块需要测试看看效果}}/*** 所有数据解析完成了 都会来调用*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库log.info("{}条数据,开始存储数据库!", cachedDataList.size());saveData();cachedDataList.clear();log.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", cachedDataList.size());
//        excelDataService.saveUser((T) new SystemUser());ResultResponse response = excelDataService.saveExcelData(functionType, cachedDataList);if (ResponseHelper.judgeResp(response)) {log.info("存储数据库成功!");}}}

2.1.5 ResponseHelper响应值处理

ResultResponse返回值封装类 任意即可

import com.alibaba.fastjson.TypeReference;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;import javax.servlet.http.HttpServletRequest;
import java.util.Objects;/*** 响应 工具类** @author pzy* @version 0.1.0* @description ok*/
public class ResponseHelper<T> {/*** 响应成功失败校验器* 疑似存在bug(未进行测试)*/@Deprecatedpublic T retBool(ResultResponse response) {/*1. 如果接口返回值返回的不是200 抛出异常*/if (response.getCode() != 200) {throw new ServiceException(response.getCode(), response.getMsg());}return response.getData(new TypeReference<T>() {});}/*** 请求响应值校验器(ResultResponse对象)*/public static void retBoolResp(ResultResponse response) {if (response == null) {throw new NullPointerException("服务响应异常!");}/*1. 如果接口返回值返回的不是200 抛出异常*/if (!Objects.equals(response.getCode(), 200)) {throw new ServiceException(response.getCode(), response.getMsg());}}/*** 判定响应返回值* <p>* true 表示200 服务通畅* false 表示500 服务不通畅(*/public static boolean judgeResp(ResultResponse response) {// 1. 如果接口返回值返回的不是200 返回falsereturn response != null && Objects.equals(response.getCode(), 200);}/*** 通过上下文对象获取请求头的token值* RequestHelper.getHeaderToken()*/@Deprecatedpublic static String getHeaderToken() {//请求上下文对象获取 线程RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();assert requestAttributes != null;HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);assert request != null;return request.getHeader("token");}}

2.1.6 MyConverter类-自定义转换器

@ExcelProperty(converter = MyConverter.class) 使用自定义转换器 针对list等类型进行操作

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;/*** list类型使用 自定义转换器(补充功能 beta版)* @author pzy* @version 0.1.0* @description ok*/
public class MyConverter implements Converter<List> {@Overridepublic Class<?> supportJavaTypeKey() {return List.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}/*** 读(导入)数据时调用*/@Overridepublic List convertToJavaData(ReadConverterContext<?> context) {//当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用//context.getReadCellData().getStringValue()会获取excel表格中该字段对应的String数据//这里可以对数据进行额外的加工处理String stringValue = context.getReadCellData().getStringValue();//将数据转换为List类型然后返回给实体类对象DTOreturn Collections.singletonList(stringValue);}/*** 写(导出)数据时调用*/@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<List> context) {//当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用//context.getValue()会获取对应字段的List类型数据//这里是将List<String>转换为String类型数据,根据自己的数据进行处理StringJoiner joiner = new StringJoiner(",");for (Object data : context.getValue()) {joiner.add((CharSequence) data);}//然后将转换后的String类型数据写入到Excel表格对应字段当中return new WriteCellData<>(joiner.toString());}
}

2.1.7 ExcelDataService

数据处理行为接口(多工程拓展)

import java.util.List;/*** 数据处理service** @author pzy* @version 0.1.0* @description ok*/
@FunctionalInterface
public interface ExcelDataService {/*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/<T> ResultResponse saveExcelData(Integer functionType, List<T> t);
}

2.1.8 ExcelReqDTO 统一请求dto

业务需要, 生成的文件名 sheet的名称 功能类型等信息
其中Lists.newArrayList() 没有的直接换成new ArrayList()即可 效果相同

import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;import java.util.List;/*** excel统一请求dto* <p>* 传入需要的参数, 生成对应的excel表格** @author pzy* @version 0.1.0* @description ok*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ExcelReqDTO {/*** 功能类型 例: 1用户 2其他业务*/private Integer functionType;/*** excel类型 1单sheet 2多sheet*/private Integer excelType;/*** 文件名称*/private String fileName;/*** sheet名称*/private String sheetName;/*** sheet名称组*/private List<String> sheetNames = Lists.newArrayList();
}

2.1.9 上传文件校验

文件大小校验可在配置文件内添加, 效果更好

import lombok.extern.slf4j.Slf4j;import org.springframework.web.multipart.MultipartFile;import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Locale;/*** 文件上传校验的公共方法* 严格校验** @author pzy* @version 1.0.0*/
@Slf4j
public class UploadCheckUtils {//20MBprivate static final Integer maxUpLoadSize = 20;/*** 只支持文件格式*/public static final String[] YES_FILE_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv"};/*** 全部文件(普通文件,图片, 视频,音频)后缀 支持的类型*/private static final String[] FILE_SUFFIX_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv",".jpg", ".jpeg", ".png", ".mp4", ".avi", ".mp3"};/*** 文件名字 需要排除的字符* 废弃:  "(", ")","",".", "——", "_","-"*/private static final String[] FILE_NAME_EXCLUDE = {"`", "!", "@", "#", "$", "%", "^", "&", "*", "=", "+","~", "·", "!", "¥", "……", "(", ")","?", ",", "<", ">", ":", ";", "[", "]", "{", "}", "/", "\\", "|","?", ",", "。", "《", "》", ":", ";", "【", "】", "、"};/*** 多文件上传* 校验+大图片压缩*/public MultipartFile[] uploadVerify(MultipartFile[] multipartFile) {/*校验1: 没有文件时,报错提示*/if (multipartFile == null || multipartFile.length <= 0) {throw new ServiceException(500, "上传文件不能为空");}/*总文件大于: ?Mb时, 拦截*/long sumSize = 0;for (MultipartFile file : multipartFile) {sumSize += file.getSize();}// 总文件超过100mb 直接拦截 beta功能 不正式使用if (sumSize > (100 * 1024 * 1024L)) {log.warn("(上传总空间)大于100MB, 文件上传过大!");
//            throw new ThirdServiceException(ResponseEnum.T160007, "(上传总空间)100");}/*校验2: 上传文件的长度小于等于1 就一个直接校验*/if (multipartFile.length <= 1) {MultipartFile[] files = new MultipartFile[1];files[0] = uploadVerify(multipartFile[0]);return files;}/*校验3: 多个文件直接校验 需要更换新的file */for (int i = 0; i < multipartFile.length; i++) {multipartFile[i] = uploadVerify(multipartFile[i]);}return multipartFile;}/*** 上传文件校验大小、名字、后缀** @param multipartFile multipartFile*/public static MultipartFile uploadVerify(MultipartFile multipartFile) {// 校验文件是否为空if (multipartFile == null) {throw new ServiceException(500, "上传文件不能为空呦~");}/*大小校验*/log.info("上传文件的大小的是: {} MB", new BigDecimal(multipartFile.getSize()).divide(BigDecimal.valueOf(1024 * 1024), CommonConstants.FINANCE_SCALE_LENGTH, RoundingMode.HALF_UP));log.info("上传限制的文件大小是: {} MB", maxUpLoadSize);if (multipartFile.getSize() > (maxUpLoadSize * 1024 * 1024L)) {throw new ServiceException(500, String.format("上传文件不得大于 %s MB", maxUpLoadSize));}// 校验文件名字String originalFilename = multipartFile.getOriginalFilename();if (originalFilename == null) {throw new ServiceException(500, "上传文件名字不能为空呦~");}for (String realKey : FILE_NAME_EXCLUDE) {if (originalFilename.contains(realKey)) {throw new ServiceException(500, String.format("文件名字不允许出现 '%s' 关键字呦~", realKey));}}// 校验文件后缀if (!originalFilename.contains(".")) {throw new ServiceException(500, "文件不能没有后缀呦~");}String suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));/*校验: 文件格式是否符合要求*/if (!Arrays.asList(FILE_SUFFIX_SUPPORT).contains(suffix.toLowerCase(Locale.ROOT))) {//throw new RuntimeException("文件格式' " + realFormat + " '不支持,请更换后重试!");throw new ServiceException(500, "文件格式不支持呦~");}return multipartFile;}
}

2.1.10 最后写个readme.md(说明使用方式)

这里写不写都行, 如有错误,请指出,谢谢啦~

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

2.2 easyExcel工具包(全)使用方式

testGroup组演示

2.2.1 UserExcelDTO 生成用户excel数据

跟随业务随意, 用啥字段就加啥, @ExcelIgnore //表示忽略此字段

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** excel表格示例demo* ps: 不能用accessors** @author pzy* @version 0.1.0* @description ok*/
@ContentRowHeight(20)
@HeadRowHeight(30)
@ColumnWidth(25)
@NoArgsConstructor
@AllArgsConstructor
//@Accessors(chain = true)
@Data
public class UserExcelDTO {/*** 用户ID*/
//    @ExcelIgnore //忽略@ColumnWidth(20)@ExcelProperty(value = "用户编号")private Long userId;@ColumnWidth(50)@ExcelProperty(value = "真实姓名")private String realName;@ColumnWidth(50)@ExcelProperty(value = "手机号")private String phone;/*** 用户邮箱*/@ColumnWidth(50)//@ExcelProperty(value = "邮箱",converter = MyConverter.class)@ExcelProperty(value = "邮箱")private String email;}

2.2.2 ExcelDataServiceImpl实现类(工程一)

模拟一下数据库行为操作, 后面有实际操作呦~


import java.util.List;/*** 实现类 demo实现方式 (此处不可注入bean) 示例文档** @author pzy* @version 0.1.0* @description ok*/
//@Slf4j
//@RequiredArgsConstructor
//@Service
public class ExcelDataServiceImpl implements ExcelDataService {/*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/@Overridepublic <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {//测试演示(添加数据库)return ResultResponse.booleanToResponse(true);}
//
//    /**
//     * 获取数据并导出到excel表格中
//     *
//     * @param t 传入对象
//     * @return t类型集合
//     */
//    @Override
//    public <T> List<T> getExcelData(T t) {
//        //测试演示
//        return null;
//    }
}

2.2.3 upload.html测试页面

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

解决方案一: 后端放行一下, 测试后关闭即可
解决方案二: 让前端直接连, 用前端写过的页面
等等

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>EasyExcel</title>
</head><body>
<div class="app"><input type="file" id="fileInput" accept=".xlsx, .xls, .csv"><button onclick="upload()">单sheet上传</button><br><br><input type="file" id="fileInput1" accept=".xlsx, .xls, .csv"><button onclick="upload1()">多sheet上传</button>
</div>
<br>
<div><button onclick="download()">单sheet导出</button>&nbsp;<button onclick="download1()">多sheet导出</button>
</div>
</body><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>const upload = () => {// 获取文件输入元素const fileInput = document.getElementById('fileInput')// 获取选中的文件const file = fileInput.files[0]if (!file) {alert('请选择一个文件')return}// 创建 FormData 对象const formData = new FormData()// 将文件添加到 FormData 对象formData.append('file', file)// 发送 POST 请求到后端axios.post('http://localhost:8001/system/excel/upload?functionType=1', formData, {headers: {'Content-Type': 'multipart/form-data' // 设置正确的 Content-Type}}).then(response => {alert('文件上传成功')console.log('文件上传成功:', response.data)}).catch(error => {console.error('文件上传失败:', error)});}const upload1 = () => {// 获取文件输入元素const fileInput = document.getElementById('fileInput1')// 获取选中的文件const file = fileInput.files[0]if (!file) {alert('请选择一个文件')return}// 创建 FormData 对象const formData = new FormData()// 将文件添加到 FormData 对象formData.append('file', file)// 发送 POST 请求到后端axios.post('http://localhost:8001/system/excel/upload1?functionType=2', formData, {headers: {'Content-Type': 'multipart/form-data', // 设置正确的 Content-Type'token': ''}}).then(response => {alert('文件上传成功')console.log('文件上传成功:', response.data)}).catch(error => {console.error('文件上传失败:', error)});}const headers = {// 'Content-Type': 'application/json', // 设置请求头部的Content-Type为application/json// token: '', // 设置请求头部的Authorization为Bearer your_token// 'Token4545': '1', // 设置请求头部的Authorization为Bearer your_token// 'responseType': 'blob', // 设置响应类型为blob(二进制大对象)};const download = () => {const url = 'http://192.168.1.254:8001/system/excel/download?fileName=单S文件&functionType=1'axios.get(url, {responseType: 'blob'}).then(response => {// 从Content-Disposition头部中获取文件名const contentDisposition = response.headers['content-disposition']console.log(response)console.log(contentDisposition)const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)console.log(matches)let filename = 'downloaded.xlsx'if (matches != null && matches[2] != null) {console.log(matches[2])// 解码RFC 5987编码的文件名filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))} else {// 如果没有filename*,尝试使用filenameconst filenameMatch = /filename="(.*)"/.exec(contentDisposition);console.log(71)if (filenameMatch != null && filenameMatch[1] != null) {filename = filenameMatch[1]console.log(74)}}// 创建一个a标签用于下载const a = document.createElement('a')// 创建一个URL对象,指向下载的文件const url = window.URL.createObjectURL(new Blob([response.data]))a.href = urla.download = filename // 设置文件名document.body.appendChild(a)a.click()document.body.removeChild(a)window.URL.revokeObjectURL(url)}).catch(error => {console.error('下载文件时出错:', error)})}const download1 = () => {const url = 'http://192.168.1.254:8001/system/excel/test2'axios.get(url, {responseType: 'blob', // 设置响应类型为blob(二进制大对象)}).then(response => {// 从Content-Disposition头部中获取文件名const contentDisposition = response.headers['content-disposition']console.log(response)console.log(contentDisposition)const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)console.log(matches)let filename = 'downloaded.xlsx'if (matches != null && matches[2] != null) {console.log(matches[2])// 解码RFC 5987编码的文件名filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))} else {// 如果没有filename*,尝试使用filenameconst filenameMatch = /filename="(.*)"/.exec(contentDisposition);console.log(71)if (filenameMatch != null && filenameMatch[1] != null) {filename = filenameMatch[1]console.log(74)}}// 创建一个a标签用于下载const a = document.createElement('a')// 创建一个URL对象,指向下载的文件const url = window.URL.createObjectURL(new Blob([response.data]))a.href = urla.download = filename // 设置文件名document.body.appendChild(a)a.click()document.body.removeChild(a)window.URL.revokeObjectURL(url)}).catch(error => {console.error('下载文件时出错:', error)})}
</script></html>

3.业务实战方式与效果(可跳过2.2)核心

前言: 2.2介绍的是简单的demo, 根据那个进行拓展

业务需求

  1. 客户点击- 生成模板, 生成空的excel模板
  2. 根据说明填写具体信息
  3. 导入后, 如果数据正常,导入成功
  4. 导入异常, 则明确告知数据问题在哪
  5. 本次导入的数据均不生效
  6. 面对多sheet导入异常, 明确指出 sheet名内的第*条数据,什么问题, 其他上同

操作方式:

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

3.1 业务工具类

3.1.1 ThreadLocalUtils工具类(批次号)

写个基础的set和get , 通过当前线程传递xid号,

import java.util.Map;/*** threadLocal使用工具方法* <p>* ps: jdk建议将 ThreadLocal 定义为 private static* 避免: 有弱引用,内存泄漏的问题了** @author pzy* @description TODO beta01测试中* @version 1.0.1*/
public class ThreadLocalUtils {private static final ThreadLocal<Map<String, Object>> mapThreadLocal = new ThreadLocal<>();//获取当前线程的存的变量public static Map<String, Object> get() {return mapThreadLocal.get();}//设置当前线程的存的变量public static void set(Map<String, Object> map) {mapThreadLocal.set(map);}//移除当前线程的存的变量public static void remove() {mapThreadLocal.remove();}
}

3.1.2 自定义字段校验(注解)

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

如有更细致的校验, 请自行添加

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 实体类-数据校验注解* <p>* ps: 第一版* 校验方式* 1. 数据为空* 2. 最大长度* 3. 正则表达式* 4. 报错信息* <p>* 其中功能校验在 ValidatorUtils 中** @author pzy* @version 1.0.1* @description ok*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataCheck {/*** 校验不能为空 true开启  false关闭*/boolean notBank() default false;/*** 长度*/int maxLength() default -1;/*** 正则表达式*/String value() default "";/*** 报错信息*/String message() default "";}
-> 3.1.2_2 注解实现类ValidatorUtils(校验逻辑)

@DataCheck校验逻辑进行支持, 其中异常条数和异常sheet名称(多sheet需要)需要传递
这里先不管这俩参数
方法一: 单sheet
方法二: 多sheet

import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Field;/*** 校验器工具类*/
@Slf4j
public class ValidatorUtils {/*** DataCheck注册-正则校验器1*/@SneakyThrowspublic static ResultResponse validate(Object obj, Integer errorCounts) {return validate(obj, errorCounts, null);}/*** DataCheck注册-正则校验器2*/@SneakyThrowspublic static ResultResponse validate(Object obj, Integer errorCounts, String sheetName) {Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(DataCheck.class)) {DataCheck annotation = field.getAnnotation(DataCheck.class);field.setAccessible(true);Object value = field.get(obj);//实体类参数int maxLength = annotation.maxLength(); //长度String message = "";if (StringUtils.isNotBlank(sheetName)) {message = String.format("可能是: 品类: %s ,第 %s 条,要求: %s", sheetName, errorCounts, annotation.message()); //报错信息} else {message = String.format("可能是: 第 %s 条,要求: %s", errorCounts, annotation.message()); //报错信息}String matchValue = annotation.value();//正则表达式/*校验1: 开启校验 且参数是空的 */if (annotation.notBank() && (value == null || value == "")) {log.warn("Field :[" + field.getName() + "] is null");log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
//                    throw new IllegalArgumentException("数据为空呦, " + message);return ResultResponse.error("数据为空呦, " + message);}/*校验2: 长度字段大于0 并且长度大于*/if (maxLength > 0) {if (maxLength < String.valueOf(value).length()) {log.warn("Field :[" + field.getName() + " ] is out of range");log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
//                        throw new IllegalArgumentException("数据超范围了呦, " + message);return ResultResponse.error("数据超范围了呦, " + message);}}/*校验3: 正则不匹配 则刨除异常*/if (StringUtils.isNotBlank(matchValue) && value != null && !value.toString().matches(matchValue)) {log.warn("Field :[" + field.getName() + "] is not match");log.error("校验出异常的数据是:=====>  {}", JSON.toJSONString(obj));
//                    throw new IllegalArgumentException("数据格式不对呦, " + message);return ResultResponse.error("数据格式不对呦, " + message);}}}return ResultResponse.ok();}}

3.2 工程内业务使用

3.2.0 创建上传或下载对象dto

添加校验注解 excel注册等, 不可使用@Accessors注解

/*** 临时客户dto** @author pzy* @version 0.1.0* @description ok*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserTempDTO {@DataCheck(notBank = true, maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "(非空)用户姓名支持中文,英文,数字,'-' 和'_', 长度255位")@ExcelProperty(value = "真实姓名")private String realname;@DataCheck(maxLength = 2, message = "性别请填写: 男,女,未知")@ExcelProperty(value = "性别")private String gender;@DataCheck(notBank = true,maxLength = 255, value = "0?(13|14|15|18|17)[0-9]{9}", message = "(非空)手机号需纯数字且长度11位")@ExcelProperty(value = "电话号")private String phone;//    @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "地址信息名称支持中文,英文,数字,'-' 和'_', 长度255位")@DataCheck(maxLength = 255, message = "地址信息名称长度255位")@ExcelProperty(value = "地址信息")private String familyAddr;//    @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "头像链接地址,长度255位")@DataCheck(maxLength = 255, message = "头像链接地址,长度255位")@ExcelProperty(value = "头像")private String avatarUrl;//---------------------------------------->@ExcelIgnore@ExcelProperty(value = "备用电话号")private String sparePhone;@ExcelIgnore@ExcelProperty(value = "昵称")private String nickname;@ExcelIgnore@ApiModelProperty(value = "excel的sheet名称")private String sheetName;@ExcelIgnore@ApiModelProperty(value = "excel的sheet名称对应行号,用于报错行数")private String sheetDataNo;@ExcelIgnore@ApiModelProperty(value = "xid号")private String xid;}

测试校验是否生效

    public static void main(String[] args) {UserTempDTO userTempDTO = new UserTempDTO();userTempDTO.setRealname("");userTempDTO.setGender("男");userTempDTO.setPhone("14788888888");userTempDTO.setFamilyAddr("");userTempDTO.setAvatarUrl("");ValidatorUtils.validate(userTempDTO,10);}

3.2.1 创建controller

业务的入口

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/excel/test")
public class SystemExcelController {private final SystemExcelService systemExcelService;@PostMapping("/upload")public ResultResponse upload(MultipartFile file, ExcelReqDTO excelReqDTO) throws IOException {log.info("===> excel文件上传 <===");//文件校验UploadCheckUtils.uploadVerify(file);try {Map<String, Object> map = new HashMap<>();long snowId = IdGenerater.getInstance().nextId();log.info("excel导入e_xid===> {}",snowId);map.put("e_xid", snowId);//存入threadLocalThreadLocalUtils.set(map);systemExcelService.upload(file, excelReqDTO);} finally {ThreadLocalUtils.remove();}return ResultResponse.ok("操作成功");}@GetMapping("/download")public void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) throws IOException {log.info("===> excel文件下载 <===");systemExcelService.download(httpServletResponse, excelReqDTO);}}

3.2.2 接口SystemExcelService

/*** excel表格实现类* @author pzy* @version 0.1.0* @description ok*/
public interface SystemExcelService {/*** 上传excel文件* @param file 文件* @param excelReqDTO 请求参数*/void upload(MultipartFile file, ExcelReqDTO excelReqDTO);void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO);
}

3.2.3 实现类SystemExcelServiceImpl(需根业务自行调整)

这里面就是具体业务了
使用了ExcelUtils方法 实现多/单sheet导入与导出
导入ps: 在使用excelUtils方法时, 需要注入ExcelDataService接口来实现数据库存储操作
导出ps: 查询数据库数据, 处理 传入Lists.newArrayList() 这个位置即可

/*** excel表格实现类** @author pzy* @version 0.1.0* @description ok*/
@Service
@Slf4j
@RequiredArgsConstructor
public class SystemExcelServiceImpl implements SystemExcelService {private final ExcelDataService excelDataService;/*** 上传excel功能文件** @param file        文件* @param excelReqDTO 请求参数*/@SneakyThrows@Overridepublic void upload(MultipartFile file, ExcelReqDTO excelReqDTO) {//功能类型 1 2 3Integer functionType = excelReqDTO.getFunctionType();if (Objects.equals(functionType, 1)) {//多sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType),MathUtils.getIntRangeToList(0, 8));} else if (Objects.equals(functionType, 2)) {////单sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType), 0);} else if (Objects.equals(functionType, 3)) {////单sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType), 0);} else {throw new ServiceException(ResponseEnum.E30001);}}@SneakyThrows@Overridepublic void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) {String fileName = excelReqDTO.getFileName();if (StringUtils.isBlank(fileName) || fileName.length() > 6) {throw new ServiceException("抱歉名称长度需大于0且不能超过6呦~");}//功能类型 1 2 3Integer functionType = excelReqDTO.getFunctionType();if (Objects.equals(functionType, 1)) {//sheet名字List<String> sheetNameList = ?;List<List<*>> sheetDataList = Lists.newArrayList();sheetNameList.forEach(sheetDto->sheetDataList.add(Lists.newArrayList()));ExcelUtils.complexDownload(httpServletResponse, fileName,ShopOfflineListDTO.class, sheetNameList,sheetDataList,new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else if (Objects.equals(functionType, 2)) {////写出excel核心代码ExcelUtils.easyDownload(httpServletResponse,fileName,*.class,"模板1",Lists.newArrayList(),//需要数据就传入 不需要就传递空集合new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else if (Objects.equals(functionType, 3)) {//写出excel核心代码ExcelUtils.easyDownload(httpServletResponse,fileName,*.class,"模板1",Lists.newArrayList(),new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else {throw new ServiceException(ResponseEnum.E30001);}}
}

3.2.4 寻找ExcelDataService的实现类

选择自己工程下的实现类, 写3.2.3的具体业务

如遇问题请提出
在这里插入图片描述

实现类重写saveExcelData()方法, 这里就列举其中的两种使用方式, 业务代码跳过

    /*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/
//    @Transactional@Overridepublic <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {MemberResponseVo user = AuthServerConstant.loginUser.get();int companyId = user.getCompanyId();log.info("需要保存的数据: {}", JSON.toJSONString(t));//获取当前xid号-批次号(数据安全)String eXid = String.valueOf(ThreadLocalUtils.get().get("e_xid"));log.info("业务中: e_xid号=========================> {}", eXid);//功能类型 1配件(库存) 2供应商 3客户(假)资料if (Objects.equals(functionType, 1)) {//1return upload111Data(t, companyId, eXid);} else if (Objects.equals(functionType, 2)) {//2return upload222Data(t, companyId, eXid);} else if (Objects.equals(functionType, 3)) {//3return upload333Data(t, companyId, eXid);} else {throw new ServiceException(ResponseEnum.E30001);}}/*** 1. 上传配件数据** @param t         传入数据* @param companyId 公司id* @param eXid      eXid* @return ResultResponse对象*/private <T> ResultResponse uploadPartsData(List<T> t, Integer companyId, String eXid) {List<***> a1List;try {a1ListList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<***>>() {});} catch (Exception e) {e.printStackTrace();return ResultResponse.error("类型不匹配,请先检查金额字段,必须是纯数字的整数或小数哟~");}if (CollectionUtils.isEmpty(a1List)) {return ResultResponse.ok("无数据需要导入~");}//数据处理a1List.forEach(a1DTO -> {//数据校验ResultResponse response = ValidatorUtils.validate(a1DTO, Integer.valueOf(a1.getSheetDataNo()), a1.getSheetName());if (!ResponseHelper.judgeResp(response)) {//执行回滚操作if (!ResponseHelper.judgeResp(productFeignService.rollBackPartsData(eXid))) {log.error("======> 数据eXid: {} 回滚失败了 ", eXid);}throw new IllegalArgumentException(response.getMsg());}a1.setSourceType(1);a1.setXid(eXid);//根据品类名称 转换成品类ida1.setTypeId(changeTypeNameToId(a1.getSheetName()));});//远程调用 即使出现问题也不会滚 业务内直接删除数据重新传递return ***.saveBatch(a1List);}

客户导入, 这个保留业务代码 方便查看具体使用方式

   /*** 3. 上传客户临时数据** @param t         传入数据* @param companyId 公司id* @param eXid      eXid* @return ResultResponse对象*/private <T> ResultResponse uploadUserTempData(List<T> t, Integer companyId, String eXid) {List<UserTempDTO> userTempList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<UserTempDTO>>() {});if (CollectionUtils.isEmpty(userTempList)) {return ResultResponse.ok("无数据需要导入呦~");}List<AxUserTemp> axUserTempList = userTempList.stream().map(userTempDTO -> {//数据校验(包含回滚)ResultResponse response = ValidatorUtils.validate(userTempDTO, Integer.valueOf(userTempDTO.getSheetDataNo()));if (!ResponseHelper.judgeResp(response)) {rollBackAxUserTemp(eXid);throw new IllegalArgumentException(response.getMsg());}AxUserTemp axUserTemp = new AxUserTemp();BeanUtils.copyProperties(userTempDTO, axUserTemp);axUserTemp.setId(IdGenerater.getInstance().nextId()).setUserRole(UserRoleEnum.CONSUMER.getCode()).setCompanyId(companyId).setCreateTime(DateUtils.getNowDate()).setDelFlag(1).setXid(eXid);return axUserTemp;}).collect(Collectors.toList());try {if (!SqlHelper.retBool(axUserTempMapper.insertBatchSomeColumn(axUserTempList))) {rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入失败", axUserTempList.size()));}} catch (DuplicateKeyException e) {e.printStackTrace();rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据重复,请检查(可能重复提交)", axUserTempList.size()));} catch (Exception e) {e.printStackTrace();rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入异常", axUserTempList.size()));}return ResultResponse.ok();}

其中rollbackAxUserTemp()方法如下, 手动提交事务
第一步: 注入事务管理器

 	/*** 事务管理器*/private final PlatformTransactionManager platformTransactionManager;/*** 事务的一些基础信息,如超时时间、隔离级别、传播属性等*/private final TransactionDefinition transactionDefinition;

第二步: 根据xid号进行删除数据代表回滚, 添加代码 (其中可以添加一些参数 我这直接默认了)

 /*** 回滚临时用户数据(调用-事务不看结果直接提交)** @param eXid xid号*/private void rollBackAxUserTemp(String eXid) {TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);//TransactionStatus : 事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚try {axUserTempMapper.delete(Wrappers.<AxUserTemp>lambdaQuery().eq(AxUserTemp::getXid, eXid));platformTransactionManager.commit(transaction);}  catch (Exception e) {// 回滚事务platformTransactionManager.rollback(transaction);throw e;}}

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

相关文章:

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

全文目录,一步到位 1.前言简介1.1 链接传送门1.1.1 easyExcel传送门 2. Excel表格导入过程2.1 easyExcel的使用准备工作2.1.1 导入maven依赖2.1.2 建立一个util包2.1.3 ExcelUtils统一功能封装(单/多sheet导入)2.1.4 ExcelDataListener数据监听器2.1.5 ResponseHelper响应值处理…...

志愿者小程序源码社区网格志愿者服务小程序php

志愿者服务小程序源码开发方案&#xff1a;开发语言后端php&#xff0c;tp框架&#xff0c;前端是uniapp。 一 志愿者端-小程序&#xff1a; 申请成为志愿者&#xff0c;志愿者组织端进行审核。成为志愿者后&#xff0c;可以报名参加志愿者活动。 志愿者地图&#xff1a;可以…...

HTML实现 扫雷游戏

前言&#xff1a; 游戏起源与发展 扫雷游戏的雏形可追溯到 1973 年的 “方块&#xff08;cube&#xff09;” 游戏&#xff0c;后经改编出现了 “rlogic” 游戏&#xff0c;玩家需为指挥中心探出安全路线避开地雷。在此基础上&#xff0c;开发者汤姆・安德森编写出了扫雷游戏的…...

小白学多线程(持续更新中)

1.JDK中的线程池 JDK中创建线程池有一个最全的构造方法&#xff0c;里面七个参数如上所示。 执行流程分析&#xff1a; 模拟条件&#xff1a;10个核心线程数&#xff0c;200个最大线程数&#xff0c;阻塞队列大小为100。 当有小于十个任务要处理时&#xff0c;因为小于核心线…...

【uni-app多端】修复stmopjs下plus-websocket无心跳的问题

从这篇文章接着向下看&#xff1a; uniapp plus-websocket 和stompjs连接教程 安卓ios手机端有效 - 简书 按照文章的方式&#xff0c;能够实现APP下stmopjs长连接。但是有一个问题&#xff0c;就是会频繁输出 res-创建连接-1- 跟踪连接&#xff0c;会发现连接都会在大约40s后…...

【SLAM文献阅读】基于概率模型的视觉SLAM动态检测与数据关联方法

A dynamic detection and data association method based on probabilistic models for visual SLAM 《基于概率模型的视觉SLAM动态检测与数据关联方法》 2024 摘要&#xff1a; 通常&#xff0c;静态特征采用多视图几何来估计相机姿态和重建环境地图。因此&#xff0c;动态特…...

Linux系统使用valgrind分析C++程序内存资源使用情况

内存占用是我们开发的时候需要重点关注的一个问题&#xff0c;我们可以人工根据代码推理出一个消耗内存较大的函数&#xff0c;也可以推理出大概会消耗多少内存&#xff0c;但是这种方法不仅麻烦&#xff0c;而且得到的只是推理的数据&#xff0c;而不是实际的数据。 我们可以…...

Selenium+Java(19):使用IDEA的Selenium插件辅助超快速编写Pages

前言 或是惊叹于Selenium对于IDEA的支持已经达到了这样的地步,又或是由于这个好用的小工具的入口就在那里,它已经陪伴了我这么久,而我这么久的时间却都没有发现它。在突然发现这个功能的一瞬间,真的是喜悦感爆棚,于是赶快写下了这篇文章。希望可以帮助到其他同样在做UI自动…...

Unity 设计模式-单例模式(Singleton)详解

设计模式 设计模式 是指在软件开发中为解决常见问题而总结出的一套 可复用的解决方案。这些模式是经过长期实践证明有效的 编程经验总结&#xff0c;并可以在不同的项目中复用。设计模式并不是代码片段&#xff0c;而是对常见问题的 抽象解决方案&#xff0c;它提供了代码结构…...

OAuth协议详解

一、基本概念 OAuth&#xff08;Open Authorization&#xff09;是一种授权协议&#xff0c;用于允许第三方应用程序以受信任的方式访问用户的资源&#xff0c;而无需共享用户的身份验证凭据。OAuth协议的核心目标是在保持用户数据安全的前提下&#xff0c;简化用户在不同应用…...

2024收官之战:车展向下,智驾向上

作者 | 德新 编辑 | 王博 广州车展在上周拉开帷幕&#xff0c;在激烈的车市竞争中&#xff0c;可以说没有一届车展比本届更加「 冰火两重天」。 在本届车展前夕&#xff0c;已经传出不少车企集团面临业务整合的消息&#xff0c;一部分品牌缺席了本届车展&#xff0c;而势头强…...

开源项目-如何更好的参与开源项目开发

开源之谜-提升自我核心竞争力 一、寻找适合自己的开源项目二、像坐牢一样闭关修炼三、最后的实践 开源代码对所有人开放&#xff0c;开发者可以基于现有代码进行扩展和创新&#xff0c;而不是从零开始&#xff0c;参与开源项目可以提升自我的技术能力&#xff0c;丰富个人的经历…...

如何重命名 Conda 环境 - 详细教程

如何重命名 Conda 环境 - 详细教程 前言重命名步骤1. 克隆现有环境2. 验证新环境3. 删除旧环境 实例演示注意事项常见问题解答Q1: 为什么 Conda 没有直接的重命名命令&#xff1f;Q2: 重命名过程会影响环境中的包吗&#xff1f;Q3: 如果克隆过程中断&#xff0c;会怎么样&#…...

自动驾驶之激光雷达

这里写目录标题 1 什么是激光雷达2 激光雷达的关键参数3 激光雷达种类4 自动驾驶感知传感器5 激光雷达感知框架5.1 pointcloud_preprocess5.2 pointcloud_map_based_roi5.3 pointcloud_ground_detection5.4 lidar_detection5.5 lidar_detection_filter5.6 lidar_tracking 1 什么…...

Python毕业设计选题:基于python的豆瓣电影数据分析可视化系统-flask+spider

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 系统首页 个人中心 管理员登录界面 管理员功能界面 电影管理 用户管理 系统管理 摘要…...

从 Mac 远程控制 Windows:一站式配置与实践指南20241123

引言&#xff1a;跨平台操作的需求与挑战 随着办公场景的多样化&#xff0c;跨平台操作成为现代开发者和 IT 人员的刚需。从 Mac 系统远程控制 Windows&#xff0c;尤其是在同一局域网下&#xff0c;是一种高效解决方案。不仅能够灵活管理资源&#xff0c;还可以通过命令行简化…...

k8s部署Nginx详细教程

Kubernetes&#xff08;简称k8s&#xff09;是一个开源的容器编排平台&#xff0c;用于自动化部署、扩展和管理容器化应用程序。本文将详细介绍如何使用k8s部署Nginx&#xff0c;包括创建部署配置、创建服务以及如何通过一个命令完成部署和删除。 环境准备 在开始之前&#x…...

PySpark3:pyspark.sql.functions常见的60个函数

目录 一、常见的60个函数 1、col 2、lit 3、sum 4、avg/mean 5、count 6、max 7、min 8、concat 9、substring 10、lower 11、upper 12、trim 13、ltrim 14、rtrim 15、split 16、explode 17、collect_list 18、collect_set 19、asc 20、desc 21、when 2…...

网络爬虫总结与未来方向

通过深入学习和实际操作&#xff0c;网络爬虫技术从基础到进阶得以系统掌握。本节将全面总结关键内容&#xff0c;并结合前沿技术趋势与最新资料&#xff0c;为开发者提供实用性强的深度思考和方案建议。 1. 网络爬虫技术发展趋势 1.1 趋势一&#xff1a;高性能分布式爬虫 随…...

【优先算法】专题——双指针

1.移动零 移动零 题目描述&#xff1a; 思路&#xff1a; 本题我们把数组分块&#xff0c;将非零元素移动到左边&#xff0c;为零元素移动右边。 我们使用双指针算法&#xff08;利用数组下标来充当指针&#xff09; 两个指针的作用&#xff1a; cur&#xff1a;从左往右…...

互联网时代的隐私保护

在这个数字化时代&#xff0c;我们的生活与互联网密不可分。打开手机刷刷朋友圈&#xff0c;浏览一下购物网站&#xff0c;约个网约车&#xff0c;点个外卖&#xff0c;这些看似平常的行为都在默默产生着数据足迹。可就在这不经意间&#xff0c;我们的个人信息正在被收集、分析…...

活着就好20241124

今天是周日&#xff0c;一个同样洋溢着休闲与宁静气息的日子。亲爱的朋友们&#xff0c;大家早上好&#xff01;在经历了一周的忙碌之后&#xff0c;我们终于迎来了这个让人期待已久的休息日。周日&#xff0c;不仅是一个放松身心的绝佳时机&#xff0c;更是我们回归自我、享受…...

镁光MT25QU01GXXX norflash调试笔记

目录 前言一、芯片概述二、数据手册解释1. 数据手册获取2.内容概括 三、几个操作的代码1.复位芯片操作2.读取芯片ID3.擦除芯片扇区4.向芯片存入数据5.读取存储的数据6.其它操作函数 前言 本笔记总结如何使用MCU对nor flash进行数据存储&#xff0c;包括芯片基本介绍&#xff0…...

并行IO接口8255

文章目录 8255A芯片组成外设接口三个端口两组端口关于C口&#xff08;★&#xff09; 内部逻辑CPU接口 8255A的控制字&#xff08;★&#xff09;位控字&#xff08;D70&#xff09;方式选择控制字&#xff08;D71&#xff09; 8255A的工作方式工作方式0&#xff08;基本输入/输…...

[Unity Demo]从零开始制作空洞骑士Hollow Knight第二十集:制作专门渲染HUD的相机HUD Camera和画布HUD Canvas

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作HUD Camera以及让两个相机同时渲染屏幕二、制作HUD Canvas 1.制作法力条Soul Orb引入库2.制作生命条Health读入数据3.制作吉欧统计数Geo Counter4.制作…...

《AI大模型开发笔记》——Prompt提示词最佳实践

为什么需要提示词优化 什么是 Prompt enginnering&#xff1f; 提示词&#xff1a;解释一下什么是大语言模型的Prompt enginnering 图1. 什么是Prompt Enginnering&#xff1f; 通过上面ChatGPT的说明&#xff0c;我们可以看到&#xff0c;prompt工程化就是如何写prompt提示…...

Frontend - 防止多次请求,避免重复请求

目录 一、避免重复执行的多种情况 &#xff08;一&#xff09;根据用途 &#xff08;二&#xff09;根据用户操作 二、具体实现 &#xff08;一&#xff09;“Ajax ”结合disabled (防止多次请求)&#xff0c;避免多次点击重复请求 1. 适用场景 2. 解决办法 3. 示例 &…...

MongoDB 简介

MongoDB 简介 MongoDB 是一种流行的 NoSQL 数据库管理系统,以其灵活的数据模型、高性能和易于扩展的特点而闻名。本文将详细介绍 MongoDB 的基本概念、特点、使用场景以及如何在实践中应用。 1. MongoDB 基本概念 1.1 文档数据库 MongoDB 是一个文档数据库,它使用 JSON 风…...

Ngrok实现内网穿透(Windows)

Ngrok实现内网穿透&#xff08;Windows&#xff09; 什么是内网穿透&#xff0c;内网穿透有什么用 内网穿透&#xff08;NAT traversal&#xff09;是一种技术手段&#xff0c;使得位于内网或防火墙后面的设备能够通过外网访问。例如&#xff0c;如果你的计算机、服务器等设备…...

【Python-办公自动化】实现自动化输出模板表格报告

import pandas as pd import numpy as np# 定义时间范围 date_range = pd.date_range(start=2023-11-01, end=2024-10-31, freq=M...

医学AI公开课·第一期|Machine LearningTransformers in Med AI

小罗碎碎念 从这周开始&#xff0c;我计划每个周末录一个视频&#xff0c;分享一些医学人工智能领域的进展。 作为第一期视频&#xff0c;我打算介绍一下机器学习和Transformer在医学AI领域中的应用。 为了准备这期视频&#xff0c;总共做了24页PPT&#xff08;三部分内容&…...

ESP8266 STA模式TCP服务器 电脑手机网络调试助手

STA模式TCP服务器和手机电脑网络调试助手多连接...

Transformer架构笔记

Attention is All You Need. 3.Model Architecture 3.1 整体架构如图 3.2 Encoder与Decoder Encoder&#xff1a;由 N 6 N6 N6个相同的Block/Layer堆叠而成。每个Block有两个子层sub-layer&#xff1a;多头注意力和MLP&#xff08;FFN&#xff0c;前馈神经网络&#xff09;&…...

初学 flutter 环境变量配置

一、jdk&#xff08;jdk11&#xff09; 1&#xff09;配置环境变量 新增&#xff1a;JAVA_HOMEC:\Program Files\Java\jdk-11 //你的jdk目录 在path新增&#xff1a;%JAVA_HOME%\bin2&#xff09;验证是否配置成功&#xff08;cmd运行命令&#xff09; java java -version …...

【大数据技术基础】 课程 第8章 数据仓库Hive的安装和使用 大数据基础编程、实验和案例教程(第2版)

第8章 数据仓库Hive的安装和使用 8.1 Hive的安装 8.1.1 下载安装文件 访问Hive官网&#xff08;http://www.apache.org/dyn/closer.cgi/hive/&#xff09;下载安装文件apache-hive-3.1.2-bin.tar.gz 下载完安装文件以后&#xff0c;需要对文件进行解压。按照Linux系统使用的…...

Postman之newman

系列文章目录 1.Postman之安装及汉化基本使用介绍 2.Postman之变量操作 3.Postman之数据提取 4.Postman之pm.test断言操作 5.Postman之newman Postman之newman 1.基础环境node安装1.1.配置环境变量1.2.安装newman和html报告组件 2.newman运行 newman可以理解为&#xff0c;没有…...

Lua 实现继承的一种方式

以下代码来自Loxodon Framework&#xff0c;截取自其中的一段 function class(classname, super)local cls {}cls.__classname classnamecls.__class clscls.base function (self)return cls.superendcls.__type 0cls.super supercls.__index clsif super thensetmetat…...

相机网卡开启巨型帧和关闭节能模式方法

2022 年 8 月 2 日 Tank 阅读次数&#xff08;ip/1年&#xff09;: 26,796 win10为例子 首先在开始菜单搜索&#xff1a;网络连接 对想要设置的网络右键&#xff1a;属性 点 配置 高级里面找到这三个选项&#xff0c;参考下图设置&#xff0c;螃蟹网卡建议关掉所有节能有关的…...

如何在 Ubuntu 22.04 上安装带有 Nginx 的 ELK Stack

今天我们来聊聊如何在 Ubuntu 22.04 服务器上安装 ELK Stack&#xff0c;并集成 Nginx 作为 Web 服务器&#xff0c;同时使用 Let’s Encrypt Certbot 进行 SSL 认证。ELK Stack&#xff0c;包括 Elasticsearch、Logstash 和 Kibana&#xff0c;是一套强大的工具&#xff0c;用…...

Android中的依赖注入(DI)框架Hilt

Hilt 是 Android 提供的一种依赖注入&#xff08;DI&#xff09;框架&#xff0c;它基于 Dagger&#xff0c;目的是简化依赖注入的使用&#xff0c;提供更易用的接口和与 Android 生命周期组件的紧密集成。下面是 Hilt 的详细介绍。 为什么选择 Hilt? 依赖注入的优势&#xf…...

笔记记录 k8s操作

docker下载arm架构的镜像 docker pull centos --platform arm64 其中华为鲲鹏、飞腾CPU采用的是ARM架构,龙芯采用的是MIPS架构,而兆芯、海光CPU采用的是X86架构,申威采用的是Alpha架构 docker查看容器的日志文件目录 docker inspect --format={{.LogPath}} containername…...

掌握Go语言中的异常控制:panic、recover和defer的深度解析

掌握Go语言中的异常控制:panic、recover和defer的深度解析 在Go语言的编程世界中,异常处理是一个不可忽视的话题。Go语言提供了panic、recover和defer三个关键字来处理程序中的异常情况。本文将深入探讨这三个关键字的工作原理、使用场景和最佳实践,帮助读者在实际编程中更…...

SpringBoot项目部署到云服务器全流程

文章目录 一、前期准备&#xff08;一&#xff09;云服务器选择&#xff08;二&#xff09;本地环境准备&#xff08;三&#xff09;数据库准备&#xff08;若项目需要&#xff09; 二、服务器配置&#xff08;一&#xff09;获取服务器信息&#xff08;二&#xff09;重置实例…...

对传统加密算法降维打击?!——量子计算

量子计算 声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无…...

什么是Lodash,有什么特点

什么是 Lodash&#xff1f; Lodash 是一个 JavaScript 工具库&#xff0c;提供了一系列实用的函数来处理常见的编程任务&#xff0c;如数组操作、对象操作、字符串处理等。Lodash 由 John-David Dalton 创建&#xff0c;旨在提供一个更全面、更高效的替代方案&#xff0c;以弥…...

Simulink中Model模块的模型保护功能

在开发工作过程中&#xff0c;用户为想要知道供应商的开发能力&#xff0c;想要供应商的模型进行测试。面对如此要求&#xff0c;为了能够尽快拿到定点项目&#xff0c;供应商会选择一小块算法或是模型以黑盒的形式供客户测试。Simulink的Model模块除了具有模块引用的功能之外&…...

【电子通识】LED的一些基础知识

什么是LED LED是被称为"发光二极管"的半导体&#xff0c;名称取至 "Light Emitting Diode" 的首字母。 从爱迪生1879年发明白炽灯后&#xff0c;白炽灯统治了照明一百多年&#xff0c;为世界带来光明。而在21世纪&#xff0c;白炽灯却早已被取代&#xff0…...

React Native 基础

React 的核心概念 定义函数式组件 import组件 要定义一个Cat组件,第一步要使用 import 语句来引入React以及React Native的 Text 组件: import React from react; import { Text } from react-native; 定义函数作为组件 const CatApp = () => {}; 渲染Text组件...

C语言 蓝桥杯某例题解决方案(查找完数)

蓝桥杯原题&#xff1a; 一个数如果恰好等于它的因子之和&#xff0c;这个数就称为“完数”。例如6 1 2 3.编程找出1000以内的所有完数。 这个题没有很大的难点&#xff0c;与我们上一个解决的问题“质因数分解”不同&#xff0c;它不需要判断因数是否是质数&#xff0c;因此…...

【shodan】(三)vnc漏洞利用

shodan基础&#xff08;三&#xff09; 声明&#xff1a;该笔记为up主 泷羽的课程笔记&#xff0c;本节链接指路。 警告&#xff1a;本教程仅作学习用途&#xff0c;若有用于非法行为的&#xff0c;概不负责。 count count命令起到一个统计计数的作用。 用上节的漏洞指纹来试…...