使用 EasyExcel 实现 Web 端 Excel 导入与导出

本文记录了我在学习使用阿里巴巴开源项目 EasyExcel 实现 Web 端 Excel 文件导入与导出的过程,涵盖前端交互、后端处理及数据映射等关键步骤。以下为详细整理,旨在总结核心知识点并为后续开发提供参考。

一、Excel 数据导入

1. 需求概述

实现通过前端上传 Excel 文件(支持 .xls.xlsx 格式),后端接收并解析文件内容,存储到数据库。请求方式为 POST,路径为 /api/import-excel,参数为表单数据,键为 file,值为上传的文件。

2. 前端实现

前端采用 Element Plus 组件库,通过 <el-upload> 实现文件上传功能,结合弹窗提示用户上传规则。以下为主要代码:

2.1 界面与交互

<el-button type="success" @click="importExcel">导入线索(Excel)</el-button>

<el-dialog v-model="importExcelWindows" title="导入线索 Excel" width="600" draggable>
 <el-upload
   ref="uploadRef"
   :auto-upload="false"
   :http-request="uploadFile"
   method="post"
 >
   <template #trigger>
     <el-button type="primary">选择 Excel 文件</el-button>
   </template>
   <div class="fileTip">
    仅支持后缀名为 .xls 或 .xlsx 的文件
     <div>
       <strong>重要提示:</strong>
       <ul>
         <li>上传文件需为 .xls 或 .xlsx 格式;</li>
         <li>Excel 第一行视为字段名;</li>
         <li>文件大小不得超过 50MB;</li>
         <li>日期格式为 yyyy-MM-dd,例如:2020-01-01;</li>
         <li>日期时间格式为 yyyy-MM-dd HH:mm:ss,例如:2020-01-01 09:05:01;</li>
       </ul>
     </div>
   </div>
 </el-upload>
 <template #footer>
   <div class="dialog-footer">
     <el-button @click="importExcelWindows = false">取消</el-button>
     <el-button type="primary" @click="importExcelSubmit">导入</el-button>
   </div>
 </template>
</el-dialog>
.fileTip {
 padding-top: 15px;
}

2.2 逻辑处理

data() {
 return {
   importExcelWindows: false,
};
},
methods: {
 importExcel() {
   this.importExcelWindows = true;
},
 uploadFile(param) {
   console.log(param); // param 包含上传的文件对象
},
 importExcelSubmit() {
   this.$refs.uploadRef.submit();
},
}

3. 后端实现

后端基于 Spring Boot 和 EasyExcel,通过多层架构(Model、Listener、Controller、Service、Mapper)实现 Excel 文件解析与数据存储。

3.1 Model 层

3.1.1 数据模型

定义 TClue 实体类,对应数据库表 t_clue,使用 @ExcelProperty 注解映射 Excel 表头与字段,使用 @ExcelIgnoreUnannotated 忽略未注解字段:

package com.sangui.model;

import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.sangui.config.converter.*;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

@ExcelIgnoreUnannotated
@Data
public class TClue implements Serializable {
   private Integer id;
   @ExcelProperty("负责人")
   private Integer ownerId;
   @ExcelProperty("所属活动")
   private Integer activityId;
   @ExcelProperty("姓名")
   private String fullName;
   @ExcelProperty(value = "称呼", converter = AppellationConverter.class)
   private Integer appellation;
   @ExcelProperty("手机号")
   private String phone;
   @ExcelProperty("微信号")
   private String weixin;
   @ExcelProperty("QQ号")
   private String qq;
   @ExcelProperty("邮箱")
   private String email;
   @ExcelProperty("年龄")
   private Integer age;
   @ExcelProperty("职业")
   private String job;
   @ExcelProperty("年收入")
   private BigDecimal yearIncome;
   @ExcelProperty("地址")
   private String address;
   @ExcelProperty(value = "是否贷款", converter = NeedLoanConverter.class)
   private Integer needLoan;
   @ExcelProperty(value = "意向状态", converter = IntentionStateConverter.class)
   private Integer intentionState;
   @ExcelProperty(value = "意向产品", converter = IntentionProductConverter.class)
   private Integer intentionProduct;
   @ExcelProperty(value = "线索状态", converter = StateConverter.class)
   private Integer state;
   @ExcelProperty(value = "线索来源", converter = SourceConverter.class)
   private Integer source;
   @ExcelProperty("线索描述")
   private String description;
   @ExcelProperty("下次联系时间")
   private Date nextContactTime;
   private Date createTime;
   private Integer createBy;
   private Date editTime;
   private Integer editBy;
   private static final long serialVersionUID = 1L;

   // 关联对象
   private TUser ownerDo;
   private TActivity activityDo;
   private TDicValue appellationDO;
   private TDicValue needLoanDO;
   private TDicValue intentionStateDO;
   private TProduct intentionProductDO;
   private TDicValue stateDO;
   private TDicValue sourceDO;
   private TUser createDo;
   private TUser editDo;
}
3.1.2 类型转换器

为处理 Excel 文本与 Java 类型不一致的字段(如“先生”转为整数 ID),实现自定义转换器。例如:

package com.sangui.config.converter;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.sangui.ShzServerApplication;
import com.sangui.model.TDicValue;
import com.sangui.result.DicEnum;

import java.util.List;

public class AppellationConverter implements Converter<Integer> {
   @Override
   public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
       String cellAppellationName = cellData.getStringValue();
       List<TDicValue> tDicValueList = (List<TDicValue>) ShzServerApplication.cacheMap.get(DicEnum.APPELLATION.getCode());
       for (TDicValue tDicValue : tDicValueList) {
           if (cellAppellationName.equals(tDicValue.getTypeValue())) {
               return tDicValue.getId();
          }
      }
       return -1;
  }
}

类似地,IntentionStateConverterIntentionProductConverter 处理其他字段的转换,确保数据准确映射。

补充说明:转换器通过缓存的字典数据(如 TDicValueTProduct)实现文本到 ID 的映射,避免直接数据库查询,提高性能。

3.2 Listener 层

通过 UploadDataListener 监听 Excel 解析过程,批量保存数据以优化性能:

package com.sangui.config.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.sangui.mapper.TClueMapper;
import com.sangui.model.TClue;
import com.sangui.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;
import java.util.List;

@Slf4j
public class UploadDataListener implements ReadListener<TClue> {
   private static final int BATCH_COUNT = 100;
   private List<TClue> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
   private TClueMapper tClueMapper;
   private Integer userId;

   public UploadDataListener(TClueMapper tClueMapper, Integer userId) {
       this.tClueMapper = tClueMapper;
       this.userId = userId;
  }

   @Override
   public void invoke(TClue tClue, AnalysisContext context) {
       log.info("读取到数据: {}", JsonUtils.toJson(tClue));
       tClue.setCreateTime(new Date());
       tClue.setCreateBy(userId);
       cachedDataList.add(tClue);
       if (cachedDataList.size() >= BATCH_COUNT) {
           saveData();
           cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
      }
  }

   @Override
   public void doAfterAllAnalysed(AnalysisContext context) {
       saveData();
       log.info("所有数据解析完成!");
  }

   private void saveData() {
       log.info("存储 {} 条数据到数据库", cachedDataList.size());
       tClueMapper.saveClue(cachedDataList);
       log.info("存储成功!");
  }
}

3.3 Controller 层

接收前端上传的文件并调用服务层处理:

@PostMapping("/api/import-excel")
public R importExcel(@RequestParam("file") MultipartFile file, Authentication authentication) throws IOException {
   clueService.uploadFile(file.getInputStream(), authentication);
   return R.ok();
}

3.4 Service 层

ClueServiceImpl 使用 EasyExcel 解析文件并调用监听器:

@Override
public void uploadFile(InputStream inputStream, Authentication authentication) {
   TUser user = (TUser) authentication.getPrincipal();
   Integer userId = user.getId();
   EasyExcel.read(inputStream, TClue.class, new UploadDataListener(tClueMapper, userId)).sheet().doRead();
}

3.5 Mapper 层

批量插入数据到数据库:

void saveClue(List<TClue> cachedDataList);
<insert id="saveClue" keyColumn="id" keyProperty="id" parameterType="com.sangui.model.TClue" useGeneratedKeys="true">
insert into t_clue (owner_id, activity_id, full_name, appellation, phone, weixin, qq, email, age, job,
year_income, address, need_loan, intention_state, intention_product, state, source,
description, next_contact_time, create_time, create_by, edit_time, edit_by)
values
<foreach collection="tClueList" item="tClue" separator=",">
(#{tClue.ownerId,jdbcType=INTEGER}, #{tClue.activityId,jdbcType=INTEGER}, #{tClue.fullNameJimmy
fullName,jdbcType=VARCHAR}, #{tClue.appellation,jdbcType=INTEGER}, #{tClue.phone,jdbcType=VARCHAR},
#{tClue.weixin,jdbcType=VARCHAR}, #{tClue.qq,jdbcType=VARCHAR}, #{tClue.email,jdbcType=VARCHAR},
#{tClue.age,jdbcType=INTEGER}, #{tClue.job,jdbcType=VARCHAR}, #{tClue.yearIncome,jdbcType=DECIMAL},
#{tClue.address,jdbcType=VARCHAR}, #{tClue.needLoan,jdbcType=INTEGER}, #{tClue.intentionState,jdbcType=INTEGER},
#{tClue.intentionProduct,jdbcType=INTEGER}, #{tClue.state,jdbcType=INTEGER}, #{tClue.source,jdbcType=INTEGER},
#{tClue.description,jdbcType=VARCHAR}, #{tClue.nextContactTime,jdbcType=TIMESTAMP},
#{tClue.createTime,jdbcType=TIMESTAMP}, #{tClue.createBy,jdbcType=INTEGER}, #{tClue.editTime,jdbcType=TIMESTAMP},
#{tClue.editBy,jdbcType=INTEGER})
</foreach>
</insert>

4. 导入流程总结

  1. 前端通过 <el-upload> 上传 Excel 文件,触发 POST 请求。

  2. 后端接收文件流,使用 EasyExcel 解析为 TClue 对象。

  3. 自定义转换器处理字段类型不一致问题。

  4. UploadDataListener 监听解析过程,批量保存数据到数据库。

二、Excel 数据导出

1. 需求概述

前端发送 GET 请求到 /api/exportExcel,后端查询数据库,生成 Excel 文件并返回给浏览器下载,无需参数。

2. 前端实现

<el-button type="primary" @click="allExportExcel">所有导出(Excel)</el-button>
allExportExcel() {
let iframe = document.createElement("iframe");
iframe.src = axios.defaults.baseURL + "/api/exportExcel?Authorization=" + getToken();
document.body.appendChild(iframe);
}

3. 后端实现

3.1 Model 层

定义 CustomerExcel 模型,映射导出所需的表头字段:

package com.sangui.result;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class CustomerExcel {
@ExcelProperty("所属人")
private String ownerName;
@ExcelProperty("所属活动")
private String activityName;
@ExcelProperty("客户姓名")
private String fullName;
@ExcelProperty("客户称呼")
private String appellationTypeValue;
@ExcelProperty("客户手机")
private String phone;
@ExcelProperty("客户微信")
private String weixin;
@ExcelProperty("客户QQ")
private String qq;
@ExcelProperty("客户邮箱")
private String email;
@ExcelProperty("客户年龄")
private int age;
@ExcelProperty("客户职业")
private String job;
@ExcelProperty("客户年收入")
private BigDecimal yearIncome;
@ExcelProperty("客户住址")
private String address;
@ExcelProperty("是否贷款")
private String needLoadTypeValue;
@ExcelProperty("客户产品")
private String productName;
@ExcelProperty("客户来源")
private String sourceTypeValue;
@ExcelProperty("客户描述")
private String description;
@ExcelProperty("下次联系时间")
private Date nextContactTime;
}

3.2 Controller 层

设置响应头,触发浏览器下载 Excel 文件:

@GetMapping("/api/exportExcel")
public void exportExcel(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String fileName = URLEncoder.encode("顾客表格_" + sdf.format(new Date()), "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
List<CustomerExcel> dataList = customerService.getCustomersByExcel();
EasyExcel.write(response.getOutputStream(), CustomerExcel.class).sheet().doWrite(dataList);
}

3.3 Service 层

查询数据库并转换为 CustomerExcel 对象列表:

@Override
public List<CustomerExcel> getCustomersByExcel() {
List<CustomerExcel> res = new ArrayList<>();
List<TCustomer> customerList = customerMapper.selectCustomersByExcel();
customerList.forEach(customer -> {
CustomerExcel customerExcel = new CustomerExcel();
customerExcel.setOwnerName(ObjectUtils.isEmpty(customer.getOwnerDO()) ? "" : customer.getOwnerDO().getName());
customerExcel.setActivityName(ObjectUtils.isEmpty(customer.getActivityDO()) ? "" : customer.getActivityDO().getName());
customerExcel.setFullName(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getFullName());
customerExcel.setAppellationTypeValue(ObjectUtils.isEmpty(customer.getAppellationDO()) ? "" : customer.getAppellationDO().getTypeValue());
customerExcel.setPhone(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getPhone());
customerExcel.setWeixin(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getWeixin());
customerExcel.setQq(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getQq());
customerExcel.setEmail(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getEmail());
customerExcel.setAge(ObjectUtils.isEmpty(customer.getClueDO()) ? 0 : customer.getClueDO().getAge());
customerExcel.setJob(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getJob());
customerExcel.setYearIncome(ObjectUtils.isEmpty(customer.getClueDO()) ? null : customer.getClueDO().getYearIncome());
customerExcel.setAddress(ObjectUtils.isEmpty(customer.getClueDO()) ? "" : customer.getClueDO().getAddress());
customerExcel.setNeedLoadTypeValue(ObjectUtils.isEmpty(customer.getNeedLoanDO()) ? "" : customer.getNeedLoanDO().getTypeValue());
customerExcel.setProductName(ObjectUtils.isEmpty(customer.getProductDO()) ? "" : customer.getProductDO().getName());
customerExcel.setSourceTypeValue(ObjectUtils.isEmpty(customer.getSourceDO()) ? "" : customer.getSourceDO().getTypeValue());
customerExcel.setDescription(ObjectUtils.isEmpty(customer.getDescription()) ? "" : customer.getDescription());
customerExcel.setNextContactTime(customer.getNextContactTime());
res.add(customerExcel);
});
return res;
}

3.4 Mapper 层

复杂 SQL 查询,关联多表获取完整数据:

<select id="selectCustomersByExcel" resultMap="selectCustomersByPageMap">
select tcu.*,
tcl.id clueId,
tcl.full_name clueFullName,
tcl.phone cluePhone,
tcl.weixin clueWeixin,
tcl.job clueJob,
tcl.qq clueQq,
tcl.email clueEmail,
tcl.age clueAge,
tcl.year_income clueYearIncome,
tcl.address clueAddress,
tcl.description clueDescription,
tu1.id ownerId,
tu1.name ownerName,
tu2.id createId,
tu2.name createName,
tu3.id editId,
tu3.name editName,
ta.id activityId,
ta.name activityName,
tdv1.id appellationId,
tdv1.type_value appellationTypeValue,
tdv2.id needLoanId,
tdv2.type_value needLoanTypeValue,
tdv3.id intentionStateId,
tdv3.type_value intentionStateTypeValue,
tdv4.id stateId,
tdv4.type_value stateTypeValue,
tdv5.id sourceId,
tdv5.type_value sourceTypeValue,
tp.id productId,
tp.name productName
from t_customer tcu
left join t_clue tcl on tcu.clue_id = tcl.id
left join t_user tu1 on tcl.owner_id = tu1.id
left join t_user tu2 on tcl.create_by = tu2.id
left join t_user tu3 on tcl.edit_by = tu3.id
left join t_activity ta on tcl.activity_id = ta.id
left join t_dic_value tdv1 on tcl.appellation = tdv1.id
left join t_dic_value tdv2 on tcl.need_loan = tdv2.id
left join t_dic_value tdv3 on tcl.intention_state = tdv3.id
left join t_dic_value tdv4 on tcl.state = tdv4.id
left join t_dic_value tdv5 on tcl.source = tdv5.id
left join t_product tp on tcu.product = tp.id
</select>

4. 导出流程总结

  1. 前端通过 iframe 触发 GET 请求。

  2. 后端查询数据库,转换为 CustomerExcel 对象列表。

  3. 使用 EasyExcel 生成 Excel 文件并输出到浏览器,自动触发下载。

  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录