EasyExcel 实现动态导入导出

前言

语雀社区的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) {
        //获取sheet对象
        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));
        }
        //向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<OrderRsp> {

    private final ImportResultService importResultService;

    private final String fileName;

    private final String batchNumber;

    /**
     * 每隔100条存储数据库,然后清理list,方便内存回收
     */
    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();
            // 存储完成清理 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<String, String> getHeadProperty(Map<String, String> 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 格式一致。

EasyExcel 实现动态导入导出_第1张图片

动态导出的核心方法:writeDate,也是利用反射,根据 header 的内容,取出 ImportResultDetail 的部分字段展示给用户。

结语

亲测可用。各位看官可自行测试,若觉得有用,欢迎评论留言。

你可能感兴趣的:(easyexcel,Springboot,excel,spring,boot)