【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
    @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());
        }
    }
相关推荐
小猿姐4 小时前
实测对比:哪款开源 Kubernetes MySQL Operator 最值得用?(2026 深度评测)
数据库·mysql·云原生
一灯架构5 小时前
90%的人答错!一文带你彻底搞懂ArrayList
java·后端
倔强的石头_6 小时前
从 “存得下” 到 “算得快”:工业物联网需要新一代时序数据平台
数据库
Y4090016 小时前
【多线程】线程安全(1)
java·开发语言·jvm
TDengine (老段)7 小时前
TDengine IDMP 可视化 —— 分享
大数据·数据库·人工智能·时序数据库·tdengine·涛思数据·时序数据
布局呆星7 小时前
SpringBoot 基础入门
java·spring boot·spring
风吹迎面入袖凉7 小时前
【Redis】Redisson的可重入锁原理
java·redis
GottdesKrieges7 小时前
OceanBase数据库备份配置
数据库·oceanbase
w6100104667 小时前
cka-2026-ConfigMap
java·linux·cka·configmap