🐛 修复币种汇率导入未整批回滚,派生写随事务一起退

问题:CurrencyServiceImpl.importCurrency 无 @Transactional,循环内吞掉异常
后正常 return,失败行之前的正确行已落库。且循环内除 save 外还有
rebuildEnabledValidity 派生写(按 code 重算启用记录有效期区间),
若只回滚主表遗漏派生写,会留下「主表回滚了、有效期却按失败批次算过」的脏数据。

改动:
- 服务层:补 @Transactional(rollbackFor = Exception.class),错误收集完后
  抛 ImportFailureException(org.springblade.common.excel),
  主表 save 与派生写 rebuildEnabledValidity 在同一事务内一起回滚。
- CurrencyExcel.errorMessage 由 @ExcelProperty 改为 @ExcelIgnore:
  原先导出模板会多出 errorMessage 空列(与 issue #12 运单模板同款问题)。
- 控制器:改 catch 异常导出 + 补上传文件校验,对齐 PortTerminal 写法。

验证:mvn install -DskipTests 通过;币种回归 25/25(含派生区间
正确性断言与零写入断言);同批修复顺带覆盖 issue #12 同类问题。
This commit is contained in:
2026-09-24 05:27:25 +08:00
parent cd2052e5d0
commit 30e1ada0cc
3 changed files with 24 additions and 4 deletions
@@ -149,9 +149,18 @@ public class CurrencyController extends BladeController {
@ApiOperationSupport(order = 7) @ApiOperationSupport(order = 7)
@Operation(summary = "导入币种汇率", description = "传入excel") @Operation(summary = "导入币种汇率", description = "传入excel")
public R importCurrency(MultipartFile file, HttpServletResponse response) { public R importCurrency(MultipartFile file, HttpServletResponse response) {
List<CurrencyExcel> failureList = currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class)); if (file == null || file.isEmpty()) {
if (Func.isNotEmpty(failureList)) { return R.fail("上传文件不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CurrencyExcel.class); }
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CurrencyExcel.class);
return null; return null;
} }
return R.success("操作成功"); return R.success("操作成功");
@@ -76,7 +76,7 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@ExcelProperty @ExcelIgnore
private String errorMessage; private String errorMessage;
} }
@@ -115,10 +115,16 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public List<CurrencyExcel> importCurrency(List<CurrencyExcel> data) { public List<CurrencyExcel> importCurrency(List<CurrencyExcel> data) {
if (Func.isEmpty(data)) { if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空"); throw new ServiceException("导入数据不能为空");
} }
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意:循环内除 save 外还有 rebuildEnabledValidity 这个派生写——
// 若吞掉异常正常 return,主表数据和派生的有效期区间都会落库;
// 必须抛异常让两者一起回滚,否则会出现「主表回滚了、有效期区间
// 却按失败批次算过」的脏数据。
List<CurrencyExcel> errorList = new ArrayList<>(); List<CurrencyExcel> errorList = new ArrayList<>();
int successCount = 0; int successCount = 0;
for (int index = 0; index < data.size(); index++) { for (int index = 0; index < data.size(); index++) {
@@ -140,6 +146,11 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
errorList.add(excel); errorList.add(excel);
} }
} }
if (Func.isNotEmpty(errorList)) {
// 抛出携带失败明细的异常,触发事务回滚(含 rebuildEnabledValidity 的派生写)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
return errorList; return errorList;
} }