首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >后台批量导入别再做"整批失败":逐行校验、错误回执与幂等重导的完整实现

后台批量导入别再做"整批失败":逐行校验、错误回执与幂等重导的完整实现

原创
作者头像
用户5598620
发布于 2026-09-17 09:00:49
发布于 2026-09-17 09:00:49
2320
举报

导读:运营同事丢来一份 500 行的报名名单 Excel,点导入后后台提示"导入失败"、整批回滚——排查半天才发现只有第 137 行手机号格式不对。这类"一行出错、全盘皆输"的后台导入,是不少业务系统上线后踩的第一个坑。本文给一套可直接抄走的实现:逐行校验收集错误、分批事务落库、错误清单回执、按批次幂等重导。

一、为什么"校验不过就整批回滚"必然被吐槽

很多系统第一版导入的逻辑是:边读 Excel 边写库,遇到任何一行校验失败就 throw 并回滚整个事务。500 行名单里 1 行出错,用户得到的结果是"导入失败",却不知道错在哪一行、错在哪个字段;改完重传,又可能因为前面已写入的部分数据没有清理干净而重复。

  • 错误定位难:只报"第 N 行错误"不给原因,用户只能肉眼逐行找。
  • 正确数据反复重传:整批回滚后,正确的 499 行也要跟着重新导入一次。
  • 大事务拖累性能:一个事务塞几千条 insert,行锁/间隙锁范围大,在线业务会跟着抖动。

正确的做法是拆成四个阶段:模板校验 → 全量逐行校验(只收错误不写库)→ 分批落库 → 错误回执与幂等重导。

二、第一步:模板与列头校验,先挡住"文件不对"

导入模板要固定列头(如 姓名/手机号/报名场次),并带上版本号。上传后第一件事是拿真实表头与模板逐列比对,列头不匹配直接拒绝,别等解析到一半才发现缺列。

代码语言:java
复制
// 基于 Apache POI:上传后先校验列头,不匹配立即返回可读错误
try (Workbook wb = WorkbookFactory.create(inputStream)) {
    Sheet sheet = wb.getSheetAt(0);
    List<String> expected = Arrays.asList("姓名", "手机号", "报名场次");
    Row header = sheet.getRow(0);
    for (int i = 0; i < expected.size(); i++) {
        String cell = getCellValue(header.getCell(i));
        if (!expected.get(i).equals(cell)) {
            throw new ImportFormatException(
                "模板列头不匹配:第" + (i + 1) + "列应为「" + expected.get(i) + "」");
        }
    }
}

要点:列头比对要在读数据之前完成;模板里不要用"合并单元格+第二行表头"这类复杂结构,解析成本高还容易踩坑。

三、第二步:逐行校验,只收集错误不写库

校验阶段与落库阶段完全解耦。每一行做四类检查:

  • 必填:姓名、手机号为空则报"必填项缺失";
  • 格式:手机号用正则 ^1\d{10}$,场次日期按配置格式解析;
  • 枚举:报名场次必须存在于后台配置的场次表中;
  • 唯一性:文件内重复(手机号+场次相同)、或库内已存在,均提示。

错误统一收集成 行号 + 字段 + 原因 的结构,一行多个错误合并成一条提示,避免用户改一次传一次。

代码语言:java
复制
List<RowError> errors = new ArrayList<>();
List<RowData> validRows = new ArrayList<>();

for (int i = 1; i <= sheet.getLastRowNum(); i++) {
    Row row = sheet.getRow(i);
    if (row == null || isBlankRow(row)) {
        continue; // 跳过空行,别把空行当错误
    }
    RowData data = parseRow(row, i + 1, errors); // 行号=Excel 中的行号,给用户定位用
    if (data != null) {
        validRows.add(data);
    }
}
if (!errors.isEmpty()) {
    // 返回错误清单:含行号、字段、原因,不落任何数据
    return ImportResult.fail(errors);
}

要点:错误先行——只要有任何错误行,整份文件先不落库,等用户按错误清单改完重传。这与"部分成功"并不矛盾:错误行为 0 的文件进入下一阶段后,才允许部分成功(见下一步)。

四、第三步:分批落库,事务边界要小

校验全部通过后进入写入阶段。不要一个文件一个事务,按固定大小(如 200 行/批)分批,每批一个事务:批内失败整批回滚,已提交的批次不回滚,最后返回"成功 n 行、失败 m 行"。

代码语言:java
复制
int batchSize = 200;
int success = 0;
List<BatchError> batchErrors = new ArrayList<>();

for (int from = 0; from < validRows.size(); from += batchSize) {
    List<RowData> batch = validRows.subList(from,
        Math.min(from + batchSize, validRows.size()));
    try {
        importBatch(batch);          // 事务内批量 insert
        success += batch.size();
    } catch (Exception e) {
        batchErrors.add(new BatchError(from + 1, e.getMessage()));
    }
}
代码语言:sql
复制
-- MyBatis 批量插入示例:单批一次执行,减少网络往返
<insert id="batchInsertSignUp" parameterType="list">
    INSERT INTO sign_up (phone, session_id, name, source)
    VALUES
    <foreach collection="list" item="it" separator=",">
        (#{it.phone}, #{it.sessionId}, #{it.name}, 'IMPORT')
    </foreach>
</insert>

要点:分批粒度按业务量调,500 行内可 100–200 行一批;落库前对库内唯一性再做一次批量预查询(WHERE phone IN (...) AND session_id IN (...)),把绝大多数冲突挡在 insert 之前,避免靠异常捕获兜底。批处理建议复用 Spring 的 JdbcTemplate.batchUpdate 或 MyBatis 的 batch executor,逐条 insert 的解析开销在几百行时已能明显感知。

五、第四步:错误回执与幂等重导

用户改完错误行后通常会把整份文件重新上传。这时要防两件事:正确行重复入库、同批文件被重复导入。两个手段:

  1. 业务唯一键:给表加 (phone, session_id) 唯一索引,重导时正确行通过 upsert 语义去重(先查后插,或 INSERT ... ON DUPLICATE KEY UPDATE),不产生重复记录;
  2. 导入批次号:每次上传生成 batch_no,写入导入批次表,文件与批次一一对应;同一批次重复提交直接返回上次结果,不做二次写入。
代码语言:sql
复制
-- 同一场次同一手机号只允许一条,幂等重导的根基
ALTER TABLE sign_up
    ADD UNIQUE KEY uk_phone_session (phone, session_id);

-- 导入批次记录:重复提交同一文件时先查批次,已存在则直接返回结果
CREATE TABLE import_batch (
    batch_no      VARCHAR(64) PRIMARY KEY,
    file_name     VARCHAR(255) NOT NULL,
    total_count   INT NOT NULL DEFAULT 0,
    success_count INT NOT NULL DEFAULT 0,
    error_count   INT NOT NULL DEFAULT 0,
    status        TINYINT NOT NULL DEFAULT 0, -- 0处理中 1完成 2失败
    created_at    DATETIME NOT NULL
);

错误回执文件按"行号 + 字段 + 原因"生成,供用户对照修改;只导出错误行,行号保持原 Excel 行号不变,用户一眼能定位。回执文件本身复用导出的正确性工程(UTF-8 BOM、文本列、公式注入转义),避免"错误清单又打不开"的二次问题。

补充一个细节:校验与落库之间可能隔了一段时间(异步场景),落库前要重查场次配置是否仍有效、名单上限是否已满,防止"校验时通过、落库时已失效"的竞态;这类二次校验在异步导入里尤其不能省。

踩坑清单

  • 坑 1:边读边写、遇错回滚 → 先全量校验再分批落库,两阶段解耦。
  • 坑 2:一个 Excel 一个事务 → 按 100–500 行分批,缩事务边界。
  • 坑 3:错误只报"第 N 行错误" → 报"行号 + 字段 + 原因",多错误合并。
  • 坑 4:重传后正确行重复入库 → 业务唯一键 + 批次号幂等。
  • 坑 5:手机号/日期被 Excel 预处理丢前导零 → 模板列设文本格式,后端按字符串读取。
  • 坑 6:空行、全空格行被当成数据 → 解析时显式跳过并做去空白。

工程落地建议

  • 解析库用成熟的 Apache POI / EasyExcel / xlrd 等,不要自己解析 xlsx 二进制;CSV 场景注意编码识别(UTF-8 无 BOM 时优先按 GBK 兜底,中文模板常用)。
  • 校验规则抽成可配置(必填/正则/枚举/长度),改规则不动代码。
  • 导入任务量大时可丢到异步队列执行,前端轮询批次状态;本方案的核心(校验→落库→回执→幂等)在同步、异步两种形态下结构一致。

上线复盘清单

  • 模板列头带版本号,上传先校验列头
  • 校验规则可配置,错误含行号与原因
  • 单批事务 ≤ 500 行,批间互不影响
  • 业务唯一键覆盖所有重导场景
  • 批次表记录每次导入的统计与状态
  • 导入文件大小与行数有上限保护

常见问题

Q:批量导入时怎么避免"一行错误整批失败"?

A:把导入拆成"全量校验 → 分批落库"两阶段:先逐行校验并收集错误,有错误就整份返回错误清单不落库;校验通过后按 200 行一批写入,批内失败只回滚本批。

Q:同一份名单重新上传会不会重复入库?

A:不会。给业务表加唯一索引(如手机号+场次),重导时正确行通过先查后插去重;同时用导入批次号记录文件,同一批次重复提交直接返回上次结果。

结语

仅作技术分享,具体实现按自身业务规模取舍,功能以官方实时信息为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么"校验不过就整批回滚"必然被吐槽
  • 二、第一步:模板与列头校验,先挡住"文件不对"
  • 三、第二步:逐行校验,只收集错误不写库
  • 四、第三步:分批落库,事务边界要小
  • 五、第四步:错误回执与幂等重导
  • 踩坑清单
  • 工程落地建议
  • 上线复盘清单
  • 常见问题
    • Q:批量导入时怎么避免"一行错误整批失败"?
    • Q:同一份名单重新上传会不会重复入库?
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档