【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());
        }
    }
相关推荐
L.EscaRC3 小时前
Lua语言知识与应用解析
java·python·lua
S7777777S3 小时前
easyExcel单元格动态合并示例
java·excel
不见长安在3 小时前
redis集群下如何使用lua脚本
数据库·redis·lua
可观测性用观测云3 小时前
阿里云 RDS PostgreSQL 可观测最佳实践
数据库
刘个Java3 小时前
对接大疆上云api---实现直播效果
java
用户9545156811623 小时前
== 和 equals 区别及使用方法组件封装方法
java
馨谙3 小时前
SELinux 文件上下文管理详解:从基础到实战
jvm·数据库·oracle
hashiqimiya3 小时前
html的input的required
java·前端·html
ClouGence3 小时前
百草味数据架构升级实践:打造 Always Ready 的企业级数据平台
大数据·数据库·数据分析