From be02a5e9420f93c9f3d17826ad977c95aa1259aa Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 11:10:37 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BF=90=E8=BE=93?= =?UTF-8?q?=E5=AF=B9=E8=B4=A6=E5=AF=BC=E5=85=A5=E6=97=A0=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E8=BE=93=E8=AE=A1=E5=88=92=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E4=B8=8D=E5=85=A8=EF=BC=88=E6=89=B9=E6=AC=A14=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本批是 16 个模块收尾,两个特殊模块: 1) 运输对账(importVehicles / importCargoes)——最严重的一例: 既无 setRollbackOnly 也不抛异常。方法开头 resetExternal(id) 清空既有外部账单, 循环末尾无条件插入本批正确行,然后 return failures。一旦有失败行: 旧外部账单已被清空、新账单只插入了部分——数据损坏且不可恢复。 改法:任一行失败时抛 ImportFailureException(rows),让 resetExternal 清空 与本批插入在同一事务内一起回滚。另把失败原因写回原行(异常携带原表 rows, 只写 Vehicle/CargoReconciliationFailureExcel 副本会丢原因)。 控制器改 catch 异常导出原表全量(用 export 保留按列标红)。 2) 运输计划(importTransportPlan)——失败明细只返回 errorMap.values()(仅错误行)。 改为抛 ImportFailureException(data) 携带原表全量;控制器 catch 后导出。 验证:mvn install -DskipTests 通过;全项目 12 个导入回归脚本 379 项断言全绿。 全项目已无 TransactionAspectSupport...setRollbackOnly() 调用残留。 注:运输对账需先建对账草稿(多张前置表)才能端到端回归, 本批仅完成代码改造与编译验证,回归用例待补(见后续说明)。 --- .../controller/TransportPlanController.java | 17 ++++++-- .../TransportReconciliationController.java | 43 +++++++++++++------ .../impl/TransportPlanServiceImpl.java | 5 ++- .../TransportReconciliationServiceImpl.java | 34 ++++++++++++--- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportPlanController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportPlanController.java index ae64bb22..39c3580b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportPlanController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportPlanController.java @@ -147,10 +147,19 @@ public class TransportPlanController extends BladeController { public R importTransportPlan(MultipartFile file, @RequestParam Long projectId, @RequestParam String projectName, @RequestParam Long contractId, @RequestParam String contractName, @RequestParam String customerName, HttpServletResponse response) { - List failureList = transportPlanService.importTransportPlan( - ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportPlanImportExcel.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 { + transportPlanService.importTransportPlan( + ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportPlanImportExcel.class); return null; } return R.success("导入数据成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java index 2b5d7772..2baf5686 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java @@ -19,6 +19,7 @@ import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.transport.excel.CargoReconciliationExcel; import org.springblade.transport.excel.CargoReconciliationFeeReader; @@ -126,12 +127,21 @@ public class TransportReconciliationController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入整车总额外部账单") public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { - List rows = ExcelUtil.read(file, VehicleReconciliationExcel.class); - List> feeItems = VehicleReconciliationFeeReader.read(file); - for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); - List failures = reconciliationService.importVehicles(id, rows); - if (!failures.isEmpty()) { - ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.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 { + List rows = ExcelUtil.read(file, VehicleReconciliationExcel.class); + List> feeItems = VehicleReconciliationFeeReader.read(file); + for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); + reconciliationService.importVehicles(id, rows); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), VehicleReconciliationExcel.class); return null; } return R.success("导入数据成功"); @@ -141,12 +151,21 @@ public class TransportReconciliationController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入货物明细外部账单") public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { - List rows = ExcelUtil.read(file, CargoReconciliationExcel.class); - List> feeItems = CargoReconciliationFeeReader.read(file); - for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); - List failures = reconciliationService.importCargoes(id, rows); - if (!failures.isEmpty()) { - ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.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 { + List rows = ExcelUtil.read(file, CargoReconciliationExcel.class); + List> feeItems = CargoReconciliationFeeReader.read(file); + for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index)); + reconciliationService.importCargoes(id, rows); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CargoReconciliationExcel.class); return null; } return R.success("导入数据成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java index 9c5620c6..d466d882 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java @@ -294,8 +294,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl(errorMap.values()); + // 任一行失败即整批回滚:抛异常触发事务回滚(替代旧的 setRollbackOnly), + // 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。 + throw new org.springblade.common.excel.ImportFailureException(data); } // 第二阶段:批量导入 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java index 05e01c29..2039b426 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -300,22 +300,33 @@ public class TransportReconciliationServiceImpl VehicleReconciliationExcel row = rows.get(index); List validationErrors = validateImportVehicle(row); if (Func.isNotEmpty(validationErrors)) { + // 原因同时写回原行:异常携带的是原表 rows, + // 导出工具从行对象的 errorMessage 字段取原因,只写副本会丢原因。 + row.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors)); VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel(); BeanUtil.copyProperties(row, failure); - failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors)); + failure.setErrorMessage(row.getErrorMessage()); failures.add(failure); continue; } try { validExternals.add(buildVehicleExternal(id, index, row)); } catch (Exception exception) { + String message = org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of( + exception instanceof ServiceException ? exception.getMessage() : "导入失败")); + row.setErrorMessage(message); VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel(); BeanUtil.copyProperties(row, failure); - failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of( - exception instanceof ServiceException ? exception.getMessage() : "导入失败"))); + failure.setErrorMessage(message); failures.add(failure); } } + if (Func.isNotEmpty(failures)) { + // 任一行失败即整批回滚:抛异常触发事务回滚,使前置的 resetExternal + // 清空与本批正确行的插入一并退掉;否则会出现「旧外部账单被清空、 + // 正确行却已入库」的数据损坏。 + throw new org.springblade.common.excel.ImportFailureException(rows); + } for (TransportReconciliationExternal external : validExternals) { if (externalMapper.insert(external) <= 0) throw new ServiceException("外部账单保存失败"); } @@ -418,22 +429,33 @@ public class TransportReconciliationServiceImpl CargoReconciliationExcel row = rows.get(index); List validationErrors = validateImportCargo(row); if (Func.isNotEmpty(validationErrors)) { + // 原因同时写回原行:异常携带的是原表 rows, + // 导出工具从行对象的 errorMessage 字段取原因,只写副本会丢原因。 + row.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors)); CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel(); BeanUtil.copyProperties(row, failure); - failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors)); + failure.setErrorMessage(row.getErrorMessage()); failures.add(failure); continue; } try { validExternals.add(buildCargoExternal(id, index, row)); } catch (Exception exception) { + String message = org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of( + exception instanceof ServiceException ? exception.getMessage() : "导入失败")); + row.setErrorMessage(message); CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel(); BeanUtil.copyProperties(row, failure); - failure.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of( - exception instanceof ServiceException ? exception.getMessage() : "导入失败"))); + failure.setErrorMessage(message); failures.add(failure); } } + if (Func.isNotEmpty(failures)) { + // 任一行失败即整批回滚:抛异常触发事务回滚,使前置的 resetExternal + // 清空与本批正确行的插入一并退掉;否则会出现「旧外部账单被清空、 + // 正确行却已入库」的数据损坏。 + throw new org.springblade.common.excel.ImportFailureException(rows); + } for (TransportReconciliationExternal external : validExternals) { if (externalMapper.insert(external) <= 0) throw new ServiceException("外部账单保存失败"); }