提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
实现思路
最近公司项目刚好有一个大批量Excel数据导入到数据库的需求,使用框架自带的文件导入可以实现,但是用户体验不佳,想着优化一下,以下做个记录,先看效果
需求:前端选中Excel文档,后台实现多线程批量插入数据库,前端同时展示实时进度条
和同为打工仔的好兄弟们讨论了实现思路如下:
1、前端-将Excel数据和一个时间戳唯一key传入后台的导入接口;
2、后台-解析数据,此时间戳唯一key作为此次导入数据的插入行数的redis唯一key,用于获取上传进度;
3、后台-使用框架自带的@Async注解写一个Task任务类,实现多线程批量插入(此处由于是框架自带的,我并未深入研究),插入执行完后以前端传过来的时间戳为key,插入行数+=为值,创建redis变量,并设置30秒自动销毁;
3、前端-导入请求发出时,同时创建一个定时器 轮巡发送获取导入行数请求,后台以时间戳为key查询redis中存放的影响行数返回给前端;
4、前端-拿到插入行数后计算导入进度并渲染到进度条中
5、前端-导入成功-关闭进度条
具体实现如下
一、后端实现
思路:后台-解析数据,此时间戳唯一key作为此次导入数据的插入行数的redis唯一key,用于获取上传进度;
根据思路我们直接上代码,此处就只展示关键代码,其他的解析数据那些根据自己实际情况写就好
1.多线程导入
········获取导入的Excel集合和redis唯一key······
········数据查重 等等等······
以下代码含义是:
将导入的Excel集合根据Count条目数,截取然后使用多线程分批处理
//单次处理条目数
int count=500;
for (int i = 0; i < (Math.round((dataCellCapacityDtoList.size() / count)+0.5)); i++) {
int startLen = i * count;
int endLen = ((i + 1) * count > dataCellCapacityDtoList.size() ? dataCellCapacityDtoList.size() : (i + 1) * count);
//截取指定条目数
List<DataCellCapacityDto> newList = dataCellCapacityDtoList.subList(startLen, endLen);
//多线程执行插入
Future<Integer> integerFuture = addCellFactoryDataTask.insertBatchCellFactoryData(newList);
Integer row = integerFuture.get();
importRow = importRow + row;
if(row==newList.size()){
// System.out.println("---------------------------插入成功,影响行数:"+row+"--------------------");
//更新redis变量
redisUtils.set(importCellDataParam.getKey(),importRow);
//设定 30秒 过期时间
redisUtils.expire(importCellDataParam.getKey(),30);
}
}
线程任务类代码:
@Component
public class AddCellFactoryDataTask {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//电芯出厂数据
@Autowired
private DataCellCapacityMapper dataCellCapacityMapper;
@Async
public void doTask2(int i) throws InterruptedException{
logger.info("Task2-Native"+i+" started.");
}
@Async
public Future<Integer> insertBatchCellFactoryData(List<DataCellCapacityDto> dataCellCapacityDtoList) throws InterruptedException{
// long startTime = System.currentTimeMillis();
//获得当前线程的名称
// System.out.println("---------------------线程名称:"+Thread.currentThread().getName()+"-----------------");
Integer row = dataCellCapacityMapper.importCellCapacityBatch(dataCellCapacityDtoList);
// long insetEndTime = System.currentTimeMillis();
// System.out.println("-------------------插入执行耗时:"+(insetEndTime-startTime)+"/ms----------------------");
return new AsyncResult(row);
}
}
线程是用的项目自带的,别问怎么实现,因为我也没深究,网上基本上都有教程,能跑起来就是好样的。
2.获取导入进度接口
这个就很简单啦,就是把前端的时间戳key传个后台然后依次为redis唯一key查影响行数
@Override
public int getImportCount(String key) {
Object importRow = redisUtils.get(key);
//此处为了解决唯一key还未生成时的空指针异常
if(ObjectUtil.isNull(importRow)){
return 0;
}
return (Integer)importRow;
}
二、前端
由于导入组件是封装在crud组件的,所以前端的遇到的难点就是父页面中如何控制子组件的显示隐藏,以及父子组件的传值,可以参阅以下代码
官方给的Excel导入中有一个on-progress事件也可以实现文件上传时触发事件。
that.showProgress=!that.showProgress;
//获取当前时间戳作为redis唯一key
let redisKey = parseInt(new Date().getTime() / 1000) + '';
//组装上传实体类
let importDatas = {
key: redisKey,
excelDatas: excelDatas
};
//上传
crudDataCellCapacity.importCellCapacityBatch(importDatas).then(response => {
that.$notify.success({
title: '成功',
message: '导入成功1!'
});
console.log("导入成功")
that.crud.loading = false;
//刷新
that.crud.toQuery();
}).catch((e) => {
that.crud.loading = false;
that.$notify.error({
title: '错误',
message: '导入失败系统发生错误!'
});
})
1.父组件传值给子组件
由于导入组件是封装好引入使用的,所以需要在父页面中传值给子组件控制进度条的显示/隐藏
思路就是:在子组件的props中定义好属性,然后就可以通过在父页面引入时传值到子组件中,从而控制进度条隐藏/显示;
子组件
<!--进度条-->
<el-progress :hidden=showProgress :text-inside="true" :stroke-width="24" :percentage=progressValue status="success"></el-progress>
-----省略一万行代码------
/*进度条值*/
progressValue:{
type:Number,
default:0
},
showProgress: {
type: Boolean,
default: true
},
父页面
<crudOperation
:permission="permission"
:progressValue="progressValue"
:showProgress="showProgress"/>
----省略一万行代码-----
//进度条值
progressValue:0,
//进度条是否显示 http://t.csdn.cn/cdMZa
//这里的思路是通过!取反来控制进度条是否显示
showProgress:true,
上传Excel(用Ajax也是一样的,前端我不是很懂 这个是封装好的,不过基本大差不差)
//上传
crudDataCellCapacity.importCellCapacityBatch(importDatas).then(response => {
that.$notify.success({
title: '成功',
message: '导入成功1!'
});
console.log("导入成功")
that.crud.loading = false;
//刷新
that.crud.toQuery();
}).catch((e) => {
that.crud.loading = false;
that.$notify.error({
title: '错误',
message: '导入失败系统发生错误!'
});
})
实现思路参阅:
父组件传值给子组件
2.定时器获取上传进度
获取上传进度
下面的that就是this对象,因为我是封装好的回调函数,所以导致我函数里this指向发生了改变,因为不知道怎么解决,所以我在最外层把this赋值给了that而已
//启动定时器 间隔200毫秒获取上传进度
let timer = setInterval(function () {
let i = 0;
crudDataCellCapacity.getImportCount(redisKey).then(importRow => {
console.log("上传进度=" + importRow + "/" + excelDatas.length + "=" + (importRow / excelDatas.length))
//保留两位小数
let val = importRow / excelDatas.length * 100;
// let realVal = parseFloat("48.99999").toFixed(2);
let realVal = Math.round(val);
//将上传进度渲染到进度条中
that.progressValue = realVal;
if (importRow == excelDatas.length) {
//如果导入行数=Excel行数 就销毁定时器
console.log("定时器被销毁");
clearInterval(timer);
console.log("关闭进度条");
that.showProgress=!that.showProgress;
location.reload()
return;
}
})
}, 200);
总结
此文主要是用于记录自己开发中所遇到的问题和解决办法,有用可以参阅,无用勿喷,感谢
(附:感谢打工仔龙工以及打工仔王工的技术、思路支持)