前言
语雀社区的EasyExcel 导入导出,都是在已知需要哪些表头的情况下。往往实际情况,一张表有很多的字段,有些字段用户并不需要那么多,他只想看关键的部分。假设就有客户提出:我不需要那么多列,我的数据并没有那么多表头项,我想省事,该怎么办? 只能是动态实现,我不知道用户会填什么列,只管接收到,然后返回给用户。 话不多说,直接上码实现。
代码实现
springboot 的项目搭建省略,所用的SQL、配置和依赖请参考我之前写过的:EasyExcel3.0.5导出多个sheet,含查询优化,这里有详细的交代,此篇省略繁琐的步骤,进入核心内容。
新建两张核心的表
我们要实现动态导入,需要借助这两张表做周旋。一张是导入结果表,另一张是导入结果明细表,为什么这么做呢?
思路: 因为每个用户导入的内容可以不一样,所以每次导入可以用唯一的批次作为该用户执行的结果,另一个用户导的是另一批次。每个批次有N多条数据,就是导入的明细。当用户要查询自己到的文件内容,可以根据批次号关联导入数据明细。看具体的数据表设计~~~
导入结果表: tb_import_result
CREATE TABLE `tb_import_result` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`batch_number` varchar(50) NOT NULL COMMENT '导入批次号',
`file_name` varchar(100) DEFAULT NULL COMMENT '文件名称',
`upload_time` datetime DEFAULT NULL COMMENT '上传时间',
`total` bigint(20) DEFAULT NULL COMMENT '总数',
`headers` varchar(4000) DEFAULT NULL COMMENT '表头',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
导入结果明细表:tb_import_result_detail 明细表除了包含数据表的完整字段,还有批次号(batch_number)。但是这么多字段用户并非全部需要。
CREATE TABLE `tb_import_result_detail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`batch_number` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '导入批次号',
`total_pay` bigint(20) DEFAULT NULL COMMENT '总金额,单位为分',
`actual_pay` bigint(20) DEFAULT NULL COMMENT '实付金额。单位:分。如:20007,表示:200元7分',
`payment_type` tinyint(1) unsigned zerofill DEFAULT NULL COMMENT '支付类型,1、在线支付,2、货到付款',
`post_fee` bigint(20) DEFAULT NULL COMMENT '邮费。单位:分。如:20007,表示:200元7分',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
`shipping_name` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流名称',
`shipping_code` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流单号',
`buyer_message` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '买家留言',
`buyer_nick` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '买家昵称',
`buyer_rate` tinyint(1) DEFAULT NULL COMMENT '买家是否已经评价,0未评价,1已评价',
`receiver_state` varchar(100) COLLATE utf8_bin DEFAULT '' COMMENT '收获地址(省)',
`receiver_city` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '收获地址(市)',
`receiver_district` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '收获地址(区/县)',
`receiver_address` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '收获地址(街道、住址等详细地址)',
`receiver_mobile` varchar(12) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人手机',
`receiver_zip` varchar(15) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人邮编',
`receiver` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',
`invoice_type` int(1) DEFAULT '0' COMMENT '发票类型(0无发票1普通发票,2电子发票,3增值税发票)',
`source_type` int(1) DEFAULT '2' COMMENT '订单来源:1:app端,2:pc端,3:M端,4:微信端,5:手机qq端',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
EasyExcel 动态导入、导出
所谓的动态导入,不管用户导入什么表头(前提是你数据库中的一张表中的部分字段,字段很多,每个用户导入的内容不一样),后端只需要保存用户每次导入的表头和数据。开干!
Controller 层
package cn.com.easyExcel.controller;
import cn.com.easyExcel.param.OrderExportParam;
import cn.com.easyExcel.service.OrderExcelService;
import cn.com.easyExcel.vo.ResultVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Api(tags = "订单信息导入导出")
@RestController
@RequestMapping(value = "/order/excel")
public class OrderExcelController {
@Autowired
private OrderExcelService orderExcelService;
@ApiOperation(value = "动态导入")
@PostMapping(value="/dynamicImport")
public ResultVo<Void> dynamicImportExcel(@RequestParam(name = "file") MultipartFile file) throws IOException {
orderExcelService.dynamicImportExcel(file);
return ResultVo.successMsg("导入成功");
}
@ApiOperation(value = "动态导出")
@PostMapping(value="/dynamicExport")
public void dynamicExportExcel(@RequestBody OrderExportParam param, HttpServletResponse response) throws IOException {
orderExcelService.dynamicExportExcel(param, response);
}
}
Service 实现
动态导出的完整代码全部在这个实现类里面,动态导入还有一个 监听器,重点部分,后面会补充解释。
package cn.com.easyExcel.service.impl;
import cn.com.easyExcel.excel.converter.CellWriteWeight;
import cn.com.easyExcel.excel.listener.OrderImportListener;
import cn.com.easyExcel.excel.util.EasyExcelUtils;
import cn.com.easyExcel.mapper.ImportResultDetailMapper;
import cn.com.easyExcel.mapper.ImportResultMapper;
import cn.com.easyExcel.param.OrderExportParam;
import cn.com.easyExcel.pojo.ExcelHeader;
import cn.com.easyExcel.pojo.ImportResult;
import cn.com.easyExcel.pojo.ImportResultDetail;
import cn.com.easyExcel.pojo.OrderRsp;
import cn.com.easyExcel.service.ImportResultService;
import cn.com.easyExcel.service.OrderExcelService;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class OrderExcelServiceImpl implements OrderExcelService {
@Autowired
private ImportResultService importResultService;
@Autowired
private ImportResultDetailMapper detailMapper;
@Autowired
private ImportResultMapper resultMapper;
private static final int PAGE_SIZE = 100;
private static final String CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String CONTENT_DISPOSITION = "Content-Disposition";
private static final String ACCESS_CONTROL_EXPOSE = "Access-Control-Expose-Headers";
private static final String CHARACTER = "UTF-8";
@Override
public void dynamicImportExcel(MultipartFile file) throws IOException {
String batchNumber = IdUtil.simpleUUID();
EasyExcel.read(file.getInputStream(),
OrderRsp.class,
new OrderImportListener(importResultService, file.getOriginalFilename(), batchNumber))
.sheet().headRowNumber(1).doRead();
}
@Override
public void dynamicExportExcel(OrderExportParam param,
HttpServletResponse response) throws IOException {
long startTime = System.currentTimeMillis();
LambdaQueryWrapper<ImportResultDetail> detailWrapper = new LambdaQueryWrapper<>();
detailWrapper.eq(ImportResultDetail::getBatchNumber, param.getBatchNumber());
List<ImportResultDetail> allDetailList = new ArrayList<>();
int startIndex = 1;
while (true) {
int startParam = (startIndex - 1) * PAGE_SIZE;
int pageIndex = (int) Math.ceil((double) startParam / (double) PAGE_SIZE + 1);
Page<ImportResultDetail> pageQuery = new Page<>(pageIndex, PAGE_SIZE, false);
Page<ImportResultDetail> detailByPage = detailMapper.selectPage(pageQuery, detailWrapper);
List<ImportResultDetail> detailList = detailByPage.getRecords();
if (CollectionUtils.isEmpty(detailList)) {
break;
}
allDetailList.addAll(detailList);
startIndex++;
}
ServletOutputStream outputStream = exportHeader(response, "动态导出信息.xlsx");
ExcelWriter excelWriter = EasyExcelFactory.write(outputStream)
.registerWriteHandler(new CellWriteWeight()).build();
LambdaQueryWrapper<ImportResult> resultWrapper = new LambdaQueryWrapper<>();
resultWrapper.eq(ImportResult::getBatchNumber, param.getBatchNumber());
ImportResult importResult = resultMapper.selectOne(resultWrapper);
List<ExcelHeader> excelHeaders = JSON.parseArray(importResult.getHeaders(), ExcelHeader.class);
writeDate(excelWriter, allDetailList, excelHeaders);
excelWriter.finish();
outputStream.flush();
long endTime = System.currentTimeMillis();
log.info("动态导出耗时:{}", endTime - startTime);
}
private void writeDate(ExcelWriter excelWriter, List<ImportResultDetail> detailList, List<ExcelHeader> headers) {
WriteSheet writeSheet = EasyExcel.writerSheet("导入结果明细").sheetNo(0)
.head(EasyExcelUtils.headers(headers))
.needHead(true)
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.build();
List<List<Object>> allList = new ArrayList<>();
for (ImportResultDetail detail : detailList) {
allList.addAll(EasyExcelUtils.dataList(headers, detail));
}
excelWriter.write(allList, writeSheet);
}
public ServletOutputStream exportHeader(HttpServletResponse response,
String fileName) throws IOException {
response.setContentType(CONTENT_TYPE);
response.setHeader(ACCESS_CONTROL_EXPOSE, CONTENT_DISPOSITION);
response.setHeader(CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, CHARACTER));
return response.getOutputStream();
}
}
动态导入监听器
package cn.com.easyExcel.excel.listener;
import cn.com.easyExcel.excel.converter.HeadPropertiesConverter;
import cn.com.easyExcel.pojo.ExcelHeader;
import cn.com.easyExcel.pojo.ImportResult;
import cn.com.easyExcel.pojo.ImportResultDetail;
import cn.com.easyExcel.pojo.OrderRsp;
import cn.com.easyExcel.service.ImportResultService;
import cn.com.easyExcel.util.BeanCopyUtils;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderImportListener implements ReadListener<OrderRsp> {
private final ImportResultService importResultService;
private final String fileName;
private final String batchNumber;
private static final int BATCH_COUNT = 100;
AtomicInteger total = new AtomicInteger(0);
private String headers;
private List<ImportResultDetail> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
public OrderImportListener(ImportResultService importResultService,
String fileName,
String batchNumber) {
this.importResultService = importResultService;
this.fileName = fileName;
this.batchNumber = batchNumber;
}
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
Map<String, String> propHead = new HashMap<>();
Map<String, String> propMap = HeadPropertiesConverter.getHeadProperty(propHead, context.readWorkbookHolder().getClazz());
List<ExcelHeader> headerList = new ArrayList<>();
assert propMap != null;
for (Map.Entry<Integer, ReadCellData<?>> headMapEntry : headMap.entrySet()) {
log.info("head的key:{},head的value:{} ",headMapEntry.getKey(), headMapEntry.getValue());
for (Map.Entry<String, String> propMapEntry : propMap.entrySet()) {
if (StrUtil.equals(propMapEntry.getKey(), headMapEntry.getValue().getStringValue())) {
headerList.add(ExcelHeader.builder().name(propMapEntry.getKey()).prop(propMapEntry.getValue()).build());
}
}
}
headers = JSON.toJSONString(headerList);
List<ExcelHeader> excelHeaders = JSON.parseArray(headers, ExcelHeader.class);
log.info("excelHeaders:{}", excelHeaders);
}
@Override
public void invoke(OrderRsp orderRsp, AnalysisContext analysisContext) {
log.info("解析到一条数据:{}", JSON.toJSONString(orderRsp));
total.addAndGet(1);
ImportResultDetail importResultDetail = BeanCopyUtils.copyBean(orderRsp, ImportResultDetail::new);
importResultDetail.setBatchNumber(batchNumber);
cachedDataList.add(importResultDetail);
if (cachedDataList.size() >= BATCH_COUNT) {
batchSaveData();
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
batchSaveData();
ImportResult importResult = ImportResult.builder()
.batchNumber(batchNumber)
.fileName(fileName)
.uploadTime(new Date())
.total(total.get())
.headers(headers).build();
importResultService.insertImportResult(importResult);
}
public void batchSaveData(){
importResultService.batchInsertResultDetails(cachedDataList);
}
}
HeadPropertiesConverter:利用反射转换 excel 表头
package cn.com.easyExcel.excel.converter;
import com.alibaba.excel.annotation.ExcelProperty;
import java.lang.reflect.Field;
import java.util.Map;
public class HeadPropertiesConverter {
public static Map<String, String> getHeadProperty(Map<String, String> propHead, Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
if (fields.length == 0) {
return null;
}
for (Field field : fields) {
if (field.getAnnotation(ExcelProperty.class) != null) {
propHead.put(field.getAnnotation(ExcelProperty.class).value()[0], field.getName());
}
}
return propHead;
}
}
实体
导入结果: ImportResult.java
package cn.com.easyExcel.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_import_result")
@ApiModel(value = "ImportResult", description = "导入结果")
public class ImportResult {
@ApiModelProperty(value = "每次导入的批次号(uuid)")
private String batchNumber;
@ApiModelProperty(value = "上传文件名")
private String fileName;
@ApiModelProperty(value = "上传时间")
private Date uploadTime;
@ApiModelProperty(value = "总数")
private int total;
@ApiModelProperty(value = "excel表头", hidden = true)
private String headers;
}
导入结果明细:ImportResultDetail.java
package cn.com.easyExcel.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_import_result_detail")
@ApiModel(value = "ImportResultDetail", description = "导入结果明细")
public class ImportResultDetail {
@ApiModelProperty(value = "导入结果明显id", hidden = true)
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "导入批次号")
private String batchNumber;
@ApiModelProperty("总金额,单位为分")
private Long totalPay;
@ApiModelProperty("实付金额。单位:分。如:20007,表示:200元7分")
private Long actualPay;
@ApiModelProperty("支付类型,1、在线支付,2、货到付款")
private Boolean paymentType;
@ApiModelProperty("邮费。单位:分。如:20007,表示:200元7分")
private Long postFee;
@ApiModelProperty("订单创建时间")
private LocalDateTime createTime;
@ApiModelProperty("物流名称")
private String shippingName;
@ApiModelProperty("物流单号")
private String shippingCode;
@ApiModelProperty("买家留言")
private String buyerMessage;
@ApiModelProperty("买家昵称")
private String buyerNick;
@ApiModelProperty("买家是否已经评价,0未评价,1已评价")
private Boolean buyerRate;
@ApiModelProperty("收获地址(省)")
private String receiverState;
@ApiModelProperty("收获地址(市)")
private String receiverCity;
@ApiModelProperty("收获地址(区/县)")
private String receiverDistrict;
@ApiModelProperty("收获地址(街道、住址等详细地址)")
private String receiverAddress;
@ApiModelProperty("收货人手机")
private String receiverMobile;
@ApiModelProperty("收货人邮编")
private String receiverZip;
@ApiModelProperty("收货人")
private String receiver;
@ApiModelProperty("发票类型(0无发票1普通发票,2电子发票,3增值税发票)")
private Integer invoiceType;
@ApiModelProperty("订单来源:1:app端,2:pc端,3:M端,4:微信端,5:手机qq端")
private Integer sourceType;
}
表头:ExcelHeader.java
package cn.com.easyExcel.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExcelHeader {
private String name;
private String prop;
}
核心代码思路
动态导入核心关键在监听器的 invokeHead 方法,用了反射获取表头的 属性字段和中文名称。拼成 name 和 prop 的JSON 字符,与前端表格 header 格式一致。
动态导出的核心方法:writeDate,也是利用反射,根据 header 的内容,取出 ImportResultDetail 的部分字段展示给用户。
结语
亲测可用。各位看官可自行测试,若觉得有用,欢迎评论留言。
|