easyExcel文件导入的实际应用

需要实现的功能

参考exchange项目
备注:2024-05-15修改
1、修复成功页和失败页单独记录序号
2、完善业务层代码,入参修改为实体类集合
3、新增mapper批量插入脚本

在excel中录入文件并导入到数据库

要求

1、文件内容去重,包括文件内容去重和数据库去重
2、实时查看文件导入状态
3、生成文件导入结果文件,便于查错

controller层写法

 @Override
    public void importFile(MultipartFile file, ChargeListReqVO reqVO) throws Exception {
        // 判断是否有进行中的任务
        TImpNorealFilterTaskCriteria taskCriteria = new TImpNorealFilterTaskCriteria();
        taskCriteria.createCriteria()
                .andStatusEqualTo(FILE_IMPORT_STATUS.ZERO.getCode());
        long count = filterTaskMapper.countByExample(taskCriteria);
        if (count > 0) {
            throw new ValidateException(CoreConstant.TRANS_STATUS__FAILED, "存在导入中的文件");
        }

        // 基本信息校验
        if (null == file) {
            throw new ValidateException(CoreConstant.TRANS_STATUS__FAILED, "请选择文件后再执行导入!");
        }

        String originalFilename = file.getOriginalFilename();
        String extname = originalFilename.substring(originalFilename.lastIndexOf("."));
        if (!".xlsx".equalsIgnoreCase(extname)) {
            throw new ValidateException(CoreConstant.TRANS_STATUS__FAILED, "仅支持.xlsx格式文件导入,请重新选择文件后再执行导入!");
        }

        String organCode = reqVO.getOrganCode();
        if (StringUtils.isBlank(organCode)) {
            throw new ValidateException(CoreConstant.TRANS_STATUS__FAILED, "机构编码不能为空");
        }

        // 公共变量定义,即主键id生成策略
        String impBatchNo = commonMapper.generatePK().toString();

        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd/");
        String formatPath = dateFormat.format(new Date());

        String loginName = reqVO.getHead().getLoginName();


        // 应该从yaml配置文件中读取
        // String originalFilePath="/data/share/static/exchange/charge/";

        // 原始文件放置
        String filePath = originalFilePath + formatPath;
        String fileName = file.getOriginalFilename();
        int lastIndexOfDot = originalFilename.lastIndexOf('.');
        if (lastIndexOfDot == -1) {
            fileName = fileName; // 没有后缀
        }
        fileName = originalFilename.substring(0, lastIndexOfDot);
        fileName = fileName + "_" + commonMapper.generatePK();

        String fileFinalPath = filePath + fileName;

        Path path = Paths.get(fileFinalPath + extname);
        File fileTmp = path.toFile();

        File parentDir = fileTmp.getParentFile();
        if (!parentDir.exists()) {
            parentDir.mkdirs();
        }

        if (!fileTmp.exists()) {
            try {
                fileTmp.createNewFile();
            } catch (IOException var) {
                log.error("结果文件创建失败:{}", var.getMessage());
            }
        }

        try {
            file.transferTo(fileTmp);
        } catch (Exception e) {
            log.error("原始文件放置失败:{}", e.getMessage());
        }

        TImpNorealFilterTask filterTask = new TImpNorealFilterTask();
        filterTask.setImpBatchNo(impBatchNo);
        filterTask.setUserName(loginName);
        String pathTmp = fileFinalPath + extname;
        filterTask.setSourceFile(pathTmp.replace("/data/share/",""));
        Date insertTime = new Date();
        filterTask.setInsertTime(insertTime);
        filterTask.setStartTime(insertTime);
        filterTask.setInsertBy(loginName);
        filterTask.setOrganCode(organCode);
        filterTask.setStatus(FILE_IMPORT_STATUS.ZERO.getCode());
        log.info("数据插入数据库");

        // 异步导入文件
        asyncChargeService.asyncImportFile(file, impBatchNo, loginName, fileFinalPath);
    }

业务层写法

/**
 * @author wangtuanyuan
 * @date: 2023/12/11 011 16:39
 * @description:
 */
@Service
@Async
@Slf4j
@RequiredArgsConstructor
public class AsyncChargeServiceImpl implements AsyncChargeService {

    /**
     * @author wangtuanyuan
     * @date 2023/12/11 16:40
     * @description
     **/
    public void asyncImportFile(InputStream inputStream, Long batchId) throws IOException {
        List<CustDetailDTO> list = new ArrayList<>();

        EasyExcel.read(inputStream, CustDetailDTO.class, new AnalysisEventListener<CustDetailDTO>() {
            @SneakyThrows
            @Override
            public void invoke(CustDetailDTO data, AnalysisContext analysisContext) {
                // 读取时,会读取到一个实体对象,但是所有属性都为空,需要排除这个对象
                boolean fieldNull = isAllFieldNull(data);

                if (!fieldNull) {
                    list.add(data);
                }
            }

            @SneakyThrows
            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                batchImportSave(list, batchId);
            }
        }).sheet(0).doRead();
    }

    private void batchImportSave(List<CustDetailDTO> dtoList, Long batchId) {
        // 成功结果数据
        List<SuccStatusDTO> successList = new ArrayList<>();
        // 失败结果数据
        List<FileStatusDTO> failList = new ArrayList<>();
        // 文件校验后需要入库的数据
        List<CustDetailDTO> dbBatchList = new ArrayList<>();
        // excel文件去重处理,利用map进行去重
        Map<String, String> map = new HashMap<>();

        // 总数据量
        int total = dtoList.size();
        // 本次导入的批次号
        final String batchNo = commonMapper.generatePK().toString();

        for (int i = 0; i < total; i++) {
            // 结果文件中的当前行数
            int num = i + 1;
            // 需要入库的数据
            CustDetailDTO custDetailDTO = new CustDetailDTO();

            // 行号,表示当前数据是第几行
            int serialNumber = i + 2;
            CustDetailDTO detailDTO = dtoList.get(i);

            final String custId = detailDTO.getCustId();
            if (StringUtils.isBlank(custId)) {
                FileStatusDTO statusDTO = new FileStatusDTO();
                statusDTO.setSerialNumber(serialNumber)
                        .setNum(num)
                        .setCustId(custId)
                        .setMsg("客户号不应为空");
                failList.add(statusDTO);
                continue;
            } else {
                if (custId.length() > 40) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("该客户号超过40位");
                    failList.add(statusDTO);
                    continue;
                } else {
                    // 查询数据库是否存在
                    final AppCrsBsDetailCriteria criteria = new AppCrsBsDetailCriteria();
                    criteria.createCriteria()
                            .andCustIdEqualTo(custId);
                    final Long count = appCrsBsDetailMapper.countByExample(criteria);
                    if (null == count || 0 == count) {
                        FileStatusDTO statusDTO = new FileStatusDTO();
                        statusDTO.setSerialNumber(serialNumber)
                                .setNum(num)
                                .setCustId(custId)
                                .setMsg("该客户号不存在");
                        failList.add(statusDTO);
                        continue;
                    } else {
                        custDetailDTO.setCustId(custId);
                    }
                }
            }

            if (!map.isEmpty()) {
                String sDate = map.get(custId);
                if (StringUtils.isNotBlank(sDate)) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("该客户号本次已导入");
                    failList.add(statusDTO);
                    continue;
                } else {
                    // 存入公共Map,进行文件去重
                    map.put(custId, custId);
                }
            } else {
                // 存入公共Map,进行文件去重
                map.put(custId, custId);
            }


            // 判断其他项都为空
            final String selfcertification = detailDTO.getSelfcertification();
            final String addressfreeen = detailDTO.getAddressfreeen();
            final String rescountrycode = detailDTO.getRescountrycode();
            final String tin = detailDTO.getTin();
            final String explanation = detailDTO.getExplanation();
            if (StringUtils.isBlank(selfcertification)
                    && StringUtils.isBlank(addressfreeen)
                    && StringUtils.isBlank(rescountrycode)
                    && StringUtils.isBlank(tin)
                    && StringUtils.isBlank(explanation)) {
                FileStatusDTO statusDTO = new FileStatusDTO();
                statusDTO.setSerialNumber(serialNumber)
                        .setNum(num)
                        .setCustId(custId)
                        .setMsg("除客户号,其他信息项至少录入一项");
                failList.add(statusDTO);
                continue;
            }

            if (StringUtils.isNotBlank(selfcertification)) {
                if (!"TRUE,FALSE".contains(selfcertification)) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("是否取得自证声明应为“TRUE”或“FALSE”");
                    failList.add(statusDTO);
                    continue;
                } else {
                    custDetailDTO.setSelfcertification(selfcertification);
                }
            }

            if (StringUtils.isNotBlank(addressfreeen)) {
                if (addressfreeen.length() > 100) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("客户地址_英文长度不应超100字符");
                    failList.add(statusDTO);
                    continue;
                } else {
                    custDetailDTO.setAddressfreeen(addressfreeen);
                }
            }

            if (StringUtils.isNotBlank(rescountrycode)) {
                if (rescountrycode.length() > 100) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("税收居民国长度不应超100字符");
                    failList.add(statusDTO);
                    continue;
                } else {
                    custDetailDTO.setRescountrycode(rescountrycode);
                }
            }


            if (StringUtils.isNotBlank(tin)) {
                if (tin.length() > 100) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("税号长度不应超100字符");
                    failList.add(statusDTO);
                    continue;
                } else {
                    custDetailDTO.setTin(tin);
                }
            }

            if (StringUtils.isNotBlank(explanation)) {
                if (explanation.length() > 100) {
                    FileStatusDTO statusDTO = new FileStatusDTO();
                    statusDTO.setSerialNumber(serialNumber)
                            .setNum(num)
                            .setCustId(custId)
                            .setMsg("未填写税号的原因长度不应超100字符");
                    failList.add(statusDTO);
                    continue;
                } else {
                    custDetailDTO.setExplanation(explanation);
                }
            }

            // 成功结果文件记录
            SuccStatusDTO statusDTO = new SuccStatusDTO();
            statusDTO.setSerialNumber(serialNumber)
                    .setNum(num)
                    .setCustId(custId);
            successList.add(statusDTO);

            // 需要入库的数据
            dbBatchList.add(custDetailDTO);
        }

        // 清空map  GC
        map = null;

        // 批次切片,把大的集合切片成小的集合
        List<List<CustDetailDTO>> splitNList = SplitListUtils.split(dbBatchList, 50);

        final Date date = new Date();
        for (List<CustDetailDTO> split : splitNList) {
            List<TImpCrsCustDetail> detailList = new ArrayList<>();
            for (CustDetailDTO fileStatus : split) {
                TImpCrsCustDetail custDetail = new TImpCrsCustDetail();
                custDetail.setBatchNo(batchNo);
                custDetail.setCustId(fileStatus.getCustId());
                custDetail.setSelfcertification(fileStatus.getSelfcertification());
                custDetail.setAddressfreeen(fileStatus.getAddressfreeen());
                custDetail.setRescountrycode(fileStatus.getRescountrycode());
                custDetail.setTin(fileStatus.getTin());
                custDetail.setExplanation(fileStatus.getExplanation());
                detailList.add(custDetail);
            }
            extImpCrsCustDetailMapper.insertBatch(detailList, date);
        }


        // 结果文件放到服务器
        String resultFile = "/data/share/static/dm-web/crs/result/" + batchNo + "_result.xlsx";
        ExcelWriter excelWriter = null;
        try {
            File dest = new File(resultFile);
            if (!dest.getParentFile().exists()) {
                dest.getParentFile().mkdirs();
            }

            // 结果文件,包含成功和失败
            excelWriter = EasyExcel.write(resultFile).build();

            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
            WriteSheet successSheet = EasyExcel.writerSheet(1, "成功页").head(SuccStatusDTO.class).build();
            WriteSheet failSheet = EasyExcel.writerSheet(2, "失败页").head(FileStatusDTO.class).build();

            // 遍历正确集合,重新修改序号
            for (int i = 0; i < successList.size(); i++) {
                final SuccStatusDTO statusDTO = successList.get(i);
                statusDTO.setNum(i + 1);
            }

            // 遍历错误集合,重新修改序号
            for (int i = 0; i < failList.size(); i++) {
                final FileStatusDTO fileStatusDTO = failList.get(i);
                fileStatusDTO.setNum(i + 1);
            }

            // 正确数据和错误数据进行归类
            excelWriter.write(successList, successSheet);
            excelWriter.write(failList, failSheet);

        } catch (Exception e) {
            log.error("结果文件写出失败:{}", e.getMessage(), e);
        } finally {
            excelWriter.finish();

            int failSize = failList.size();
            Integer successNum = total - failSize;
            String impState = CRS_FILE_IMPORT.STATE_ONE.getCode();

            if ((successNum == total) && (failSize == 0)) {
                // 全部导入成功
                impState = CRS_FILE_IMPORT.STATE_ONE.getCode();
            } else if ((failSize > 0) && (successNum > 0)) {
                // 部分导入成功
                impState = CRS_FILE_IMPORT.STATE_THREE.getCode();
            } else if ((failSize == total) && (successNum == 0)) {
                // 全部导入失败
                impState = CRS_FILE_IMPORT.STATE_TWO.getCode();
            }

            Date now = date;
            TImpCrsCustTask custTask = new TImpCrsCustTask();
            custTask.setBatchId(new BigDecimal(batchId));
            custTask.setState(impState);
            custTask.setImpTotal(new BigDecimal(total));
            custTask.setImpSuccess(new BigDecimal(successNum));
            custTask.setResultPath(resultFile.split("/data/share/")[1]);
            custTask.setEndTime(now);
            custTask.setUpdateTime(now);
            crsCustTaskMapper.updateByPrimaryKeySelective(custTask);
        }
    }

    /**
     * 判断该对象是否所有属性为空
     * 返回ture表示所有属性为null,返回false表示不是所有属性都是null
     */
    public static boolean isAllFieldNull(Object object) {
        boolean flag = true;

        Class<?> clazz = object.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //设置属性是可以访问的(私有的也可以)
            field.setAccessible(true);
            Object value = null;
            try {
                value = field.get(object);
                // 只要有1个属性不为空,那么就不是所有的属性值都为空
                if (value != null) {
                    flag = false;
                    break;
                }
            } catch (Exception e) {
                log.error("文件导入存在空对象异常:{}", e.getMessage());
            }
        }
        return flag;
    }
}

// ===============================================使用到的类===================================================================

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
class SuccStatusDTO {

    @ExcelProperty("序号")
    @ColumnWidth(value = 20)
    private Integer num;

    /**
     * 本条数据在excel原始文件中的行数
     */
    @ExcelProperty("行号")
    @ColumnWidth(value = 20)
    private Integer serialNumber;

    /**
     * 客户号
     */
    @ExcelProperty("客户号")
    @ColumnWidth(value = 20)
    private String custId;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class FileStatusDTO {

    @ExcelProperty("序号")
    @ColumnWidth(value = 20)
    private Integer num;

    /**
     * 本条数据在excel原始文件中的行数
     */
    @ExcelProperty("行号")
    @ColumnWidth(value = 20)
    private Integer serialNumber;

    /**
     * 客户号
     */
    @ExcelProperty("客户号")
    @ColumnWidth(value = 20)
    private String custId;

    /**
     * 错误原因
     */
    @ExcelProperty("错误原因")
    @ColumnWidth(value = 40)
    private String msg;

}

class SplitListUtils {

    /**
     * 拆分集合
     *
     * @param <T> 泛型对象
     * @param resList 需要拆分的集合
     * @param subListLength 每个子集合的元素个数
     * @return 返回拆分后的各个集合组成的列表
     * 代码里面用到了guava和common的结合工具类
     **/
    public static <T> List<List<T>> split(List<T> resList, int subListLength) {
        if (CollectionUtils.isEmpty(resList) || subListLength <= 0) {
            return Lists.newArrayList();
        }
        List<List<T>> ret = Lists.newArrayList();
        int size = resList.size();
        if (size <= subListLength) {
            // 数据量不足 subListLength 指定的大小
            ret.add(resList);
        } else {
            int pre = size / subListLength;
            int last = size % subListLength;
            // 前面pre个集合,每个大小都是 subListLength 个元素
            for (int i = 0; i < pre; i++) {
                List<T> itemList = Lists.newArrayList();
                for (int j = 0; j < subListLength; j++) {
                    itemList.add(resList.get(i * subListLength + j));
                }
                ret.add(itemList);
            }
            // last的进行处理
            if (last > 0) {
                List<T> itemList = Lists.newArrayList();
                for (int i = 0; i < last; i++) {
                    itemList.add(resList.get(pre * subListLength + i));
                }
                ret.add(itemList);
            }
        }
        return ret;
    }
}

补充

excel批量导入的另外一种写法,

 @Async
    public void importFileAsync(MultipartFile file, PrivacyBatchImportReqVO reqVO, Long batchId) throws IOException {
        // 每隔3000条存储数据库,然后清理list,方便内存回收
        final int BATCH_COUNT = 3000;
        List<PrivacyBatchImportDTO> list = new ArrayList<>();

        EasyExcel.read(file.getInputStream(), PrivacyBatchImportDTO.class, new AnalysisEventListener<PrivacyBatchImportDTO>() {
            @SneakyThrows
            @Override
            public void invoke(PrivacyBatchImportDTO data, AnalysisContext analysisContext) {
                // 读取时,会读取到一个实体对象,但是所有属性都为空,需要排除这个对象
                boolean fieldNull = isAllFieldNull(data);

                if (!fieldNull) {
                    list.add(data);

                    if (list.size() >= BATCH_COUNT) {
                        batchImportSave(list, reqVO, batchId);
                        list.clear();
                    }
                }
            }

            @SneakyThrows
            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                batchImportSave(list, reqVO, batchId);
            }
        }).sheet(0).doRead();
    }

批量插入数据写法

 <insert id="insertBatch">
        INSERT INTO CIRC_OTH.T_IMP_CRS_CUST_DETAIL (BATCH_NO,CUST_ID,SELFCERTIFICATION,ADDRESSFREEEN,RESCOUNTRYCODE,TIN,EXPLANATION,LOADTIME,IMPDATE)
        <foreach collection="detailList" item="item"  separator="union all" >
            select
            #{item.batchNo,jdbcType=VARCHAR},
            #{item.custId,jdbcType=VARCHAR},
            #{item.selfcertification,jdbcType=CHAR},
            #{item.addressfreeen,jdbcType=VARCHAR},
            #{item.rescountrycode,jdbcType=VARCHAR},
            #{item.tin,jdbcType=VARCHAR},
            #{item.explanation,jdbcType=VARCHAR},
            #{date,jdbcType=TIMESTAMP},
            #{date,jdbcType=TIMESTAMP}
            from dual
        </foreach>
    </insert>

最终效果展示

需要导入的文件
在这里插入图片描述
文件导入的结果
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值