easyexcel的批量导入和导出的实际运用
前端 - vue3
后端 - springboot
数据库-oracle
tip:逐步完善中
tip: button按钮的:loading属性仅是禁止了按钮绑定方法,搭配:disabled才能达到禁用上传按钮的目的
<el-dialog v-model="state.dialogVisible" title="导入" :close-on-click-modal="false" :before-close="handleClose" >
<el-upload
:disabled="state.uploadLoading"
ref="upload"
accept=".xlsx,.xls"
v-model:file-list="state.fileList"
class="upload-demo"
:limit="1"
:auto-upload="false"
:on-change="uploadFileChange"
>
<el-button type="primary" :loading="state.uploadLoading">上传el-button>
<template #tip>
<div class="el-upload__tip">
tip:仅可以上传.xlsx、.xls类型的文件
div>
template>
el-upload>
el-dialog>
const uploadFileChange = async(file: any) => {
state.uploadLoading=true;
// 上传文件
let formData = new FormData();
formData.append("file", file.raw);
const data = {
id:"123",
}
const strJSON = JSON.stringify(data)
let rsp: any = await sysApi.importExcel(formData);
state.uploadLoading=false;
if(rsp.RespCode=="00"){
ElMessage.success('上传成功');
state.dialogVisible=false;
}else{//上传失败
ElMessage.error('上传失败');
}
upload.value!.clearFiles();
};
const handleClose = (done: any) => {
if (state.uploadLoading) {
ElMessage.info('正在上传,请等待上传完成')
} else {
done()
}
}
controller层
@ResponseBody
@RequestMapping(value = "/importExcel",method = RequestMethod.POST)
public Result importExcel(@RequestParam(value = "file", required = false) MultipartFile file,@RequestParam(value = "strJSON", required = false) String strJSON, HttpServletRequest request){
try{
Gson gson = new Gson();
Map reqMap = gson.fromJson(strJSON, Map.class); //body参数
reqMap.put("fileName",file.getOriginalFilename());
//防止文件名出现重复导致后续删除数据出错
//sql:select count(1) from table_name where FILE_NAME = #{fileName, jdbcType=VARCHAR}
int row = userService.getExistByFileName(reqMap.get("fileName"));
if(row > 0){
return Result.TranError("文件名重复");
}
//文件输入流
InputStream in=file.getInputStream();
//调用方法进行读取
//把service直接注入进来为了后面能使用
//因为在listener中不能注入service所以在这个serviceiimpl中,通过listener使service注入进去,为了在listener中能够使用service中的发方法save/
EasyExcel.read(in, ExcelUserEntity.class,new UploadUserDataListener(userService,reqMap.get("fileName").toString())).sheet().doRead();
return Result.TranSuccess("插入成功!");
}catch (Exception e){
log.error("",e);
if(e.getMessage().toString().indexOf("ORA-00001") != -1){
return Result.TranError("存在重复数据!");
}
}
return null;
}
读的对象 :ExcelUserEntity.class
@Getter
@Setter
@EqualsAndHashCode
public class ExcelUserEntity {
@ExcelProperty("用户id")
private String UserId;
@ExcelProperty("姓名")
private String UserName;
private String FileName;//文件名
}
监听器 :UploadUserDataListener.class
@Slf4j
public class UploadUserDataListener implements ReadListener<ExcelUserEntity> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
/**
* 缓存的数据
*/
private List<ExcelUserEntity> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private UserService userService;
// private UserMapper userMapper;
private String fileName;
// public UploadUserDataListener() {
// // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
// demoDAO = new DemoDAO();
// }
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param userService
* @param fileName
*/
public UploadUserDataListener(UserService userService, String fileName) {
this.userService = userService;
this.fileName = fileName;
}
/**
* 当解析抛出异常,会被此方法进行捕获
* 抛出异常-停止解析
* 不抛出-继续解析
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.info("解析出错:"+exception.getMessage());
int row=0,column=0;
if (exception instanceof ExcelDataConvertException){
ExcelDataConvertException convertException=(ExcelDataConvertException) exception;
row=convertException.getRowIndex();
column=convertException.getColumnIndex();
log.error("解析出错:{}行 {}列",row,column);
}
//捕获到异常,删除之前已经存储的数据,保证数据库的数据干净
//sql:delete from table_name where FILE_NAME = #{fileName, jdbcType=VARCHAR}
userService.deleteByFileName(fileName);
//如果抛出该异常,controller层无法捕获
// throw new ExcelAnalysisStopException("解析出错:"+row+"行 "+column+"列,停止运行");
throw new RuntimeException(exception.getMessage());
}
/**
* 解析表数据的表头,headMap 默认表格第一行数据
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
System.out.println("headMap:"+ headMap);
}
/**
* 一条数据解析后的对象 data,可以在这个方法中进行再次处理
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(ExcelUserEntity data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
data.setFileName(fileName);
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 解析完成后,会运行此方法
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
log.info("解析结束");
if(cachedDataList.size()>0){
saveData();
}
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
userService.save(cachedDataList);
log.info("存储数据库成功!");
}
/**
* 校验是否继续解析,默认true
* @param context
* @return
*/
@Override
public boolean hasNext(AnalysisContext context){
return true;
}
}
const downloadUserExcel = async ()=>{
let reqQuery = {
...
}
//发送请求
let res: any = await sysApi.downloadUserExcel(reqQuery);
// 特殊处理返回类型是blob,获取错误信息的情况
if(res.status==200 ){
if(res.data.size==0 || res.data.type.includes('json')){//json表示下载失败,后端返回了错误详情的json格式
let data=res.data
let fileReader=new FileReader()
fileReader.readAsText(data)
fileReader.onload=function(result){
let jsondata=JSON.parse(result.target.result)
ElMessage.error(jsondata.RespMsg);
}
return;
}
}
// 处理响应
// 请求参数记得加'【responseType: 'blob'】',否则容易乱码
let blob = new Blob([res.data], { type: 'application/vnd.ms-excel' });
let url = URL.createObjectURL(blob);
let temp = res.headers['content-disposition']
.split(';')[1]
.split('filename=')[1] //从返回头取出文件名称
let fileNameStr = decodeURI(temp) //中文文件名需解码
let fileName = fileNameStr.replace(/\"/g, '').replace(/\'/g, '')
const link = document.createElement('a'); //创建a标签
link.href = url;
link.download = fileName; //此处可以重命名文件
link.click();
//释放内存
URL.revokeObjectURL(url);
if (document.body.contains(link)) {
document.body.removeChild(link)
}
}
@ResponseBody
@RequestMapping(value = "/downloadUserExcel",method = RequestMethod.POST)
public Result downloadUserExcel(@RequestBody String reqMsg , HttpServletRequest request , HttpServletResponse response){
try{
Gson gson = new Gson();
Map reqMap = gson.fromJson(reqMsg,Map.class);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("用户信息表", "UTF-8").replaceAll("\\+", "%20");
//建议加上该段,否则可能会出现前端无法获取Content-disposition
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
//getData(reqMap) 返回的数据必须是ExcelUserEntity.class的集合,并且无特殊配置每个字段必须有值不可为null,否则无法生成
EasyExcel.write(response.getOutputStream(), ExcelUserEntity.class).sheet("用户信息表").doWrite(getData(reqMap));
}catch (Exception e){
log.error("",e);
}
return null;
}