使用 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;
public class TClue implements Serializable {
private Integer id;
("负责人")
private Integer ownerId;
("所属活动")
private Integer activityId;
("姓名")
private String fullName;
(value = "称呼", converter = AppellationConverter.class)
private Integer appellation;
("手机号")
private String phone;
("微信号")
private String weixin;
("QQ号")
private String qq;
("邮箱")
private String email;
("年龄")
private Integer age;
("职业")
private String job;
("年收入")
private BigDecimal yearIncome;
("地址")
private String address;
(value = "是否贷款", converter = NeedLoanConverter.class)
private Integer needLoan;
(value = "意向状态", converter = IntentionStateConverter.class)
private Integer intentionState;
(value = "意向产品", converter = IntentionProductConverter.class)
private Integer intentionProduct;
(value = "线索状态", converter = StateConverter.class)
private Integer state;
(value = "线索来源", converter = SourceConverter.class)
private Integer source;
("线索描述")
private String description;
("下次联系时间")
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> {
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;
}
}类似地,IntentionStateConverter 和 IntentionProductConverter 处理其他字段的转换,确保数据准确映射。
补充说明:转换器通过缓存的字典数据(如 TDicValue 或 TProduct)实现文本到 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;
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;
}
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);
}
}
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
log.info("所有数据解析完成!");
}
private void saveData() {
log.info("存储 {} 条数据到数据库", cachedDataList.size());
tClueMapper.saveClue(cachedDataList);
log.info("存储成功!");
}
}3.3 Controller 层
接收前端上传的文件并调用服务层处理:
("/api/import-excel")
public R importExcel(("file") MultipartFile file, Authentication authentication) throws IOException {
clueService.uploadFile(file.getInputStream(), authentication);
return R.ok();
}3.4 Service 层
ClueServiceImpl 使用 EasyExcel 解析文件并调用监听器:
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. 导入流程总结
前端通过
<el-upload>上传 Excel 文件,触发 POST 请求。后端接收文件流,使用 EasyExcel 解析为
TClue对象。自定义转换器处理字段类型不一致问题。
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. 导出流程总结
前端通过 iframe 触发 GET 请求。
后端查询数据库,转换为
CustomerExcel对象列表。
- 微信
- 赶快加我聊天吧

- 赶快加我聊天吧
