【Java】EasyExcel实现导入导出数据库中的数据为Excel
最近有一些Excel的导入导出的需求要做。
这里直接贴出代码,按照代码去执行就行了,因为现在的ai工具比较多,所以不太需要繁文缛节去介绍如何使用,直接想要用的可以自己根据代码就搜到教程了。
首先导入EasyExcel
<!-- EasyExcel --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version></dependency>
编写你的Excel实体类
package com.ecard.dto;import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.math.BigDecimal;/*** 卡片库存Excel导入导出DTO*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CardStockExcelDTO {@ExcelProperty(value = "卡ID", index = 0)private Long cardId;@ExcelProperty(value = "卡种名称", index = 1)private String cardTypeName;@ExcelProperty(value = "国家代码", index = 2)private String countryCode;@ExcelProperty(value = "是否区分卡类型(0=否,1=是)", index = 3)private String distinguishVariant;@ExcelProperty(value = "卡类型名称", index = 4)private String cardVariantName;@ExcelProperty(value = "价格区间", index = 5)private String priceRange;@ExcelProperty(value = "最小价格", index = 6)private BigDecimal minPrice;@ExcelProperty(value = "最大价格", index = 7)private BigDecimal maxPrice;@ExcelProperty(value = "价格区间描述", index = 8)private String priceRangeDesc;@ExcelProperty(value = "出卡价格", index = 9)private BigDecimal cardIssuancePrice;@ExcelProperty(value = "扣除点数", index = 10)private BigDecimal deductPoints;@ExcelProperty(value = "状态(0=启用,1=禁用)", index = 11)private Integer status;@ExcelProperty(value = "价格规则ID", index = 12)private Long amountRuleId;@ExcelProperty(value = "卡种图片链接", index = 13)private String pictureLink;@ExcelProperty(value = "虚拟客服头像", index = 14)private String virtualCustomerAvatar;@ExcelProperty(value = "虚拟客服名字", index = 15)private String virtualCustomerName;@ExcelProperty(value = "虚拟客服标签", index = 16)private String virtualCustomerTags;@ExcelProperty(value = "备注", index = 17)private String remark;// 用于存储Excel行号,不导出到Excel@ExcelIgnoreprivate Integer excelRowNum;
}
package com.ecard.dto;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.List;/*** 卡片库存Excel导入结果DTO*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CardStockImportResultDTO {/*** 总行数(不含表头)*/private Integer totalRows;/*** 成功数量(新增 + 更新)*/private Integer successCount;/*** 新增数量*/private Integer newCount;/*** 更新数量*/private Integer updateCount;/*** 失败数量*/private Integer failCount;/*** 错误详情列表*/@Builder.Defaultprivate List<ImportErrorDetail> errors = new ArrayList<>();/*** 总体结果消息*/private String message;/*** 是否全部成功*/public boolean isAllSuccess() {return failCount == 0;}/*** 构建结果消息*/public void buildMessage() {if (isAllSuccess()) {this.message = String.format("导入成功!共%d条数据,新增%d条,更新%d条", totalRows, newCount, updateCount);} else {this.message = String.format("导入完成!成功%d条(新增%d条,更新%d条),失败%d条", successCount, newCount, updateCount, failCount);}}
}
package com.ecard.dto;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** Excel导入错误详情*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ImportErrorDetail {/*** Excel中的行号(从第2行开始,第1行是表头)*/private Integer excelRowNum;/*** 卡种名称(方便定位)*/private String cardTypeName;/*** 国家代码(方便定位)*/private String countryCode;/*** 出错的字段名*/private String errorField;/*** 错误描述*/private String errorMessage;/*** 该行的卡ID(如果有)*/private Long cardId;
}
编写业务处理逻辑
/*** Excel导入卡片库存数据*/@Overridepublic CardStockImportResultDTO importFromExcel(MultipartFile file, String operator) throws IOException {if (file == null || file.isEmpty()) {throw new ServiceException("上传文件不能为空");}// 校验文件类型String fileName = file.getOriginalFilename();if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {throw new ServiceException("只支持Excel文件格式(.xlsx 或 .xls)");}// 创建监听器CardStockExcelListener listener = new CardStockExcelListener(cardStockMapper, operator);try {// 读取ExcelEasyExcel.read(file.getInputStream(), CardStockExcelDTO.class, listener).sheet().doRead();// 构建返回结果CardStockImportResultDTO result = CardStockImportResultDTO.builder().totalRows(listener.getTotalRows()).successCount(listener.getSuccessCount()).newCount(listener.getNewCount()).updateCount(listener.getUpdateCount()).failCount(listener.getFailCount()).errors(listener.getErrors()).build();result.buildMessage();return result;} catch (Exception e) {throw new ServiceException("Excel文件解析失败:" + e.getMessage());}}/*** Excel导出卡片库存数据*/@Overridepublic void exportToExcel(HttpServletResponse response) throws IOException {// 查询所有卡片库存数据LambdaQueryWrapper<CardStock> lqw = new LambdaQueryWrapper<>();lqw.eq(CardStock::getDel, 0); // 只导出未删除的数据lqw.orderByDesc(CardStock::getUpdateTime);List<CardStock> cardStockList = cardStockMapper.selectList(lqw);// 转换为Excel DTOList<CardStockExcelDTO> excelDataList = new ArrayList<>();for (CardStock cardStock : cardStockList) {CardStockExcelDTO excelDTO = CardStockExcelDTO.builder().cardId(cardStock.getCardId()).cardTypeName(cardStock.getCardTypeName()).countryCode(cardStock.getCountryCode()).distinguishVariant(cardStock.getDistinguishVariant()).cardVariantName(cardStock.getCardVariantName()).priceRange(cardStock.getPriceRange()).minPrice(cardStock.getMinPrice()).maxPrice(cardStock.getMaxPrice()).priceRangeDesc(cardStock.getPriceRangeDesc()).cardIssuancePrice(cardStock.getCardIssuancePrice()).deductPoints(cardStock.getDeductPoints()).status(cardStock.getStatus()).amountRuleId(cardStock.getAmountRuleId()).pictureLink(cardStock.getPictureLink()).virtualCustomerAvatar(cardStock.getVirtualCustomerAvatar()).virtualCustomerName(cardStock.getVirtualCustomerName()).virtualCustomerTags(cardStock.getVirtualCustomerTags()).remark(cardStock.getRemark()).build();excelDataList.add(excelDTO);}// 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String encodedFileName = URLEncoder.encode("卡片库存数据_" + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");// 写入ExcelEasyExcel.write(response.getOutputStream(), CardStockExcelDTO.class).sheet("卡片库存").doWrite(excelDataList);}/*** 下载Excel模板*/@Overridepublic void downloadTemplate(HttpServletResponse response) throws IOException {// 创建模板数据(包含一条示例)List<CardStockExcelDTO> templateData = new ArrayList<>();CardStockExcelDTO example = CardStockExcelDTO.builder().cardId(null) // 新增时留空,更新时填写.cardTypeName("Steam").countryCode("US").distinguishVariant("0").cardVariantName("E-code").priceRange("10-200").minPrice(new BigDecimal("10.00")).maxPrice(new BigDecimal("200.00")).priceRangeDesc("10-200").cardIssuancePrice(new BigDecimal("5.22")).deductPoints(new BigDecimal("0.20")).status(0).amountRuleId(3L).pictureLink("https://example.com/image.png").virtualCustomerAvatar("").virtualCustomerName("Wendy").virtualCustomerTags("金牌交易员,热情").remark("示例数据").build();templateData.add(example);// 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String encodedFileName = URLEncoder.encode("卡片库存导入模板", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");// 写入ExcelEasyExcel.write(response.getOutputStream(), CardStockExcelDTO.class).sheet("卡片库存").doWrite(templateData);}
编写EasyExcel解析处理器
package com.ecard.utils;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.ecard.dto.CardStockExcelDTO;
import com.ecard.dto.ImportErrorDetail;
import com.ecard.entity.CardStock;
import com.ecard.mapper.CardStockMapper;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;/*** CardStock Excel导入监听器*/
@Slf4j
@Getter
public class CardStockExcelListener extends AnalysisEventListener<CardStockExcelDTO> {private final CardStockMapper cardStockMapper;private final String operator;// 统计信息private int totalRows = 0;private int successCount = 0;private int newCount = 0;private int updateCount = 0;private int failCount = 0;// 错误详情列表private final List<ImportErrorDetail> errors = new ArrayList<>();public CardStockExcelListener(CardStockMapper cardStockMapper, String operator) {this.cardStockMapper = cardStockMapper;this.operator = operator != null ? operator : "system";}/*** 每解析一行数据都会调用此方法*/@Overridepublic void invoke(CardStockExcelDTO data, AnalysisContext context) {// 获取Excel行号(从1开始,但第1行是表头,数据从第2行开始)Integer excelRowNum = context.readRowHolder().getRowIndex() + 1;data.setExcelRowNum(excelRowNum);totalRows++;try {// 数据校验validateData(data);// 转换为实体对象CardStock cardStock = convertToEntity(data);// 判断是新增还是更新if (data.getCardId() != null) {CardStock existingCard = cardStockMapper.selectById(data.getCardId());if (existingCard != null) {// 更新cardStock.setCardId(data.getCardId());cardStock.setUpdateBy(operator);cardStock.setUpdateTime(new Date());cardStockMapper.updateById(cardStock);updateCount++;} else {// cardId存在但数据库中没有,视为新增cardStock.setCardId(data.getCardId());cardStock.setCreateBy(operator);cardStock.setCreateTime(new Date());cardStock.setUpdateBy(operator);cardStock.setUpdateTime(new Date());cardStockMapper.insert(cardStock);newCount++;}} else {// cardId为空,新增cardStock.setCreateBy(operator);cardStock.setCreateTime(new Date());cardStock.setUpdateBy(operator);cardStock.setUpdateTime(new Date());cardStockMapper.insert(cardStock);newCount++;}successCount++;} catch (Exception e) {// 记录错误log.error("Excel第{}行数据处理失败: {}", excelRowNum, e.getMessage(), e);ImportErrorDetail error = ImportErrorDetail.builder().excelRowNum(excelRowNum).cardId(data.getCardId()).cardTypeName(data.getCardTypeName()).countryCode(data.getCountryCode()).errorMessage(e.getMessage()).build();errors.add(error);failCount++;}}/*** 所有数据解析完成后调用*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {log.info("Excel数据解析完成,总行数: {}, 成功: {}, 失败: {}", totalRows, successCount, failCount);}/*** 数据校验*/private void validateData(CardStockExcelDTO data) {Integer rowNum = data.getExcelRowNum();// 必填字段校验if (isBlank(data.getCardTypeName())) {throw new RuntimeException(String.format("第%d行:卡种名称不能为空", rowNum));}if (isBlank(data.getCountryCode())) {throw new RuntimeException(String.format("第%d行:国家代码不能为空", rowNum));}if (data.getMinPrice() == null) {throw new RuntimeException(String.format("第%d行:最小价格不能为空", rowNum));}if (data.getMaxPrice() == null) {throw new RuntimeException(String.format("第%d行:最大价格不能为空", rowNum));}if (data.getCardIssuancePrice() == null) {throw new RuntimeException(String.format("第%d行:出卡价格不能为空", rowNum));}if (data.getDeductPoints() == null) {throw new RuntimeException(String.format("第%d行:扣除点数不能为空", rowNum));}// 数据格式校验if (data.getMinPrice().compareTo(BigDecimal.ZERO) < 0) {throw new RuntimeException(String.format("第%d行:最小价格不能为负数", rowNum));}if (data.getMaxPrice().compareTo(BigDecimal.ZERO) < 0) {throw new RuntimeException(String.format("第%d行:最大价格不能为负数", rowNum));}if (data.getCardIssuancePrice().compareTo(BigDecimal.ZERO) < 0) {throw new RuntimeException(String.format("第%d行:出卡价格不能为负数", rowNum));}if (data.getDeductPoints().compareTo(BigDecimal.ZERO) < 0) {throw new RuntimeException(String.format("第%d行:扣除点数不能为负数", rowNum));}// 业务规则校验if (data.getMaxPrice().compareTo(data.getMinPrice()) < 0) {throw new RuntimeException(String.format("第%d行:最大价格(%s)不能小于最小价格(%s)", rowNum, data.getMaxPrice(), data.getMinPrice()));}// 状态值校验if (data.getStatus() != null && data.getStatus() != 0 && data.getStatus() != 1) {throw new RuntimeException(String.format("第%d行:状态值必须是0(启用)或1(禁用)", rowNum));}// 区分卡类型校验if (data.getDistinguishVariant() != null && !"0".equals(data.getDistinguishVariant()) && !"1".equals(data.getDistinguishVariant())) {throw new RuntimeException(String.format("第%d行:是否区分卡类型必须是0或1", rowNum));}}/*** 转换为实体对象*/private CardStock convertToEntity(CardStockExcelDTO dto) {CardStock entity = new CardStock();BeanUtils.copyProperties(dto, entity);// 设置默认值if (entity.getStatus() == null) {entity.setStatus(0); // 默认启用}if (entity.getDel() == null) {entity.setDel(0); // 默认未删除}if (isBlank(entity.getDistinguishVariant())) {entity.setDistinguishVariant("0"); // 默认不区分}return entity;}/*** 判断字符串是否为空*/private boolean isBlank(String str) {return str == null || str.trim().isEmpty();}
}
编写Controller
/*** Excel批量导入卡片库存* @param file Excel文件* @return 导入结果统计*/@PostMapping("/import")public Result<CardStockImportResultDTO> importFromExcel(@RequestParam("file") MultipartFile file) {try {// 获取当前操作人,如果未登录则使用"admin"String operator = "admin";try {Long userId = UserContextInfo.getUserId();if (userId != null) {operator = "user_" + userId;}} catch (Exception ignored) {// 如果获取用户信息失败,使用默认值}CardStockImportResultDTO result = cardStockService.importFromExcel(file, operator);if (result.isAllSuccess()) {return Result.ok(result, result.getMessage());} else {// 部分成功,返回200但附带错误详情return Result.ok(result, result.getMessage());}} catch (IOException e) {return Result.fail("文件读取失败:" + e.getMessage());} catch (Exception e) {return Result.fail("导入失败:" + e.getMessage());}}/*** Excel导出卡片库存数据*/@GetMapping("/export")public void exportToExcel(HttpServletResponse response) {try {cardStockService.exportToExcel(response);} catch (IOException e) {throw new RuntimeException("导出失败:" + e.getMessage());}}/*** 下载Excel导入模板*/@GetMapping("/download-template")public void downloadTemplate(HttpServletResponse response) {try {cardStockService.downloadTemplate(response);} catch (IOException e) {throw new RuntimeException("模板下载失败:" + e.getMessage());}}
