From 99c4bf4f7d58ba87a76019c912f7f9bf80df6ab9 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Wed, 23 Sep 2026 03:47:58 +0800 Subject: [PATCH 1/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=B4=A7?= =?UTF-8?q?=E7=89=A9=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=9B=9E=E6=BB=9A=EF=BC=8C=E5=B9=B6=E6=8A=8A=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=9B=9E=E6=BB=9A=E5=BC=82=E5=B8=B8=E8=BF=81=E5=88=B0?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:CargoTypeServiceImpl.importCargoType 循环内吞掉异常后正常 return, 方法正常返回导致 Spring 照常提交事务,@Transactional 起不到回滚作用。 含 1 行错误的文件会写入前面若干行正确数据,与「任一行失败即整批零写入」口径不符。 改动: - ImportFailureException 由 org.springblade.system.excel 迁至 org.springblade.common.excel(与 ImportFailureExcelUtil 同包)。 blade-transport 只依赖 blade-system-api,原先拿不到该异常, 迁到 blade-common 后 blade-system 与 blade-transport 均可引用。 - CargoTypeServiceImpl.importCargoType 补 @Transactional(rollbackFor = Exception.class), 错误收集完毕后抛 ImportFailureException 触发回滚,不再 return failureList。 - CargoTypeController.importCargoType 由「拿返回值导出」改为 「catch 异常 → 取 failureList 导出 → return null」,并补上传文件校验, 对齐 PortTerminalController 的写法。 - PortTerminalController / PortTerminalServiceImpl 仅调整 import 指向新包,行为不变。 验证: - mvn install -DskipTests 全量编译通过。 - 港口码头回归 45/45 通过,确认「异常搬家」未破坏既有基线。 - 货物类型回归 24/24 通过,含 02_一行错误多行正确_应零写入(库中零写入)。 --- .../common}/excel/ImportFailureException.java | 12 ++++++--- .../controller/CargoTypeController.java | 27 +++++++++++++++---- .../controller/PortTerminalController.java | 2 +- .../service/impl/CargoTypeServiceImpl.java | 10 +++++++ .../service/impl/PortTerminalServiceImpl.java | 2 +- 5 files changed, 43 insertions(+), 10 deletions(-) rename {blade-service/blade-system/src/main/java/org/springblade/system => blade-common/src/main/java/org/springblade/common}/excel/ImportFailureException.java (72%) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java similarity index 72% rename from blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java rename to blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java index 0277ec01..6065ce0f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java @@ -23,7 +23,7 @@ *

* Author: Chill Zhuang (bladejava@qq.com) */ -package org.springblade.system.excel; +package org.springblade.common.excel; import org.springblade.core.log.exception.ServiceException; @@ -33,8 +33,14 @@ import java.util.List; /** * 导入失败异常,携带失败明细用于导出原表并标注错误。 *

- * 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚, - * 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。 + * 批量导入采用「任一行失败即整批回滚」语义:循环内先收集完全部错误明细, + * 再抛出本异常触发事务回滚。明细在抛异常前已收集完毕,因此回滚不影响明细内容。 + *

+ * 注意:仅标注 {@code @Transactional} 并不足够。若循环内把异常吞掉(catch 后 + * 只往 errorList 塞数据并正常 return),方法会正常返回,Spring 会照常提交事务。 + * 必须抛出异常才能让事务真正回滚。 + *

+ * 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。 * * @author Chill */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java index 745968b5..616e1ae6 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java @@ -46,9 +46,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.system.excel.CargoTypeExcel; import org.springblade.system.excel.CargoTypeExportExcel; -import org.springblade.system.excel.CargoTypeImportFailureExcel; import org.springblade.system.pojo.entity.CargoType; import org.springblade.system.pojo.vo.CargoTypeVO; import org.springblade.system.service.ICargoTypeService; @@ -156,15 +156,32 @@ public class CargoTypeController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入货物类型", description = "传入excel") public R importCargoType(MultipartFile file, HttpServletResponse response) { - List data = ExcelUtil.read(file, CargoTypeExcel.class); - List failureList = cargoTypeService.importCargoType(data); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.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 { + cargoTypeService.importCargoType(ExcelUtil.read(file, CargoTypeExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + exportFailure(response, exception.getFailureList()); return null; } return R.success("导入数据成功"); } + /** + * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。 + *

+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。 + */ + private void exportFailure(HttpServletResponse response, List failureList) { + ImportFailureExcelUtil.exportFailureReasonOnly(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class); + } + /** * 导出货物类型 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java index 0639b52d..8c19eedc 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java @@ -44,7 +44,7 @@ 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.system.excel.ImportFailureException; +import org.springblade.common.excel.ImportFailureException; import org.springblade.system.excel.PortTerminalExcel; import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.excel.PortTerminalImporter; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CargoTypeServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CargoTypeServiceImpl.java index e7b97e2b..a1fc4f74 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CargoTypeServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CargoTypeServiceImpl.java @@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; 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.Func; @@ -134,10 +135,14 @@ public class CargoTypeServiceImpl extends BaseServiceImpl importCargoType(List data) { if (Func.isEmpty(data)) { throw new ServiceException("导入数据不能为空"); } + // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 + // 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回, + // Spring 会照常提交事务,@Transactional 起不到回滚作用。 List failureList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { CargoTypeExcel excel = data.get(index); @@ -152,6 +157,11 @@ public class CargoTypeServiceImpl extends BaseServiceImpl Date: Thu, 24 Sep 2026 04:34:39 +0800 Subject: [PATCH 2/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=B8=B8?= =?UTF-8?q?=E7=94=A8=E8=B4=A7=E7=89=A9=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=9B=9E=E6=BB=9A=EF=BC=8C=E5=A4=B1=E8=B4=A5=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E5=8E=9F=E5=9B=A0=E5=86=99=E5=9B=9E=E5=8E=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: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 通过,确认无连带影响。 --- .../controller/CommonCargoController.java | 26 ++++++++++++++++--- .../service/impl/CommonCargoServiceImpl.java | 14 ++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonCargoController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonCargoController.java index 34b54fa5..212ca3a9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonCargoController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonCargoController.java @@ -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 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("导入数据成功"); } + /** + * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。 + *

+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。 + */ + private void exportFailure(HttpServletResponse response, List failureList) { + ImportFailureExcelUtil.exportFailureReasonOnly(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoExcel.class); + } + @GetMapping("/export-template") @ApiOperationSupport(order = 7) @Operation(summary = "导出模板") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java index f8b6b5c7..7ab5bc04 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonCargoServiceImpl.java @@ -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 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 Date: Thu, 24 Sep 2026 04:57:26 +0800 Subject: [PATCH 3/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=B8=B8?= =?UTF-8?q?=E7=94=A8=E7=BA=BF=E8=B7=AF=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=9B=9E=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:CommonRouteServiceImpl.importCommonRoute 循环内吞掉异常后正常 return, 方法正常返回导致 Spring 照常提交事务。方法虽有 @Transactional(rollbackFor = Exception.class),但异常被吞后注解救不了——失败行之前的正确行已经落库。 改动: - 服务层:错误收集完毕后若 failureList 非空,抛 ImportFailureException (org.springblade.common.excel)触发整批回滚。catch 里原本就把原因 写在原行对象上,失败明细原因不丢,无需额外处理。 - 控制器:改为 catch 异常 → 取 failureList 导出(exportFailureReasonOnly) → return null,并补上传文件校验,对齐 PortTerminalController 写法。 验证: - mvn install -DskipTests 全量编译通过。 - 常用线路回归 29/29 通过(新增 tests/fixtures/common-route/, scripts/api-common-route-import-test.sh),含 02_一行错误多行正确_应零写入、 07_与库中线路重名(基线行不受回滚影响)。 - 港口码头 45/45、常用货物 25/25 基线回归通过,确认无连带影响。 --- .../controller/CommonRouteController.java | 20 +++++++++++++------ .../service/impl/CommonRouteServiceImpl.java | 10 ++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonRouteController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonRouteController.java index 61966056..db31a347 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonRouteController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CommonRouteController.java @@ -38,6 +38,7 @@ 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.CommonRouteExportExcel; import org.springblade.transport.excel.CommonRouteImportExcel; import org.springblade.transport.pojo.entity.CommonRoute; @@ -109,15 +110,22 @@ public class CommonRouteController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入常用线路", description = "传入excel") public R importCommonRoute(MultipartFile file, HttpServletResponse response) { - List failureList = commonRouteService.importCommonRoute( - ExcelUtil.read(file, CommonRouteImportExcel.class) - ); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export( + 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 { + commonRouteService.importCommonRoute(ExcelUtil.read(file, CommonRouteImportExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + ImportFailureExcelUtil.exportFailureReasonOnly( response, "常用线路导入失败明细" + DateUtil.time(), "导入失败明细", - failureList, + exception.getFailureList(), CommonRouteImportExcel.class ); return null; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java index 10d7f8d0..505ffd51 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java @@ -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; @@ -130,6 +131,10 @@ public class CommonRouteServiceImpl extends BaseServiceImpl failureList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { CommonRouteImportExcel excel = data.get(index); @@ -142,6 +147,11 @@ public class CommonRouteServiceImpl extends BaseServiceImpl Date: Thu, 24 Sep 2026 05:27:25 +0800 Subject: [PATCH 4/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=B8=81?= =?UTF-8?q?=E7=A7=8D=E6=B1=87=E7=8E=87=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=9B=9E=E6=BB=9A=EF=BC=8C=E6=B4=BE=E7=94=9F=E5=86=99?= =?UTF-8?q?=E9=9A=8F=E4=BA=8B=E5=8A=A1=E4=B8=80=E8=B5=B7=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: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 同类问题。 --- .../system/controller/CurrencyController.java | 15 ++++++++++++--- .../springblade/system/excel/CurrencyExcel.java | 2 +- .../system/service/impl/CurrencyServiceImpl.java | 11 +++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java index 4bc43d95..be6ef0f5 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java @@ -149,9 +149,18 @@ public class CurrencyController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入币种汇率", description = "传入excel") public R importCurrency(MultipartFile file, HttpServletResponse response) { - List failureList = currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CurrencyExcel.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 { + 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 R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java index b59573d5..998f3aac 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java @@ -76,7 +76,7 @@ public class CurrencyExcel implements Serializable { @ExcelProperty("备注") private String remark; - @ExcelProperty + @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java index dea6d264..48aeedc9 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/CurrencyServiceImpl.java @@ -115,10 +115,16 @@ public class CurrencyServiceImpl extends BaseServiceImpl importCurrency(List data) { if (Func.isEmpty(data)) { throw new ServiceException("导入数据不能为空"); } + // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 + // 注意:循环内除 save 外还有 rebuildEnabledValidity 这个派生写—— + // 若吞掉异常正常 return,主表数据和派生的有效期区间都会落库; + // 必须抛异常让两者一起回滚,否则会出现「主表回滚了、有效期区间 + // 却按失败批次算过」的脏数据。 List errorList = new ArrayList<>(); int successCount = 0; for (int index = 0; index < data.size(); index++) { @@ -140,6 +146,11 @@ public class CurrencyServiceImpl extends BaseServiceImpl Date: Thu, 24 Sep 2026 05:36:41 +0800 Subject: [PATCH 5/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=B4=B9?= =?UTF-8?q?=E7=94=A8=E9=A1=B9=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4=E6=89=B9?= =?UTF-8?q?=E5=9B=9E=E6=BB=9A=EF=BC=8C=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=E5=86=99=E5=9B=9E=E5=8E=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:FeeItemServiceImpl.importFeeItem 虽有 @Transactional,但循环内吞掉 异常后正常 return,Spring 照常提交——失败行之前的正确行已落库。 改动: - 服务层:错误收集完后抛 ImportFailureException(org.springblade.common.excel) 触发整批回滚;同时把失败原因写回原行对象 excel.setErrorMessage—— 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因, 只写在 FeeItemImportFailureExcel 副本上会导致失败明细原因为空。 - 控制器:改 catch 异常导出(exportFailureReasonOnly)+ 补上传文件校验, 对齐 PortTerminalController 写法。 验证:mvn install -DskipTests 通过;费用项回归 20/20 (tests/fixtures/fee-item/,含 02_一行错误多行正确_应零写入)。 --- .../system/controller/FeeItemController.java | 18 +++++++++++++----- .../service/impl/FeeItemServiceImpl.java | 12 ++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java index b8eae386..2bf7ea2d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java @@ -36,6 +36,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.common.excel.ImportFailureException; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; @@ -47,7 +48,6 @@ import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.excel.FeeItemExcel; import org.springblade.system.excel.FeeItemExportExcel; -import org.springblade.system.excel.FeeItemImportFailureExcel; import org.springblade.system.pojo.entity.FeeItem; import org.springblade.system.pojo.vo.FeeItemVO; import org.springblade.system.service.IFeeItemService; @@ -138,10 +138,18 @@ public class FeeItemController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入费用项", description = "传入excel") public R importFeeItem(MultipartFile file, HttpServletResponse response) { - List data = ExcelUtil.read(file, FeeItemExcel.class); - List failureList = feeItemService.importFeeItem(data); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", failureList, FeeItemExcel.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 { + feeItemService.importFeeItem(ExcelUtil.read(file, FeeItemExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + ImportFailureExcelUtil.exportFailureReasonOnly(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), FeeItemExcel.class); return null; } return R.success("导入数据成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/FeeItemServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/FeeItemServiceImpl.java index a3825a46..4221d262 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/FeeItemServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/FeeItemServiceImpl.java @@ -108,6 +108,9 @@ public class FeeItemServiceImpl extends BaseServiceImpl if (Func.isEmpty(data)) { throw new ServiceException("导入数据不能为空"); } + // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 + // 注意不能只往 failureList 里塞数据就 return——异常被吞掉后方法会正常返回, + // Spring 会照常提交事务,@Transactional 起不到回滚作用。 List failureList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { FeeItemExcel excel = data.get(index); @@ -118,9 +121,18 @@ public class FeeItemServiceImpl extends BaseServiceImpl save(feeItem); } catch (Exception exception) { String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; + // 除收集到 failureList 外,还必须把原因写回原行对象: + // 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因, + // 只写在副本上会导致失败明细里错误行的原因为空。 + excel.setErrorMessage("第" + (index + 2) + "行:" + message); failureList.add(toImportFailureExcel(excel, "第" + (index + 2) + "行:" + message)); } } + if (Func.isNotEmpty(failureList)) { + // 抛出携带失败明细的异常,触发事务回滚。 + // 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。 + throw new org.springblade.common.excel.ImportFailureException(data); + } return failureList; } -- 2.52.0 From a8808942a33d65d04e3a9fa501df043c0e7d0645 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 24 Sep 2026 05:46:19 +0800 Subject: [PATCH 6/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=AF=84?= =?UTF-8?q?=E5=88=86=E9=87=8F=E5=8C=96=E8=A1=A8=E5=AF=BC=E5=85=A5=E6=9C=AA?= =?UTF-8?q?=E6=95=B4=E6=89=B9=E5=9B=9E=E6=BB=9A=EF=BC=88=E6=8C=89=E8=AF=84?= =?UTF-8?q?=E5=AE=9A=E8=A1=A8=E5=88=86=E7=BB=84=E7=B2=92=E5=BA=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:CreditScoreQuantificationServiceImpl.importCreditScoreQuantification 循环单位是「评定表名称」分组(每分组一次 submit,写主表+项目+选项等多表), 异常被 catch 吞掉后正常 return,Spring 照常提交——失败分组之前的 正确分组已经落库。 改动: - 服务层:任一分组失败、errorList 非空时抛 ImportFailureException (org.springblade.common.excel)触发整批回滚,失败分组之前已 submit 的正确分组一起退掉。失败明细粒度保留既有行为:覆盖失败分组的全部行。 「评定表名称」为空的行不走 submit、保持早退标错路径。 - 控制器:改 catch 异常导出(exportFailureReasonOnly)+ 补上传文件校验。 验证:mvn install -DskipTests 通过;评分量化表回归 15/15 (tests/fixtures/credit-score/,含 02_后组失败前组正确_应零写入)。 另确认既有语义:与库中重名的分组走覆盖式更新(saveOrUpdate),非错误。 --- .../CreditScoreQuantificationController.java | 15 ++++++++++++--- .../CreditScoreQuantificationServiceImpl.java | 7 +++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CreditScoreQuantificationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CreditScoreQuantificationController.java index 59c404ff..d10852ba 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CreditScoreQuantificationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CreditScoreQuantificationController.java @@ -138,9 +138,18 @@ public class CreditScoreQuantificationController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入评分量化表", description = "传入excel") public R importCreditScoreQuantification(MultipartFile file, HttpServletResponse response) { - List failureList = creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CreditScoreQuantificationExcel.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 { + creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一分组失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CreditScoreQuantificationExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java index c5915da9..cebba5e9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java @@ -214,6 +214,13 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl Date: Thu, 24 Sep 2026 05:55:36 +0800 Subject: [PATCH 7/8] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BF=90?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E5=AF=BC=E5=85=A5=E6=9C=AA=E6=95=B4?= =?UTF-8?q?=E6=89=B9=E5=9B=9E=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:WaybillServiceImpl.importWaybill 虽有 @Transactional,但循环内吞掉 异常后正常 return,Spring 照常提交——失败行之前的正确行已落库。 本票回滚边界比其它板块宽:submit 虽只写运单主表,但 carrierJson 构造、 多个宽容日期解析都在循环内,失败原因需覆盖;关联表写入在 import-batch 链路,不在本票范围。 改动: - 服务层:错误收集完后抛 ImportFailureException(org.springblade.common.excel) 触发整批回滚。catch 里原本就把原因写在原行对象上,无需额外处理。 - 控制器:改 catch 异常导出(exportFailureReasonOnly)+ 补上传文件校验。 验证:mvn install -DskipTests 通过;运单管理回归 19/19 (tests/fixtures/waybill-import/manage-excel/,含 02_一行错误多行正确_应零写入)。 既有 import-batch 链路的 *.json 与 excel/ 用例未改动。 --- .../transport/controller/WaybillController.java | 16 +++++++++++++--- .../service/impl/WaybillServiceImpl.java | 7 +++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 3de34089..de0ada47 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -34,6 +34,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.apache.poi.ss.usermodel.CellStyle; import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.common.excel.ImportFailureException; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; @@ -242,9 +243,18 @@ public class WaybillController extends BladeController { @ApiOperationSupport(order = 13) @Operation(summary = "导入运单管理", description = "传入excel") public R importWaybill(MultipartFile file, HttpServletResponse response) { - List failureList = waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class)); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillExcel.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 { + waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + ImportFailureExcelUtil.exportFailureReasonOnly(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), WaybillExcel.class); return null; } return R.success("导入数据成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 9594475f..889232ec 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -922,6 +922,13 @@ public class WaybillServiceImpl extends BaseServiceImpl errorList.add(excel); } } + if (Func.isNotEmpty(errorList)) { + // 抛出携带失败明细的异常,触发事务回滚:submit 内部一次写运单主表+ + // 货物+配载路线等多张关联表,回滚边界必须覆盖全部关联表。 + // 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回, + // Spring 会照常提交事务,@Transactional 起不到回滚作用。 + throw new org.springblade.common.excel.ImportFailureException(data); + } return errorList; } -- 2.52.0 From f2b6ca28347cd6e66d6dac0567e76050074638b2 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 24 Sep 2026 06:22:47 +0800 Subject: [PATCH 8/8] =?UTF-8?q?:bug:=20=E8=A1=8C=E6=94=BF=E5=8C=BA?= =?UTF-8?q?=E5=88=92=E5=AF=BC=E5=85=A5=E8=A1=A5=E4=BA=8B=E5=8A=A1=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=95=B4=E6=89=B9=E5=9B=9E=E6=BB=9A=EF=BC=8C=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=B8=85=E7=90=86=E7=A7=BB=E5=88=B0=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:RegionServiceImpl.importRegion 是唯一没有 @Transactional 的导入板块, 循环内吞掉异常后正常 return,已落库行直接提交。且 cacheChanged 标志导致 「先清缓存、再提交事务」:循环内已有行 submit 成功 → 后面行失败 → 方法 正常返回 → 惰性树缓存被清/重算成失败批次的中间状态,库回滚后缓存脏掉。 改动: - 服务层:补 @Transactional(rollbackFor = Exception.class);错误收集完后 抛 ImportFailureException(org.springblade.common.excel)触发整批回滚。 - 缓存语义重构:submit 拆出 clearCache 参数(公开 submit(region) 行为不变, 手工新增仍即时清缓存);导入路径传 false 不逐行清缓存, 统一移到方法末尾成功路径上——异常抛出后不可达,失败批次缓存保持原状。 - 控制器:改 catch 异常导出(exportFailureReasonOnly)+ 补上传文件校验。 验证:mvn install -DskipTests 通过;行政区划回归 25/25 (tests/fixtures/region/),Redis 断言覆盖:成功路径缓存被清、 失败路径缓存键原样保留、基线行不被失败批次污染。 --- .../system/controller/RegionController.java | 16 ++++++++-- .../service/impl/RegionServiceImpl.java | 30 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java index 0de1e680..c0ab7473 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java @@ -46,6 +46,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; import org.springblade.system.pojo.entity.Region; import org.springblade.system.excel.RegionExcel; import org.springblade.system.excel.RegionExportExcel; @@ -191,9 +192,18 @@ public class RegionController extends BladeController { @ApiOperationSupport(order = 10) @Operation(summary = "导入行政区划", description = "传入excel") public R importRegion(MultipartFile file, Integer isCovered, HttpServletResponse response) { - List failureList = regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1)); - if (!failureList.isEmpty()) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RegionExcel.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 { + regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RegionExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RegionServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RegionServiceImpl.java index 77ceabc1..a251af29 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RegionServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RegionServiceImpl.java @@ -41,6 +41,7 @@ import org.springblade.system.mapper.RegionMapper; import org.springblade.system.service.IRegionService; import org.springblade.system.pojo.vo.RegionVO; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Date; @@ -60,6 +61,17 @@ public class RegionServiceImpl extends ServiceImpl impleme @Override public boolean submit(Region region) { + return submit(region, true); + } + + /** + * 提交区划,clearCache 控制是否在保存成功后立即清理惰性树缓存。 + *

+ * 批量导入路径传 false:导入内部会收集全部错误,只有整批成功才统一清缓存; + * 若逐行清缓存,失败批次中已成功行会把缓存清掉/重算成中间状态, + * 与随后的事务回滚形成「库回滚了、缓存却是脏的」。 + */ + private boolean submit(Region region, boolean clearCache) { Date now = new Date(); Long currentUserId = AuthUtil.getUserId(); boolean isNew = StringUtil.isBlank(region.getOriginalCode()); @@ -138,7 +150,7 @@ public class RegionServiceImpl extends ServiceImpl impleme region.setVillageName(name); } boolean result = StringUtil.isNotBlank(region.getOriginalCode()) ? this.updateById(region) : this.save(region); - if (result) { + if (result && clearCache) { clearLazyTree(); } return result; @@ -189,12 +201,15 @@ public class RegionServiceImpl extends ServiceImpl impleme } @Override + @Transactional(rollbackFor = Exception.class) public List importRegion(List data, Boolean isCovered) { if (Func.isEmpty(data)) { throw new ServiceException("导入数据不能为空"); } + // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 + // 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回, + // Spring 会照常提交事务,@Transactional 起不到回滚作用。 List errorList = new ArrayList<>(); - boolean cacheChanged = false; for (int index = 0; index < data.size(); index++) { RegionExcel excel = data.get(index); try { @@ -210,15 +225,20 @@ public class RegionServiceImpl extends ServiceImpl impleme if (Boolean.TRUE.equals(isCovered) && this.getById(region.getCode()) != null) { region.setOriginalCode(region.getCode()); } - cacheChanged = this.submit(region) || cacheChanged; + // 导入路径不逐行清缓存,缓存只在整批成功后统一清一次。 + this.submit(region, false); } catch (Exception exception) { excel.setErrorMessage(exception.getMessage()); errorList.add(excel); } } - if (cacheChanged) { - clearLazyTree(); + if (Func.isNotEmpty(errorList)) { + // 抛出携带失败明细的异常,触发事务回滚。缓存清理在异常抛出后不可达, + // 只会发生在成功路径上——避免「库回滚了、惰性树缓存却按失败批次 + // 的中间状态被清掉/重算」的脏缓存。 + throw new org.springblade.common.excel.ImportFailureException(data); } + clearLazyTree(); return errorList; } -- 2.52.0