导读:运营同事丢来一份 500 行的报名名单 Excel,点导入后后台提示"导入失败"、整批回滚——排查半天才发现只有第 137 行手机号格式不对。这类"一行出错、全盘皆输"的后台导入,是不少业务系统上线后踩的第一个坑。本文给一套可直接抄走的实现:逐行校验收集错误、分批事务落库、错误清单回执、按批次幂等重导。
很多系统第一版导入的逻辑是:边读 Excel 边写库,遇到任何一行校验失败就 throw 并回滚整个事务。500 行名单里 1 行出错,用户得到的结果是"导入失败",却不知道错在哪一行、错在哪个字段;改完重传,又可能因为前面已写入的部分数据没有清理干净而重复。
正确的做法是拆成四个阶段:模板校验 → 全量逐行校验(只收错误不写库)→ 分批落库 → 错误回执与幂等重导。
导入模板要固定列头(如 姓名/手机号/报名场次),并带上版本号。上传后第一件事是拿真实表头与模板逐列比对,列头不匹配直接拒绝,别等解析到一半才发现缺列。
// 基于 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}$,场次日期按配置格式解析;错误统一收集成 行号 + 字段 + 原因 的结构,一行多个错误合并成一条提示,避免用户改一次传一次。
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 行"。
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()));
}
}-- 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 的解析开销在几百行时已能明显感知。
用户改完错误行后通常会把整份文件重新上传。这时要防两件事:正确行重复入库、同批文件被重复导入。两个手段:
(phone, session_id) 唯一索引,重导时正确行通过 upsert 语义去重(先查后插,或 INSERT ... ON DUPLICATE KEY UPDATE),不产生重复记录;batch_no,写入导入批次表,文件与批次一一对应;同一批次重复提交直接返回上次结果,不做二次写入。-- 同一场次同一手机号只允许一条,幂等重导的根基
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、文本列、公式注入转义),避免"错误清单又打不开"的二次问题。
补充一个细节:校验与落库之间可能隔了一段时间(异步场景),落库前要重查场次配置是否仍有效、名单上限是否已满,防止"校验时通过、落库时已失效"的竞态;这类二次校验在异步导入里尤其不能省。
A:把导入拆成"全量校验 → 分批落库"两阶段:先逐行校验并收集错误,有错误就整份返回错误清单不落库;校验通过后按 200 行一批写入,批内失败只回滚本批。
A:不会。给业务表加唯一索引(如手机号+场次),重导时正确行通过先查后插去重;同时用导入批次号记录文件,同一批次重复提交直接返回上次结果。
仅作技术分享,具体实现按自身业务规模取舍,功能以官方实时信息为准。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。