最近有一些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
@ExcelIgnore
private 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.Default
private 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导入卡片库存数据
*/
@Override
public 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 {
// 读取Excel
EasyExcel.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导出卡片库存数据
*/
@Override
public 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 DTO
List<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");
// 写入Excel
EasyExcel.write(response.getOutputStream(), CardStockExcelDTO.class)
.sheet("卡片库存")
.doWrite(excelDataList);
}
/**
* 下载Excel模板
*/
@Override
public 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");
// 写入Excel
EasyExcel.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";
}
/**
* 每解析一行数据都会调用此方法
*/
@Override
public 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++;
}
}
/**
* 所有数据解析完成后调用
*/
@Override
public 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());
}
}