excel导入较导出还是复杂一些,一般分为三个步骤.市面上低代码平台可以将常用的操作固化,并且形成收益,这也是挺好的。我将我的一些总结分享到网上也是我自己乐意的。毕竟尊重技术的还是搞技术的自身,一般企业老板并不太关心技术代码到底有什么价值,认为脱离了业务代码,这些代码就像封存多年“宝物”上的灰尘。
1下载导入模板
先定义一个对话框,下载导入的excel模板
<template><BaseDialog ref="dialog" title="导入其他入库单" :visible='visible' @close="cancelDialogs" @confirm='importConfirm' :confirm-loading='isdeling' cancelText='取消' confirmText='导入'><p class="firstStep">第一步:请点击下面的链接下载Excel模板,并填写销售出库单信息</p> <p class="upload" @click="upload()">下载模板</p><p>第二步:导入完成的Excel文件</p><el-upload class="upload-demo" ref="upload" accept=".xls,.xlsx" action="#" :on-change="handleChange" :before-upload='beforeUpload' :on-preview="handlePreview" :on-remove="handleRemove" :file-list="fileList" :auto-upload="false" :http-request="httpRequest"><span slot="trigger" class="upload">选取文件</span><!-- :action="UploadUrl()" --></el-upload></BaseDialog></template><script>import BaseDialog from '@/components/base/BaseDialog.vue';import {computed } from 'vue'import { useAppStore } from '@/store'const appStore = useAppStore()const userInfo = computed(() => appStore.userInfo);export default {props: {// period: {// type: String,// },visible: {type: Boolean,},cancelDialog: {type: Function,default: () => { },},initTable:{type: Function,default: () => { },}},components:{BaseDialog},data() {return {fileList: [],fileData: '',isdeling: false,tableData: [],status: false,message : '请选择上传文件',};},methods: {httpRequest(param) {console.log(param.file);},handleChange(file, fileList){this.fileList = fileList.slice(-1); //限定上传文件为一个},handleRemove(file, fileList) {this.fileData = '';this.status = false;this.message = '请选择上传文件';},beforeUpload(file, fileList) {let testFile = file.name.substring(file.name.lastIndexOf('.')+1)const extension = testFile === 'xls'const extension2 = testFile === 'xlsx'const isLt1M = file.size / 1024 / 1024 < 1;if(!extension && !extension2) {this.message = '上传文件只能是xls或xlsx格式!'this.status = false;}else if (!isLt1M) {this.message = '上传Excel文件大小不能超过 1MB!'this.status = false;}else{let fd = new FormData()fd.append('file', file)fd.append('userId',userInfo.value.id)fd.append('asId', userInfo.value.currentAsId)// fd.append('period', this.period)this.fileData = fd;this.status = true;}},handlePreview(file) {console.log(file);},cancelDialogs(){this.cancelDialog();this.fileData = '';this.status = false;this.message = '请选择上传文件';},async importConfirm() {this.tableData = [];this.$refs.upload.submit()if (this.fileData && this.fileData != ''&& this.status) {this.isdeling = true;// 导入这里有点差别,post请求获取的导入后的结果const re = await this.$api.invOrder.otherInstockOrder.imp(this.fileData)const res = re.dataif (res.success) {this.cancelDialog();this.initTable();this.isdeling = false;this.fileList = [];this.fileData = '';this.status = false;this.message = '请选择上传文件';var reason = ''if ( res.data.failNum > 0 ) {var i = 0;res.data.failReason.forEach(each => {i++;if (i < 5) {reason = reason.concat(`<p style="padding-top:0.5rem">第${each.index}行${each.reason},</p>`)}if (i == 5) {reason = reason.concat(`<p style="padding-top:0.5rem">...</p>`)}this.tableData.push({错误行: "第" + each.index + "行",错误信息: each.reason});})//有错误信息将错误信息通过excel导出if (this.tableData.length > 0) {this.$tool.json2Excel(this.tableData)}}let msg = `<p>总条数:${res.data.totalNum} , 成功条数:${res.data.successNum} , 失败条数:${res.data.failNum}</p>${reason}`this.$message.success({duration: 3000,dangerouslyUseHTMLString: true,message: msg}); } else {this.isdeling = false;this.fileList = [];this.fileData = '';this.cancelDialog();this.$message.error(res.msg);}} else {this.isdeling = false;this.fileList = [];this.fileData = '';this.cancelDialog();this.$message.error(this.message);return;}},async upload() {let name = '其他入库单导入模板.xls'const res = await this.$api.invOrder.otherInstockOrder.getTpl({asId:userInfo.value.currentAsId})let data = res.data;let url = window.URL.createObjectURL(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }))let link = document.createElement('a')link.style.display = 'none'link.href = url;link.setAttribute('download', name)document.body.appendChild(link)link.click()document.body.removeChild(link)},}};</script><style lang="less" scoped>.firstStep {margin: 24px 0 12px;}.upload {cursor: pointer;color: #4f71ff;}</style>
后台代码也是固定的写法
@GetMapping("getTpl")@ApiOperation("其他入库单导入模板")public void getSalOutstockTpl(@ApiParam(value = "账套ID", required = true) @RequestParam Integer asId,HttpServletResponse response){String fileName = "其他入库单导入模板.xls";//设置响应头try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("xls/" + fileName);OutputStream os = response.getOutputStream();) {response.setContentType("application/octet-stream");response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));byte[] buf = new byte[1024];int len = 0;while ((len = is.read(buf)) != -1) {os.write(buf, 0, len);}} catch (IOException e) {e.printStackTrace();}}
2 导入解析
excel的模板样式如下图
controller层,基本是套路,因为具体实现逻辑是在service层
@ApiOperation("导入")@PostMapping("imp")public ResponseResult<ExcelErrMsg> imp(Integer asId, Integer userId, MultipartFile file){ResponseResult<ExcelErrMsg> resp = new ResponseResult<>(true, "导入完成!");try {ExcelErrMsg excelErrMsg = otherInstockListExcelService.imp(asId, userId, file);resp.setData(excelErrMsg);} catch (ExcelCommonException | ExcelAnalysisException e) {log.error(e.getMessage(), e);resp = new ResponseResult<>(false, "请使用正确的导入模板导入");} catch (RuntimeException e) {log.error(e.getMessage(), e);resp = new ResponseResult<>(false, e.getMessage());} catch (Exception e) {log.error(e.getMessage(), e);resp = new ResponseResult<>(false, "导入失败!");}return resp;}
service层,这里相当于是基于easyexcel定义的一个算法模板,基本解析套路也就这样。
@Overridepublic ExcelErrMsg imp(Integer asId, Integer createUser, MultipartFile file) throws IOException {List<OtherInstockExcelDto> lines =new ArrayList<>();List<ExcelErrDetail> excelErrDetails = new ArrayList<>();// 开始处理excelEasyExcel.read(file.getInputStream(), new AnalysisEventListener<Map<Integer, String>>() {/*** 从这里判断出模板类型,目前是根据列数*/@Overridepublic void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context) {//校验表头if (headMap.isEmpty()) {throw new RuntimeException("导入模板表头不可为空");}super.invokeHead(headMap, context);}/*** 表头信息* 检查的信息有限* 1. 检查表头是否存在* 2. 检查列数是否正确* @param headMap* @param context*/@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {}/*** 解析行信息* @param integerStringMap* @param analysisContext*/@Overridepublic void invoke(Map<Integer, String> integerStringMap, AnalysisContext analysisContext) {//数据行号Integer rowIndex = analysisContext.readRowHolder().getRowIndex() + 1;// 行级校验if (rowIndex>4){// 数据是从第5行开始checkRow(integerStringMap,rowIndex,lines,excelErrDetails);}}/*** excel解析完毕后的数据处理* 逻辑是先完善基础设置,然后再写入期初* 处理辅助核算、处理科目、处理期初这三个顺序不能变* @param analysisContext*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {// 校验数据的正确性checkRecords(asId,lines,excelErrDetails);// 过滤掉存在问题的科目数据List<OtherInstockExcelDto> noerrorList = lines.stream().filter(a -> !a.isHasError()).collect(Collectors.toList());// 处理没有问题的信息handle(asId,createUser,noerrorList);}/*** 处理异常* @param exception* @param context* @throws Exception*/@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {if (exception instanceof RuntimeException) {throw (RuntimeException) exception;}}}).sheet().doRead();// 处理excel中的错误信息List<ExcelErrDetail> sortErrs = excelErrDetails.stream().sorted(Comparator.comparing(ExcelErrDetail::getIndex)).collect(Collectors.toList());List<Integer> errs = sortErrs.stream().map(ExcelErrDetail::getIndex).distinct().collect(Collectors.toList());ExcelErrMsg excelErrMsg = new ExcelErrMsg();// 所有条数求和excelErrMsg.setTotalNum(lines.size());excelErrMsg.setFailNum(errs.size());excelErrMsg.setSuccessNum(excelErrMsg.getTotalNum() - excelErrMsg.getFailNum());excelErrMsg.setFailReason(sortErrs);return excelErrMsg;}
校验excel中基本行数据,这里就不展开,因为这里是并不与数据库有交互,因为一次性校验,查看那些数据有问题,所以遇到错误不需要停止。
private void checkRow(Map<Integer, String> row, Integer rowIndex, List<OtherInstockExcelDto> records, List<ExcelErrDetail> excelErrDetails,PsiAccountSet psiAccountSet){OtherInstockExcelDto line = new OtherInstockExcelDto();line.setIndex(rowIndex);// *单据日期if (CheckEmptyUtil.isEmpty(row.get(0))){excelErrDetails.add(new ExcelErrDetail(rowIndex, "单据日期不能为空"));line.setHasError(true);}line.setBillDate(DateUtil.parseDate(row.get(0)));// *数量if (CheckEmptyUtil.isEmpty(row.get(13))){excelErrDetails.add(new ExcelErrDetail(rowIndex, "数量不能为空"));line.setHasError(true);}line.setSl(ExcelUtil.getNumber(row.get(13),psiAccountSet.getSlxsw()));//records.add(line);}
接着有些数据导入之前是一些基础数据,因此需要判断数据库中是否存在,因此进入到第二层筛选
为什么会有下面的代码,因为财务辅助核算中供应商、客户、存货基础数据都是在一张表中,有的用户,客户资料非常之多,因此即使你使用二级缓存,将数据全部加载进去,也是非常消耗性能的,因此获取基础数据的逻辑就是用到了什么,就取什么。
/*** 获取辅助核算的map,key:code_type,value:id* 包含:供应商、客户、存货* @param records* @return*/Map<String,Integer> getCodeIdMap(Integer asId,List<OtherInstockExcelDto> records){if (!CheckEmptyUtil.isEmpty(records)){Set<String> codes = new HashSet<>();for (OtherInstockExcelDto record: records){if (!CheckEmptyUtil.isEmpty(record.getVendorCode())){codes.add(record.getVendorCode());}if (!CheckEmptyUtil.isEmpty(record.getCustomerCode())){codes.add(record.getCustomerCode());}if (!CheckEmptyUtil.isEmpty(record.getStockCode())){codes.add(record.getStockCode());}}List<Integer> aaTypes = Arrays.asList(CommonSettingConstants.AssistingAccountingType.STOCK_ID,CommonSettingConstants.AssistingAccountingType.CUSTOMER_ID,CommonSettingConstants.AssistingAccountingType.VENDOR_ID);return accAssistingAccountingService.getCodeIdMap(asId,aaTypes,new ArrayList<>(codes));}return new HashMap<>();}
校验基础资料,取出最小集出来,然后判断系统中是否存在。如果不存在,则需要在导入之前先维护。
/*** 基础数据的正确性* @param records*/public void checkRecords(Integer asId,List<OtherInstockExcelDto> records,List<ExcelErrDetail> excelErrDetails,PsiAccountSet psiAccountSet){// 辅助核算:商品信息、供应商、客户辅助核算Map<String,Integer> aaMap = getCodeIdMap(asId,records);// 仓库信息Map<String,String> whMap = psiInvWarehouseService.getNameIdMap(asId);// 数据字典:单位、入库类型Map<String,String> ccMap = ccsDataDictionaryService.selectNameIdMap(asId,Arrays.asList(CommonSettingConstants.CcsDataDictType.UNIT,CommonSettingConstants.CcsDataDictType.INSTOCK));//for (OtherInstockExcelDto record: records){// 单据日期if (DateUtil.getFirstDate(Integer.parseInt(psiAccountSet.getYear()),Integer.parseInt(psiAccountSet.getMonth())).compareTo(record.getBillDate())>0){excelErrDetails.add(new ExcelErrDetail(record.getIndex(), String.format("单据日期:%s,不能在账套的启用日期之前,请先调整", record.getBillDate())));record.setHasError(true);}// 供应商if (!CheckEmptyUtil.isEmpty(record.getVendorCode())){if (!aaMap.containsKey(record.getVendorCode()+ BaseConstant.Separate.UNDERLINE+CommonSettingConstants.AssistingAccountingType.VENDOR_ID)){excelErrDetails.add(new ExcelErrDetail(record.getIndex(), String.format("供应商编码:%s,在岁月进销存中不存在,请先添加", record.getVendorCode())));record.setHasError(true);} else{record.setVendorId(aaMap.get(record.getVendorCode()+ BaseConstant.Separate.UNDERLINE+CommonSettingConstants.AssistingAccountingType.STOCK_ID));}}}
最后的handle
是将正常的数据写入到系统中,这里就不再追溯,根据各自业务系统的逻辑来。
3 导入异常处理
异常处理的包含两方面,一方面在页面上给出提示,另一方面将错误信息导出到excel中。这样有助于排查错误在什么地方。
这里就用到了,将json数据转成excel下载。操作在页面中实现
tool.json2Excel = (dataSource,title)=> {var wopts = {bookType: 'xls',bookSST: false,type: 'binary'};var workBook = {SheetNames: ['Sheet1'],Sheets: {},Props: {}};//1、XLSX.utils.json_to_sheet(data) 接收一个对象数组并返回一个基于对象关键字自动生成的“标题”的工作表,默认的列顺序由使用Object.keys的字段的第一次出现确定//2、将数据放入对象workBook的Sheets中等待输出workBook.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(dataSource)//3、XLSX.write() 开始编写Excel表格//4、changeData() 将数据处理成需要输出的格式saveAs(new Blob([changeData(XLSX.write(workBook, wopts))], {type: 'application/octet-stream'}),title)
}