语雀社区的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 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 detailWrapper = new LambdaQueryWrapper<>();
detailWrapper.eq(ImportResultDetail::getBatchNumber, param.getBatchNumber());
List 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 pageQuery = new Page<>(pageIndex, PAGE_SIZE, false);
Page detailByPage = detailMapper.selectPage(pageQuery, detailWrapper);
List 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 resultWrapper = new LambdaQueryWrapper<>();
resultWrapper.eq(ImportResult::getBatchNumber, param.getBatchNumber());
ImportResult importResult = resultMapper.selectOne(resultWrapper);
List 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 detailList, List headers) {
//获取sheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("导入结果明细").sheetNo(0)
.head(EasyExcelUtils.headers(headers))
.needHead(true)
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.build();
List> allList = new ArrayList<>();
for (ImportResultDetail detail : detailList) {
allList.addAll(EasyExcelUtils.dataList(headers, detail));
}
//向sheet写入数据
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{ 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 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 > headMap, AnalysisContext context) { Map propHead = new HashMap<>(); Map propMap = HeadPropertiesConverter.getHeadProperty(propHead, context.readWorkbookHolder().getClazz()); List headerList = new ArrayList<>(); assert propMap != null; for (Map.Entry > headMapEntry : headMap.entrySet()) { log.info("head的key:{},head的value:{} ",headMapEntry.getKey(), headMapEntry.getValue()); for (Map.Entry 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 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(); // 存储完成清理 list 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 getHeadProperty(Map propHead, Class> clazz) {
Field[] fields = clazz.getDeclaredFields(); //方式二:Field[] fields = ReflectUtil.getFields(clazz);
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 的部分字段展示给用户。
结语亲测可用。各位看官可自行测试,若觉得有用,欢迎评论留言。



