🐛 修复常用货物导入未整批回滚,失败明细原因写回原行

问题:CommonCargoServiceImpl.importCommonCargo 循环内吞掉异常后正常 return,
方法正常返回导致 Spring 照常提交事务。方法虽有 @Transactional(rollbackFor =
Exception.class),但异常被吞后注解救不了——失败行之前的正确行已经落库。

改动:
- 服务层:错误收集完毕后若 failureList 非空,抛 ImportFailureException
  (org.springblade.common.excel,#2 已迁入 blade-common)触发整批回滚。
- 修复失败明细原因为空的问题:catch 里除写入 failureExcel 副本外,
  还必须把原因写回原行对象 excel.setErrorMessage(...)——抛出异常时携带的是
  原表 data,导出工具从行对象的 errorMessage 字段取原因,只写副本会丢。
- 控制器:改为 catch 异常 → 取 failureList 导出 → return null,并补上传
  文件校验,对齐 PortTerminalController 写法。

验证:
- mvn install -DskipTests 全量编译通过。
- 常用货物回归 25/25 通过(新增 tests/fixtures/common-cargo/,
  scripts/api-common-cargo-import-test.sh),含 02_一行错误多行正确_应零写入。
- 港口码头基线回归 45/45 通过,确认无连带影响。
This commit is contained in:
2026-09-24 04:34:39 +08:00
parent 99c4bf4f7d
commit 07b2f2766e
2 changed files with 36 additions and 4 deletions
@@ -38,9 +38,9 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.transport.excel.CommonCargoExcel;
import org.springblade.transport.excel.CommonCargoExportExcel;
import org.springblade.transport.excel.CommonCargoImportFailureExcel;
import org.springframework.web.multipart.MultipartFile;
import org.springblade.transport.pojo.entity.CommonCargo;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
@@ -110,14 +110,32 @@ public class CommonCargoController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入常用货物", description = "传入excel")
public R importCommonCargo(MultipartFile file, HttpServletResponse response) {
List<CommonCargoImportFailureExcel> failureList = commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoImportFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null;
}
return R.success("导入数据成功");
}
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoExcel.class);
}
@GetMapping("/export-template")
@ApiOperationSupport(order = 7)
@Operation(summary = "导出模板")
@@ -25,6 +25,7 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
@@ -143,6 +144,10 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意:本方法虽已标注 @Transactional,但循环内把异常吞掉后正常 return,
// Spring 会认为一切成功并提交事务——注解救不了被吞掉的异常。
// 必须把错误明细收集完后抛异常,回滚才会真正发生。
List<CommonCargoImportFailureExcel> failureList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
CommonCargoExcel excel = data.get(index);
@@ -170,12 +175,21 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
commonCargo.setDataSource("批量导入");
submit(commonCargo, false);
} catch (Exception exception) {
// 除收集到 failureList 外,还必须把原因写回原行对象:
// 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因,
// 只写在副本上会导致失败明细里错误行的原因为空。
excel.setErrorMessage(exception.getMessage());
CommonCargoImportFailureExcel failureExcel = new CommonCargoImportFailureExcel();
BeanUtil.copyProperties(excel, failureExcel);
failureExcel.setErrorMessage(exception.getMessage());
failureList.add(failureExcel);
}
}
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new ImportFailureException(data);
}
return failureList;
}