From 99c4bf4f7d58ba87a76019c912f7f9bf80df6ab9 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Wed, 23 Sep 2026 03:47:58 +0800 Subject: [PATCH 01/16] =?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 02/16] =?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 03/16] =?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 04/16] =?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 05/16] =?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; } From a8808942a33d65d04e3a9fa501df043c0e7d0645 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 24 Sep 2026 05:46:19 +0800 Subject: [PATCH 06/16] =?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 07/16] =?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; } From f2b6ca28347cd6e66d6dac0567e76050074638b2 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 24 Sep 2026 06:22:47 +0800 Subject: [PATCH 08/16] =?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; } From 81824b50628b66c6ffdb2bf8e8ffeb7269cb39cd Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 10:30:39 +0800 Subject: [PATCH 09/16] =?UTF-8?q?:bug:=20=E8=BF=9D=E7=AB=A0=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=AF=BC=E5=85=A5=E5=A4=B1=E8=B4=A5=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=85=A8=E9=87=8F=E5=8E=9F=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:importViolationRecord 收集 errorList 后 setRollbackOnly 并 return, 控制器据此导出「仅错误行」——失败明细不是原表全量,用户看不到哪几行是对的、 也不便对照原表修正(用户反馈:违章记录没有返回全量表格)。 改动: - 服务层:改为抛 ImportFailureException(data),明细携带**原表全部行**; 整批回滚由异常触发(替代 setRollbackOnly,语义不变)。 - 控制器:catch 异常后取 getFailureList() 导出,并补上传文件校验。 用 export(...) 而非 exportFailureReasonOnly(...):后者会把 markErrorColumns 置 false、只标红失败原因列,导致「按列标红」能力丢失(实测 5 个既有用例 标红列变空);export(...) 同时保留全量行与按列标红。 验证:mvn install -DskipTests 通过;违章记录回归 40/40 (既有 9 用例,标红列断言全绿:03 类型 / 04 事项 / 05 事项 / 06 类型 / 08 车牌号/船号); 用户实际上传文件实测:失败明细返回全量 5 行、仅错误行带原因、标红[车船类型,状态]、 库中零写入。其余 8 个导入模块回归 187 项全绿,无连带影响。 --- .../controller/ViolationRecordController.java | 15 ++++++++++++--- .../service/impl/ViolationRecordServiceImpl.java | 7 +++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java index d3a89443..0a87e8ec 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java @@ -127,9 +127,18 @@ public class ViolationRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入违章记录", description = "传入excel") public R importViolationRecord(MultipartFile file, HttpServletResponse response) { - List failureList = violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, ViolationRecordImportExcel.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 { + violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), ViolationRecordImportExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java index 5b41f40d..017d029d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java @@ -116,8 +116,11 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl Date: Sat, 26 Sep 2026 10:46:39 +0800 Subject: [PATCH 10/16] =?UTF-8?q?:bug:=20=E8=AE=B0=E5=BD=95=E7=B1=BB?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=EF=BC=886=20=E4=B8=AA=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=89=E5=A4=B1=E8=B4=A5=E6=98=8E=E7=BB=86=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E5=8E=9F=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:这 6 个模块(事故记录/年检记录/ETC记录/保险记录/维修记录/换胎记录) 在收集 errorList 后 setRollbackOnly 并 return,控制器据此导出「仅错误行」—— 失败明细不是原表全量,用户看不到哪几行是对的、不便对照原表修正。 改动(6 个模块同构处理): - 服务层:改为抛 ImportFailureException(data),明细携带**原表全部行**; 整批回滚由异常触发(替代 setRollbackOnly,语义不变)。 - 控制器:catch 异常后取 getFailureList() 导出,并补上传文件校验。 用 export(...) 保留按列标红(exportFailureReasonOnly 会丢失该能力)。 验证:mvn install -DskipTests 通过;记录类回归 48/48 (scripts/api-records-import-test.sh,6 模块 × 2 用例)。 标志性断言:3 行正确 + 第 4 行错 → 明细返回全部 4 行、仅第 4 行带原因、 库中零写入;全部正确 → 正常入库。 --- .../controller/AccidentRecordController.java | 15 ++++++++++++--- .../AnnualInspectionRecordController.java | 15 ++++++++++++--- .../transport/controller/EtcRecordController.java | 15 ++++++++++++--- .../controller/InsuranceRecordController.java | 15 ++++++++++++--- .../controller/MaintenanceRecordController.java | 15 ++++++++++++--- .../TireReplacementRecordController.java | 15 ++++++++++++--- .../service/impl/AccidentRecordServiceImpl.java | 5 +++-- .../impl/AnnualInspectionRecordServiceImpl.java | 5 +++-- .../service/impl/EtcRecordServiceImpl.java | 5 +++-- .../service/impl/InsuranceRecordServiceImpl.java | 5 +++-- .../impl/MaintenanceRecordServiceImpl.java | 5 +++-- .../impl/TireReplacementRecordServiceImpl.java | 5 +++-- 12 files changed, 90 insertions(+), 30 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java index d28cbb12..7a6e7140 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java @@ -128,9 +128,18 @@ public class AccidentRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入事故记录", description = "传入excel") public R importAccidentRecord(MultipartFile file, HttpServletResponse response) { - List failureList = accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AccidentRecordExcel.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 { + accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AccidentRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java index e97fa2d1..1412b173 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java @@ -125,9 +125,18 @@ public class AnnualInspectionRecordController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入年检记录", description = "传入excel") public R importAnnualInspectionRecord(MultipartFile file, HttpServletResponse response) { - List failureList = annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AnnualInspectionRecordExcel.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 { + annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AnnualInspectionRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java index 8258b7d1..fb3613cc 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java @@ -94,9 +94,18 @@ public class EtcRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入ETC记录", description = "传入excel") public R importEtcRecord(MultipartFile file, HttpServletResponse response) { - List failureList = etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EtcRecordExcel.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 { + etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EtcRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java index 4dc31536..4bf40c00 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java @@ -129,9 +129,18 @@ public class InsuranceRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入保险记录", description = "传入excel") public R importInsuranceRecord(MultipartFile file, HttpServletResponse response) { - List failureList = insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, InsuranceRecordExcel.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 { + insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), InsuranceRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java index e24f851f..51db7236 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java @@ -128,9 +128,18 @@ public class MaintenanceRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入维修记录", description = "传入excel") public R importMaintenanceRecord(MultipartFile file, HttpServletResponse response) { - List failureList = maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenanceRecordExcel.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 { + maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenanceRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java index d3d5741e..608d30ec 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java @@ -128,9 +128,18 @@ public class TireReplacementRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入换胎记录", description = "传入excel") public R importTireReplacementRecord(MultipartFile file, HttpServletResponse response) { - List failureList = tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TireReplacementRecordExcel.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 { + tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TireReplacementRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java index 70b04017..b06948bb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java @@ -111,8 +111,9 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl Date: Sat, 26 Sep 2026 10:52:02 +0800 Subject: [PATCH 11/16] =?UTF-8?q?:bug:=20=E8=AE=B0=E5=BD=95=E7=B1=BB?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=EF=BC=88=E7=AC=AC=E4=BA=8C=E6=89=B9=204=20?= =?UTF-8?q?=E4=B8=AA=E6=A8=A1=E5=9D=97=EF=BC=89=E5=A4=B1=E8=B4=A5=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E8=BF=94=E5=9B=9E=E5=85=A8=E9=87=8F=E5=8E=9F=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖:里程记录 / 油电记录 / 其他费用记录 / 运输变更记录。 问题同第一批:收集 errorList 后 setRollbackOnly 并 return,控制器据此导出 「仅错误行」——失败明细不是原表全量,用户无法对照原表修正。 改动(同构): - 服务层:抛 ImportFailureException(data),明细携带原表全部行; 回滚由异常触发(替代 setRollbackOnly)。 - 控制器:catch 异常取 getFailureList() 导出(用 export 保留按列标红), 补上传文件校验。 验证:mvn install -DskipTests 通过;记录类回归 80/80 (10 模块 × 2 用例,含第一批 6 个模块的回归确认)。 --- .../controller/MileageRecordController.java | 15 ++++++++++++--- .../controller/OilElectricRecordController.java | 15 ++++++++++++--- .../controller/OtherExpenseRecordController.java | 15 ++++++++++++--- .../TransportChangeRecordController.java | 15 ++++++++++++--- .../service/impl/MileageRecordServiceImpl.java | 5 +++-- .../impl/OilElectricRecordServiceImpl.java | 5 +++-- .../impl/OtherExpenseRecordServiceImpl.java | 5 +++-- .../impl/TransportChangeRecordServiceImpl.java | 5 +++-- 8 files changed, 60 insertions(+), 20 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java index d46141cc..884e5314 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java @@ -116,9 +116,18 @@ public class MileageRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入里程记录", description = "传入excel") public R importMileageRecord(MultipartFile file, HttpServletResponse response) { - List failureList = mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MileageRecordExcel.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 { + mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MileageRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java index ee3caae1..8254949b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java @@ -94,9 +94,18 @@ public class OilElectricRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入油电记录", description = "传入excel") public R importOilElectricRecord(MultipartFile file, HttpServletResponse response) { - List failureList = oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OilElectricRecordExcel.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 { + oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OilElectricRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java index 3ea8fb5a..9e48fdd6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java @@ -94,9 +94,18 @@ public class OtherExpenseRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入其他费用记录", description = "传入excel") public R importOtherExpenseRecord(MultipartFile file, HttpServletResponse response) { - List failureList = otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OtherExpenseRecordExcel.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 { + otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OtherExpenseRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java index 10848fa9..93e10a83 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java @@ -113,9 +113,18 @@ public class TransportChangeRecordController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入变更记录", description = "传入excel") public R importTransportChangeRecord(MultipartFile file, HttpServletResponse response) { - List failureList = transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "变更记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportChangeRecordExcel.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 { + transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportChangeRecordExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java index 0e72973a..701d7030 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java @@ -105,8 +105,9 @@ public class MileageRecordServiceImpl extends BaseServiceImpl Date: Sat, 26 Sep 2026 10:58:15 +0800 Subject: [PATCH 12/16] =?UTF-8?q?:bug:=20=E5=8F=B0=E8=B4=A6/=E4=B8=BB?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=B1=BB=E5=AF=BC=E5=85=A5=EF=BC=884=20?= =?UTF-8?q?=E4=B8=AA=E6=A8=A1=E5=9D=97=EF=BC=89=E5=A4=B1=E8=B4=A5=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E8=BF=94=E5=9B=9E=E5=85=A8=E9=87=8F=E5=8E=9F=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖:设备台账 / 保养计划(blade-transport)、铁路站点 / 机场主数据(blade-system)。 问题同前两批:收集 errorList 后 setRollbackOnly 并 return,控制器据此导出 「仅错误行」——失败明细不是原表全量,用户无法对照原表修正。 改动(同构): - 服务层:抛 ImportFailureException(data),明细携带原表全部行; 回滚由异常触发(替代 setRollbackOnly)。注意 blade-system 的两个模块 用的是静态导入 TransactionAspectSupport,模式略有差异。 - 控制器:catch 异常取 getFailureList() 导出(用 export 保留按列标红), 补上传文件校验。 验证:mvn install -DskipTests 通过;台账回归 32/32,记录类回归 80/80。 --- .../controller/AirportMasterController.java | 8 +++++--- .../controller/RailwayStationController.java | 8 +++++--- .../service/impl/AirportMasterServiceImpl.java | 5 +++-- .../service/impl/RailwayStationServiceImpl.java | 5 +++-- .../controller/EquipmentLedgerController.java | 15 ++++++++++++--- .../controller/MaintenancePlanController.java | 15 ++++++++++++--- .../service/impl/EquipmentLedgerServiceImpl.java | 5 +++-- .../service/impl/MaintenancePlanServiceImpl.java | 5 +++-- 8 files changed, 46 insertions(+), 20 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java index 59950862..42374fa2 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java @@ -157,9 +157,11 @@ public class AirportMasterController extends BladeController { if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { return R.fail("请上传 .xls,.xlsx 标准格式文件"); } - List failureList = airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "空港机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AirportMasterExcel.class); + try { + airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AirportMasterExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java index 7bc1f8c2..2e643118 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java @@ -160,9 +160,11 @@ public class RailwayStationController extends BladeController { if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { return R.fail("请上传 .xls,.xlsx 标准格式文件"); } - List failureList = railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "铁路车站主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RailwayStationExcel.class); + try { + railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RailwayStationExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java index 55bdeb12..815ca194 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java @@ -160,8 +160,9 @@ public class AirportMasterServiceImpl extends BaseServiceImpl failureList = equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EquipmentLedgerExcel.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 { + equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EquipmentLedgerExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java index b279ccf4..44970f18 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java @@ -128,9 +128,18 @@ public class MaintenancePlanController extends BladeController { @ApiOperationSupport(order = 5) @Operation(summary = "导入保养记录", description = "传入excel") public R importMaintenancePlan(MultipartFile file, HttpServletResponse response) { - List failureList = maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenancePlanExcel.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 { + maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenancePlanExcel.class); return null; } return R.success("操作成功"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java index cd1eccb5..f1051332 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java @@ -114,8 +114,9 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl Date: Sat, 26 Sep 2026 11:10:37 +0800 Subject: [PATCH 13/16] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BF=90?= =?UTF-8?q?=E8=BE=93=E5=AF=B9=E8=B4=A6=E5=AF=BC=E5=85=A5=E6=97=A0=E5=9B=9E?= =?UTF-8?q?=E6=BB=9A=E4=B8=8E=E8=BF=90=E8=BE=93=E8=AE=A1=E5=88=92=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E4=B8=8D=E5=85=A8=EF=BC=88=E6=89=B9=E6=AC=A14?= =?UTF-8?q?=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("外部账单保存失败"); } From 2d185f0de64a514382c1e86fce541cb0bf5fda41 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 11:37:07 +0800 Subject: [PATCH 14/16] =?UTF-8?q?:bug:=20=E5=AF=BC=E5=85=A5=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=A0=BC=E6=97=A0=E6=B3=95=E8=BD=AC=E6=8D=A2=E6=97=B6?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E8=BF=94=E5=9B=9E=E5=8F=AF=E8=AF=BB=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=98=8E=E7=BB=86=EF=BC=8C=E5=85=9C=E5=BA=95=2026=20?= =?UTF-8?q?=E4=B8=AA=E5=AF=BC=E5=85=A5=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(用户实测反馈):金额列填「一万二千元」这类无法转换的内容时,EasyExcel 在 ExcelUtil.read(...) 阶段就抛 ExcelDataConvertException,根本走不到行级校验; 控制器只 catch ImportFailureException,异常穿透到全局处理器,用户看到的是 HTTP 500 + 「Convert data ... to class java.math.BigDecimal error」这类技术文案, 且完全拿不到失败明细。 影响面:全部 27 个导入接口(读阶段的 ExcelUtil.read 都在 try 内、只 catch ImportFailureException),非维修记录独有。 改动: - ImportFailureExcelUtil 新增三个能力(blade-common,公共层一次覆盖各模块): 1) describeReadFailure:把 ExcelDataConvertException 转成 「第N行「列名」格式不正确:<原值>」,列名按列序号反查 @ExcelProperty; 2) readRawRows:以原始文本(Map)方式重读文件,不做类型转换因此绝不抛异常, 用于拿回「原表全部行」; 3) exportReadFailure:一站式兜底(重读 → 导出全量行 + 仅出错行带原因 + 按列标红),返回 false 表示非转换类异常,调用方继续抛出保持原行为。 - 26 个导入控制器在现有 catch 之后追加 catch(Exception) 兜底,未改动原有 行级校验与整批回滚逻辑。 验证: - 实测维修记录/费用项/币种汇率三个模块:HTTP 500→200,返回全量明细, 文案如「第5行「费用」格式不正确:一万二千元」(行号经单行/四行文件双向校准)。 - 既有回归无退化:记录类 80/80、台账 32/32、港口码头 45/45、货物类型 24/24、 费用项 20/20、币种汇率 25/25。 - 新增 scripts/api-read-failure-test.sh(4 断言)固化该场景。 --- .../common/excel/ImportFailureExcelUtil.java | 342 ++++++++++++++++++ .../controller/AirportMasterController.java | 9 + .../controller/CargoTypeController.java | 9 + .../system/controller/CurrencyController.java | 9 + .../system/controller/FeeItemController.java | 9 + .../controller/RailwayStationController.java | 9 + .../system/controller/RegionController.java | 9 + .../controller/AccidentRecordController.java | 9 + .../AnnualInspectionRecordController.java | 9 + .../controller/CommonCargoController.java | 9 + .../controller/CommonRouteController.java | 9 + .../CreditScoreQuantificationController.java | 9 + .../controller/EquipmentLedgerController.java | 9 + .../controller/EtcRecordController.java | 9 + .../controller/InsuranceRecordController.java | 9 + .../controller/MaintenancePlanController.java | 9 + .../MaintenanceRecordController.java | 9 + .../controller/MileageRecordController.java | 9 + .../OilElectricRecordController.java | 9 + .../OtherExpenseRecordController.java | 9 + .../TireReplacementRecordController.java | 9 + .../TransportChangeRecordController.java | 9 + .../controller/TransportPlanController.java | 9 + .../TransportReconciliationController.java | 18 + .../controller/ViolationRecordController.java | 9 + .../controller/WaybillController.java | 9 + 26 files changed, 576 insertions(+) diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index 6b0ad40d..d4330eed 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -80,6 +80,348 @@ public class ImportFailureExcelUtil { addValidationError(validationErrors, value != null && value.length() > maxLength, message); } + /** + * 把「读取 Excel 阶段」的转换异常转成可读的失败原因。 + *

+ * 背景:单元格里填了无法转换的内容(如金额列填「一万二千元」)时,EasyExcel 会在 + * {@code ExcelUtil.read(...)} 阶段就抛 {@link cn.idev.excel.exception.ExcelDataConvertException}, + * 根本走不到行级校验;若不兜底,用户看到的是 + * 「Convert data ... to class java.math.BigDecimal error」这类技术文案。 + *

+ * 本方法据异常里的行列位置与目标类型,生成「第N行「列名」格式不正确:<原值>」这样 + * 用户能看懂、能定位的文案;列名靠表头反射拿 {@code @ExcelProperty},拿不到则退回列序号。 + * + * @param exception 读取阶段抛出的异常 + * @param excelClass 导入模型类(用于按列序号反查表头名) + * @return 可读的失败原因;非转换类异常返回 null(交由上层按原样处理) + */ + public static String describeReadFailure(Throwable exception, Class excelClass) { + cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, + cn.idev.excel.exception.ExcelDataConvertException.class); + if (convertException == null) { + return null; + } + Integer rowIndex = convertException.getRowIndex(); + Integer columnIndex = convertException.getColumnIndex(); + String columnName = columnName(excelClass, columnIndex); + // Excel 行号:EasyExcel 的 rowIndex 为 1 基数据行号(表头占第 1 行),故 Excel 行号 = rowIndex + 1 + String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行"; + String columnText = columnName == null ? ("第" + (columnIndex == null ? "?" : columnIndex + 1) + "列") : "「" + columnName + "」"; + String value = cellValue(convertException); + StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确"); + if (value != null && !value.isBlank()) { + message.append(":").append(value); + } + return message.toString(); + } + + /** + * 在异常链里查找指定类型的 cause + */ + private static T findCause(Throwable throwable, Class type) { + for (Throwable current = throwable; current != null; current = current.getCause()) { + if (type.isInstance(current)) { + return type.cast(current); + } + if (current.getCause() == current) { + break; + } + } + return null; + } + + /** + * 按列序号反查表头名(@ExcelProperty 的 value,取不到则用字段名) + */ + private static String columnName(Class excelClass, Integer columnIndex) { + if (excelClass == null || columnIndex == null) { + return null; + } + List fields = excelFields(excelClass); + if (columnIndex < 0 || columnIndex >= fields.size()) { + return null; + } + Field field = fields.get(columnIndex); + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty == null) { + return field.getName(); + } + String[] value = excelProperty.value(); + String columnName = value.length == 0 ? field.getName() : value[0]; + return columnName.replace("*", ""); + } + + /** + * 取触发转换失败的单元格原值 + */ + private static String cellValue(cn.idev.excel.exception.ExcelDataConvertException exception) { + try { + cn.idev.excel.metadata.data.CellData cellData = exception.getCellData(); + if (cellData == null) { + return null; + } + Object data = cellData.getData(); + if (data == null) { + data = cellData.getStringValue(); + } + return data == null ? null : String.valueOf(data).trim(); + } catch (Exception ignored) { + return null; + } + } + + /** + * 以「原始单元格文本」方式重读导入文件,得到的行用于构造读取阶段的失败明细。 + *

+ * 用途:当 {@code ExcelUtil.read(file, XxxExcel.class)} 因单元格无法转换而失败时, + * 已解析的实体列表是不完整的(异常中断了整批读取)。此时用本方法重读一次: + * 不做类型转换(直接读成 Map),因此绝不会再抛转换异常,可拿到**原表全部行**, + * 供导出「原表全量 + 仅出错行带原因」的失败明细。 + * + * @param file 上传的 Excel 文件 + * @param excelClass 导入模型类(按列序号反查表头名,用于把列序号映射成列名) + * @return 按行组织的明细数据:每行是「表头名 → 单元格文本」的有序 Map; + * 读不出内容时返回空列表(调用方按空明细处理) + */ + public static List> readRawRows(java.io.InputStream file, Class excelClass) { + List> rawRows = new ArrayList<>(); + try { + cn.idev.excel.read.builder.ExcelReaderBuilder builder = cn.idev.excel.FastExcel + .read(file, new cn.idev.excel.event.AnalysisEventListener>() { + @Override + public void invoke(Map data, cn.idev.excel.context.AnalysisContext context) { + rawRows.add(new java.util.LinkedHashMap<>(data)); + } + + @Override + public void doAfterAllAnalysed(cn.idev.excel.context.AnalysisContext context) { + // 无需处理 + } + }); + builder.headRowNumber(1).sheet().doRead(); + } catch (Exception ignored) { + // 重读本身失败时退回空明细,由调用方按「无法读取」提示 + return new ArrayList<>(); + } + List fields = excelClass == null ? List.of() : excelFields(excelClass); + List> rows = new ArrayList<>(); + for (Map rawRow : rawRows) { + Map row = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : rawRow.entrySet()) { + Integer columnIndex = entry.getKey(); + String columnName = columnIndex != null && columnIndex < fields.size() + ? columnName(excelClass, columnIndex) + : null; + if (columnName == null) { + columnName = "第" + (columnIndex == null ? "?" : columnIndex + 1) + "列"; + } + row.put(columnName, entry.getValue()); + } + rows.add(row); + } + return rows; + } + + /** + * 导出「读取阶段失败」明细:原表全量行 + 末尾「导入失败原因」列。 + *

+ * 与 {@link #export} 的区别:本方法直接按重读得到的「表头名 → 单元格文本」写出, + * 不依赖导入模型的字段反射,因此适用于「单元格无法转换为实体字段」的场景 + * (此时实体列表不完整,无法走常规导出)。 + * + * @param response 响应 + * @param fileName 文件名 + * @param sheetName 工作表名 + * @param rawRows {@link #readRawRows} 得到的原表全部行 + * @param failureMessageByRow 行号(0 基,对应 rawRows 下标)→ 该行失败原因;为 null 表示该行无错 + */ + public static void exportRawFailure(HttpServletResponse response, String fileName, String sheetName, + List> rawRows, Map failureMessageByRow) { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + response.setHeader("Content-disposition", "attachment;filename=" + encodeFileName + ".xlsx"); + List headers = new ArrayList<>(); + for (Map row : rawRows) { + for (String key : row.keySet()) { + if (!headers.contains(key)) { + headers.add(key); + } + } + } + headers.add(FAILURE_REASON); + List> head = new ArrayList<>(); + for (String header : headers) { + head.add(List.of(header)); + } + List> rows = new ArrayList<>(); + Map> redColumnsByRow = new HashMap<>(); + for (int rowIndex = 0; rowIndex < rawRows.size(); rowIndex++) { + Map rawRow = rawRows.get(rowIndex); + List row = new ArrayList<>(); + Set redColumns = new LinkedHashSet<>(); + // 失败原因列恒标红 + redColumns.add(headers.size() - 1); + String failureMessage = failureMessageByRow == null ? null : failureMessageByRow.get(rowIndex); + for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) { + String header = headers.get(columnIndex); + row.add(rawRow.get(header)); + // 失败原因里出现该列名时,标红该列单元格 + if (failureMessage != null && !failureMessage.isBlank() && failureMessage.contains("「" + header + "」")) { + redColumns.add(columnIndex); + } + } + row.add(failureMessage == null ? "" : failureMessage); + rows.add(row); + redColumnsByRow.put(rowIndex, redColumns); + } + try { + FastExcel.write(response.getOutputStream()) + .registerWriteHandler(new RawFailureCellStyleHandler(rows, redColumnsByRow)) + .head(head) + .sheet(sheetName) + .doWrite(rows); + } catch (IOException exception) { + throw new IllegalStateException("导出导入失败明细失败", exception); + } + } + + /** + * 读取阶段失败明细的单元格样式处理器:统一列宽 + 标红失败原因列与出错列 + */ + private static class RawFailureCellStyleHandler implements CellWriteHandler { + + private final int failureReasonColumnIndex; + private final Map> redColumnsByRow; + private final Map redStyleCache = new HashMap<>(); + private final Map noWrapStyleCache = new HashMap<>(); + private final Map columnWidthCache = new HashMap<>(); + + private RawFailureCellStyleHandler(List> rows, Map> redColumnsByRow) { + this.failureReasonColumnIndex = rows.isEmpty() ? 0 : rows.get(0).size() - 1; + this.redColumnsByRow = redColumnsByRow; + } + + @Override + public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, + List> cellDataList, Cell cell, cn.idev.excel.metadata.Head head, + Integer relativeRowIndex, Boolean isHead) { + if (Boolean.TRUE.equals(isHead)) { + adjustColumnWidth(cell); + markNoWrap(cell); + return; + } + adjustColumnWidth(cell); + if (relativeRowIndex == null || relativeRowIndex < 0) { + return; + } + Set redColumns = redColumnsByRow.get(relativeRowIndex); + if (redColumns != null && redColumns.contains(cell.getColumnIndex())) { + markRed(cell); + } + } + + private void markRed(Cell cell) { + CellStyle currentStyle = cell.getCellStyle(); + CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { + Workbook workbook = cell.getSheet().getWorkbook(); + CellStyle newStyle = workbook.createCellStyle(); + newStyle.cloneStyleFrom(currentStyle); + Font font = workbook.createFont(); + font.setColor(IndexedColors.RED.getIndex()); + newStyle.setFont(font); + newStyle.setWrapText(true); + return newStyle; + }); + cell.setCellStyle(redStyle); + } + + private void markNoWrap(Cell cell) { + CellStyle currentStyle = cell.getCellStyle(); + CellStyle noWrapStyle = noWrapStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { + CellStyle newStyle = cell.getSheet().getWorkbook().createCellStyle(); + newStyle.cloneStyleFrom(currentStyle); + newStyle.setWrapText(false); + return newStyle; + }); + cell.setCellStyle(noWrapStyle); + } + + private void adjustColumnWidth(Cell cell) { + int columnIndex = cell.getColumnIndex(); + int columnWidth = Math.min(Math.max(displayWidth(cell.toString()) + 4, 12), 80) * 256; + Integer currentWidth = columnWidthCache.get(columnIndex); + if (currentWidth == null || columnWidth > currentWidth) { + columnWidthCache.put(columnIndex, columnWidth); + cell.getSheet().setColumnWidth(columnIndex, columnWidth); + } + } + + private int displayWidth(String value) { + int width = 0; + for (int index = 0; index < value.length(); index++) { + width += value.charAt(index) > 255 ? 2 : 1; + } + return width; + } + } + + /** + * 读取阶段异常的一站式兜底:重读原表 → 导出「全量行 + 出错行原因」失败明细。 + *

+ * 控制器用法:在导入的 try 块末尾追加 + *

+	 * } catch (Exception exception) {
+	 *     if (ImportFailureExcelUtil.exportReadFailure(response, fileName, sheetName, file, XxxExcel.class, exception)) {
+	 *         return null;
+	 *     }
+	 *     throw exception;
+	 * }
+	 * 
+ * + * @return true 表示已识别为「单元格转换失败」并已写出明细(调用方直接 return null); + * false 表示不是转换类异常(调用方应继续抛出,保持原有处理) + */ + public static boolean exportReadFailure(HttpServletResponse response, String fileName, String sheetName, + org.springframework.web.multipart.MultipartFile file, + Class excelClass, Throwable exception) { + String failureMessage = describeReadFailure(exception, excelClass); + if (failureMessage == null) { + return false; + } + List> rawRows; + try (java.io.InputStream inputStream = file.getInputStream()) { + rawRows = readRawRows(inputStream, excelClass); + } catch (IOException ioException) { + rawRows = new ArrayList<>(); + } + Map failureMessageByRow = new HashMap<>(); + if (rawRows.isEmpty()) { + // 重读也拿不到内容时,退化为「仅失败原因」一行,至少让用户看到可读原因 + Map single = new java.util.LinkedHashMap<>(); + single.put(FAILURE_REASON, failureMessage); + rawRows.add(single); + failureMessageByRow.put(0, failureMessage); + exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow); + return true; + } + // 异常里的数据行下标(0 基)用于把原因挂到对应行 + Integer rowIndex = readFailureRowIndex(exception); + int targetRow = rowIndex == null ? 0 : Math.max(0, Math.min(rowIndex, rawRows.size() - 1)); + failureMessageByRow.put(targetRow, failureMessage); + exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow); + return true; + } + + /** + * 取转换异常中的数据行下标(0 基) + */ + private static Integer readFailureRowIndex(Throwable exception) { + cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, + cn.idev.excel.exception.ExcelDataConvertException.class); + return convertException == null ? null : convertException.getRowIndex(); + } + private static final String FAILURE_REASON = "导入失败原因"; private static final String ERROR_MESSAGE_FIELD = "errorMessage"; private static final String FAILURE_REASON_FIELD = "failureReason"; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java index 42374fa2..355aaee9 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java @@ -163,6 +163,15 @@ public class AirportMasterController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AirportMasterExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", file, AirportMasterExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 616e1ae6..cf73d38c 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 @@ -169,6 +169,15 @@ public class CargoTypeController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 exportFailure(response, exception.getFailureList()); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoTypeExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } 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 be6ef0f5..8b6b6c44 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 @@ -162,6 +162,15 @@ public class CurrencyController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CurrencyExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", file, CurrencyExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 2bf7ea2d..d70330bd 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 @@ -151,6 +151,15 @@ public class FeeItemController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 ImportFailureExcelUtil.exportFailureReasonOnly(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), FeeItemExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", file, FeeItemExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java index 2e643118..34149e9d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java @@ -166,6 +166,15 @@ public class RailwayStationController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RailwayStationExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", file, RailwayStationExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 c0ab7473..9f0ec270 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 @@ -205,6 +205,15 @@ public class RegionController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RegionExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", file, RegionExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java index 7a6e7140..8744be2b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AccidentRecordController.java @@ -141,6 +141,15 @@ public class AccidentRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AccidentRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AccidentRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java index 1412b173..f598e3c2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/AnnualInspectionRecordController.java @@ -138,6 +138,15 @@ public class AnnualInspectionRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AnnualInspectionRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AnnualInspectionRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 212ca3a9..7cc90b6e 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 @@ -123,6 +123,15 @@ public class CommonCargoController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 exportFailure(response, exception.getFailureList()); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonCargoExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } 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 db31a347..98cf2900 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 @@ -129,6 +129,15 @@ public class CommonRouteController extends BladeController { CommonRouteImportExcel.class ); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "常用线路导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonRouteImportExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } 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 d10852ba..86e93d7b 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 @@ -151,6 +151,15 @@ public class CreditScoreQuantificationController extends BladeController { // 任一分组失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CreditScoreQuantificationExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", file, CreditScoreQuantificationExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EquipmentLedgerController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EquipmentLedgerController.java index 17ad0f6c..c4554e4b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EquipmentLedgerController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EquipmentLedgerController.java @@ -103,6 +103,15 @@ public class EquipmentLedgerController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EquipmentLedgerExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", file, EquipmentLedgerExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java index fb3613cc..c67992b3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/EtcRecordController.java @@ -107,6 +107,15 @@ public class EtcRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EtcRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", file, EtcRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java index 4bf40c00..8c4f414a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InsuranceRecordController.java @@ -142,6 +142,15 @@ public class InsuranceRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), InsuranceRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", file, InsuranceRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java index 44970f18..a77493c6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenancePlanController.java @@ -141,6 +141,15 @@ public class MaintenancePlanController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenancePlanExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenancePlanExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java index 51db7236..42f12239 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MaintenanceRecordController.java @@ -141,6 +141,15 @@ public class MaintenanceRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenanceRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenanceRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java index 884e5314..68dbf53a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MileageRecordController.java @@ -129,6 +129,15 @@ public class MileageRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MileageRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MileageRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java index 8254949b..f2b37b13 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OilElectricRecordController.java @@ -107,6 +107,15 @@ public class OilElectricRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OilElectricRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OilElectricRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java index 9e48fdd6..d6b70a5a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/OtherExpenseRecordController.java @@ -107,6 +107,15 @@ public class OtherExpenseRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OtherExpenseRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OtherExpenseRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java index 608d30ec..9db40c33 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TireReplacementRecordController.java @@ -141,6 +141,15 @@ public class TireReplacementRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TireReplacementRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TireReplacementRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java index 93e10a83..87200541 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportChangeRecordController.java @@ -126,6 +126,15 @@ public class TransportChangeRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportChangeRecordExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportChangeRecordExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 39c3580b..f8f8fe07 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 @@ -161,6 +161,15 @@ public class TransportPlanController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportPlanImportExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportPlanImportExcel.class, readException)) { + return null; + } + throw readException; } 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 2baf5686..97242618 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 @@ -143,6 +143,15 @@ public class TransportReconciliationController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), VehicleReconciliationExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, VehicleReconciliationExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } @@ -167,6 +176,15 @@ public class TransportReconciliationController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CargoReconciliationExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoReconciliationExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java index 0a87e8ec..d9a3e4c4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ViolationRecordController.java @@ -140,6 +140,15 @@ public class ViolationRecordController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。 org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), ViolationRecordImportExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", file, ViolationRecordImportExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } 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 de0ada47..e66277bb 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 @@ -256,6 +256,15 @@ public class WaybillController extends BladeController { // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 ImportFailureExcelUtil.exportFailureReasonOnly(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), WaybillExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", file, WaybillExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } From 9bf7fdfe9960dfbfaf80f759986c4e9c13af0de9 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 11:50:57 +0800 Subject: [PATCH 15/16] =?UTF-8?q?:zap:=20=E7=A9=BA=E8=A1=A8=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=8F=90=E7=A4=BA=E6=94=B9=E4=B8=BA=E3=80=8C=E6=9C=AA?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=88=B0=E6=95=B0=E6=8D=AE=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E8=AF=B7=E7=A1=AE=E8=AE=A4=E8=A1=A8=E5=A4=B4=E4=B8=8B=E6=96=B9?= =?UTF-8?q?=E5=B7=B2=E5=A1=AB=E5=86=99=E5=86=85=E5=AE=B9=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原提示「导入数据不能为空」在用户上传「只有表头、未填数据行」的文件时容易误解 (文件并非空文件,只是没填内容)。全项目 27 个模块、30 处统一替换该文案。 验证:mvn install -DskipTests 通过;空表上传返回新文案; 13 个导入回归脚本 383 项断言全绿。 --- .../system/service/impl/AirportMasterServiceImpl.java | 2 +- .../system/service/impl/CargoTypeServiceImpl.java | 2 +- .../system/service/impl/CurrencyServiceImpl.java | 2 +- .../springblade/system/service/impl/FeeItemServiceImpl.java | 2 +- .../system/service/impl/PortTerminalServiceImpl.java | 2 +- .../system/service/impl/RailwayStationServiceImpl.java | 2 +- .../springblade/system/service/impl/RegionServiceImpl.java | 2 +- .../transport/service/impl/AccidentRecordServiceImpl.java | 2 +- .../service/impl/AnnualInspectionRecordServiceImpl.java | 2 +- .../transport/service/impl/CommonCargoServiceImpl.java | 2 +- .../transport/service/impl/CommonRouteServiceImpl.java | 2 +- .../service/impl/CreditScoreQuantificationServiceImpl.java | 2 +- .../transport/service/impl/EquipmentLedgerServiceImpl.java | 2 +- .../transport/service/impl/EtcRecordServiceImpl.java | 2 +- .../transport/service/impl/InsuranceRecordServiceImpl.java | 2 +- .../transport/service/impl/MaintenancePlanServiceImpl.java | 2 +- .../service/impl/MaintenanceRecordServiceImpl.java | 2 +- .../transport/service/impl/MileageRecordServiceImpl.java | 2 +- .../service/impl/OilElectricRecordServiceImpl.java | 2 +- .../service/impl/OtherExpenseRecordServiceImpl.java | 2 +- .../transport/service/impl/ShippingTemplateServiceImpl.java | 2 +- .../service/impl/TireReplacementRecordServiceImpl.java | 2 +- .../service/impl/TransportChangeRecordServiceImpl.java | 2 +- .../transport/service/impl/TransportPlanServiceImpl.java | 4 ++-- .../service/impl/TransportReconciliationServiceImpl.java | 6 +++--- .../transport/service/impl/ViolationRecordServiceImpl.java | 2 +- .../transport/service/impl/WaybillServiceImpl.java | 2 +- 27 files changed, 30 insertions(+), 30 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java index 815ca194..ac098b40 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/AirportMasterServiceImpl.java @@ -127,7 +127,7 @@ public class AirportMasterServiceImpl extends BaseServiceImpl importAirportMaster(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List airportMasterList = new ArrayList<>(); 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 a1fc4f74..6bb8c133 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 @@ -138,7 +138,7 @@ public class CargoTypeServiceImpl extends BaseServiceImpl importCargoType(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回, 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 48aeedc9..c9763d91 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 @@ -118,7 +118,7 @@ public class CurrencyServiceImpl extends BaseServiceImpl importCurrency(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意:循环内除 save 外还有 rebuildEnabledValidity 这个派生写—— 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 4221d262..0e04fd17 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 @@ -106,7 +106,7 @@ public class FeeItemServiceImpl extends BaseServiceImpl @Transactional(rollbackFor = Exception.class) public List importFeeItem(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意不能只往 failureList 里塞数据就 return——异常被吞掉后方法会正常返回, diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java index c7a08632..740890dc 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/PortTerminalServiceImpl.java @@ -140,7 +140,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl importPortTerminal(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 List failureList = new ArrayList<>(); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java index f9a30a06..d2a99f45 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/RailwayStationServiceImpl.java @@ -129,7 +129,7 @@ public class RailwayStationServiceImpl extends BaseServiceImpl importRailwayStation(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List railwayStationList = new ArrayList<>(); 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 a251af29..64d51c1a 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 @@ -204,7 +204,7 @@ public class RegionServiceImpl extends ServiceImpl impleme @Transactional(rollbackFor = Exception.class) public List importRegion(List data, Boolean isCovered) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java index b06948bb..2ad50d99 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AccidentRecordServiceImpl.java @@ -86,7 +86,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl importAccidentRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List accidentRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java index b44450cb..c85e8423 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/AnnualInspectionRecordServiceImpl.java @@ -105,7 +105,7 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl importAnnualInspectionRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List annualInspectionRecordList = new ArrayList<>(); 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 7ab5bc04..9ba8a1c1 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 @@ -142,7 +142,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl importCommonCargo(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意:本方法虽已标注 @Transactional,但循环内把异常吞掉后正常 return, 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 505ffd51..52191bba 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 @@ -129,7 +129,7 @@ public class CommonRouteServiceImpl extends BaseServiceImpl importCommonRoute(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } // 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。 // 注意:本方法虽已标注 @Transactional,但循环内把异常吞掉后正常 return, 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 cebba5e9..b767a50d 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 @@ -196,7 +196,7 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl importCreditScoreQuantification(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } Map> dataMap = data.stream() .filter(row -> Func.isNotEmpty(trimToNull(row.getTableName()))) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java index f1051332..5fdb8559 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EquipmentLedgerServiceImpl.java @@ -79,7 +79,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl importEquipmentLedger(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List equipmentLedgerList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java index e3966bf0..3e804f85 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/EtcRecordServiceImpl.java @@ -61,7 +61,7 @@ public class EtcRecordServiceImpl extends BaseServiceImpl importEtcRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List etcRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java index 110abe75..6b81bb71 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InsuranceRecordServiceImpl.java @@ -110,7 +110,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl importInsuranceRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List insuranceRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenancePlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenancePlanServiceImpl.java index e543a048..b2b80d6f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenancePlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenancePlanServiceImpl.java @@ -80,7 +80,7 @@ public class MaintenancePlanServiceImpl extends BaseServiceImpl importMaintenancePlan(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List maintenancePlanList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java index d9835665..8e931e2f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MaintenanceRecordServiceImpl.java @@ -82,7 +82,7 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl importMaintenanceRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List maintenanceRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java index 701d7030..c81ab0e5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MileageRecordServiceImpl.java @@ -81,7 +81,7 @@ public class MileageRecordServiceImpl extends BaseServiceImpl importMileageRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List mileageRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java index f0d56ce1..1402c0ca 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OilElectricRecordServiceImpl.java @@ -70,7 +70,7 @@ public class OilElectricRecordServiceImpl extends BaseServiceImpl importOilElectricRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List oilElectricRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java index 1ea355c4..5c74cf93 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/OtherExpenseRecordServiceImpl.java @@ -67,7 +67,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl importOtherExpenseRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List otherExpenseRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java index d695b0b6..109024c8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ShippingTemplateServiceImpl.java @@ -173,7 +173,7 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl importGoods(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List failures = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java index 0739a129..ca459523 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TireReplacementRecordServiceImpl.java @@ -82,7 +82,7 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl importTireReplacementRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List tireReplacementRecordList = new ArrayList<>(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java index 4a5b0f98..9da15784 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportChangeRecordServiceImpl.java @@ -81,7 +81,7 @@ public class TransportChangeRecordServiceImpl extends BaseServiceImpl importTransportChangeRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List transportChangeRecordList = new ArrayList<>(); 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 d466d882..e0397027 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 @@ -198,7 +198,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl validateTransportPlan(List data, Long projectId, String projectName, Long contractId, String contractName, String customerName) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } if (Func.isEmpty(projectId)) { throw new ServiceException("项目不能为空"); @@ -250,7 +250,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl importTransportPlan(List data, Long projectId, String projectName, Long contractId, String contractName, String customerName) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } if (Func.isEmpty(projectId)) { throw new ServiceException("项目不能为空"); 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 2039b426..c22902cc 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 @@ -290,7 +290,7 @@ public class TransportReconciliationServiceImpl @Override @Transactional(rollbackFor = Exception.class) public List importVehicles(Long id, List rows) { - if (Func.isEmpty(rows)) throw new ServiceException("导入数据不能为空"); + if (Func.isEmpty(rows)) throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); TransportReconciliation bill = editable(id); if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账"); resetExternal(id); @@ -419,7 +419,7 @@ public class TransportReconciliationServiceImpl @Override @Transactional(rollbackFor = Exception.class) public List importCargoes(Long id, List rows) { - if (Func.isEmpty(rows)) throw new ServiceException("导入数据不能为空"); + if (Func.isEmpty(rows)) throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); TransportReconciliation bill = editable(id); if (!CARGO.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是货物明细对账"); resetExternal(id); @@ -487,7 +487,7 @@ public class TransportReconciliationServiceImpl private List validateImportCargo(CargoReconciliationExcel row) { List validationErrors = new ArrayList<>(); if (row == null) { - validationErrors.add("导入数据不能为空"); + validationErrors.add("未读取到数据行,请确认表头下方已填写内容"); return validationErrors; } org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java index 017d029d..b8454eed 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ViolationRecordServiceImpl.java @@ -91,7 +91,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl importViolationRecord(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); List violationRecordList = new ArrayList<>(); 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 889232ec..629c296e 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 @@ -901,7 +901,7 @@ public class WaybillServiceImpl extends BaseServiceImpl @Transactional(rollbackFor = Exception.class) public List importWaybill(List data) { if (Func.isEmpty(data)) { - throw new ServiceException("导入数据不能为空"); + throw new ServiceException("未读取到数据行,请确认表头下方已填写内容"); } List errorList = new ArrayList<>(); for (int index = 0; index < data.size(); index++) { From 4ccc28e9913fea9aacb3ac61cf51428f58bb2ed4 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 12:45:17 +0800 Subject: [PATCH 16/16] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E4=B8=89?= =?UTF-8?q?=E5=A4=84=E5=AF=BC=E5=85=A5=E6=A0=A1=E9=AA=8C=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E4=B8=8E=E5=A4=B1=E8=B4=A5=E6=98=8E=E7=BB=86=E8=A1=8C=E5=8F=B7?= =?UTF-8?q?=E9=94=99=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对全仓 31 处导入入口做了一次「导入校验 vs 新增/编辑校验」的一致性审计, 以下三处是审计中严重度最高的、且业务口径无歧义的缺陷: 1) 常用货物:型号必填只在新增/编辑生效,导入侧被放过 submit(CommonCargo) 走 validate(cargo, requireModel=true),而导入调 submit(cargo, false) —— 页面上存不进去的数据(型号为空)却能通过导入写进库。 CommonCargoExcel 的「型号」列也没标 *,用户按模板填根本不知道它必填。 改法:去掉 requireModel 开关(该参数已成死参数,留着只会让两边再次分叉), 导入与新增/编辑共用同一个 validate;模板表头改 *型号。 2) 运输对账:动态费用列填非数字被静默改成 0.00 VehicleReconciliationFeeReader / CargoReconciliationFeeReader 的 decimal() 在 catch (NumberFormatException) 后直接返回 ZERO —— 「一百元」变成 0.00 存进 feeItemsJson,参与结算金额与自动匹配,属静默写脏数据。 改法:抛新增的 ImportCellReadException(blade-common),带上行号/列名/原值, 由既有读取阶段兜底导出原表全量明细。 3) 运输计划:文案「数量/里程必须为正数」与规则不符 条件只判 < 0(0 是合法值),新增/编辑侧的文案是「不能小于0」。统一为「不能小于0」。 顺带修掉一个更基础的错位: ImportFailureExcelUtil.readFailureRowIndex 直接拿 ExcelDataConvertException.getRowIndex() 当失败明细的行下标,但前者是 【绝对 0 基行号】(表头=0),后者只对数据行计数。错误恰好落在最后一行时 靠 clamping 侥幸正确,落在首行就会把原因标到下一行 —— 实测首行出错时原因 被标到了第 3 行(应为第 2 行)。现统一换算(-1)并把两类异常收口到 readFailureExcelRowIndex。 验证: - mvn clean install -DskipTests 通过。 - 全套 16 个导入回归脚本 469 项断言全绿,其中: - 运单核对新增用例 04(首行读取失败须标在第 2 行)、05(动态费用列填中文 必须报错),运单核对套件 28 项全绿; - 常用货物新增用例 08(型号为空须整批回滚),套件 29 项全绿; - 读取失败套件 4 项全绿,证明行号修正未破坏既有断言。 --- .../common/excel/ImportCellReadException.java | 93 +++++++++++++++++++ .../common/excel/ImportFailureExcelUtil.java | 31 ++++++- .../excel/CargoReconciliationFeeReader.java | 11 ++- .../transport/excel/CommonCargoExcel.java | 2 +- .../excel/VehicleReconciliationFeeReader.java | 11 ++- .../service/impl/CommonCargoServiceImpl.java | 17 ++-- .../impl/TransportPlanServiceImpl.java | 8 +- 7 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java b/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java new file mode 100644 index 00000000..7b038ad0 --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java @@ -0,0 +1,93 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.common.excel; + +import org.springblade.core.log.exception.ServiceException; + +import java.io.Serial; + +/** + * 单元格读取失败异常 —— 用于「异常不是 fastexcel 抛出、但同样发生在读取阶段」的场景。 + *

+ * fastexcel 自身解析失败时抛 {@code ExcelDataConvertException},位置信息齐全, + * {@link ImportFailureExcelUtil#exportReadFailure} 可以直接定位到行与列。但有些模块 + * 在读取阶段有自己的解析逻辑(例如运输对账的动态费用列:模板列不固定,由 + * {@code VehicleReconciliationFeeReader} 自行按表头解析),这些代码如果只是 + * {@code catch (NumberFormatException)} 后返回 {@code BigDecimal.ZERO}, + * 脏数据就会静默入库(费用 0.00 还会参与结算金额与自动匹配)。 + *

+ * 正确做法是改抛本异常,把位置信息补齐,让导出工具能把原因挂到正确的行、标红正确的列。 + *

+ * 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。 + * + * @author Chill + */ +public class ImportCellReadException extends ServiceException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 出错的单元格所在的 Excel 行号,绝对 0 基(表头占 0,第一个数据行为 1)。 + * 与 {@code ExcelDataConvertException.getRowIndex()} 保持同一口径, + * 便于导出工具统一换算。 + */ + private final transient Integer rowIndex; + + /** + * 出错的列名(导入模板表头原文),会写进原因文案里,导出工具靠它标红对应列。 + */ + private final transient String columnName; + + public ImportCellReadException(Integer rowIndex, String columnName, String cellValue) { + super(buildMessage(rowIndex, columnName, cellValue)); + this.rowIndex = rowIndex; + this.columnName = columnName; + } + + /** + * 文案格式与 {@link ImportFailureExcelUtil#describeReadFailure} 保持一致: + * 「第N行「列名」格式不正确:<原值>」。列名必须包在「」里,导出工具才能标红该列。 + */ + private static String buildMessage(Integer rowIndex, String columnName, String cellValue) { + String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行"; + String columnText = columnName == null || columnName.isBlank() ? "该列" : "「" + columnName + "」"; + StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确"); + if (cellValue != null && !cellValue.isBlank()) { + message.append(":").append(cellValue); + } + return message.toString(); + } + + public Integer getRowIndex() { + return rowIndex; + } + + public String getColumnName() { + return columnName; + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index d4330eed..955cc989 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -99,12 +99,15 @@ public class ImportFailureExcelUtil { cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, cn.idev.excel.exception.ExcelDataConvertException.class); if (convertException == null) { - return null; + // 不是 fastexcel 抛的转换异常:模块自带的读取期解析逻辑(如运输对账的动态费用列) + // 用 ImportCellReadException 补齐了位置信息,直接透传它已经拼好的原因文案。 + ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class); + return cellReadException == null ? null : cellReadException.getMessage(); } Integer rowIndex = convertException.getRowIndex(); Integer columnIndex = convertException.getColumnIndex(); String columnName = columnName(excelClass, columnIndex); - // Excel 行号:EasyExcel 的 rowIndex 为 1 基数据行号(表头占第 1 行),故 Excel 行号 = rowIndex + 1 + // Excel 行号:getRowIndex() 是绝对 0 基行号(表头占 0),故 Excel 行号 = rowIndex + 1 String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行"; String columnText = columnName == null ? ("第" + (columnIndex == null ? "?" : columnIndex + 1) + "列") : "「" + columnName + "」"; String value = cellValue(convertException); @@ -414,12 +417,32 @@ public class ImportFailureExcelUtil { } /** - * 取转换异常中的数据行下标(0 基) + * 取读取阶段异常中「失败数据行」的下标(0 基,只对数据行计数),供挂失败原因使用。 + *

+ * 无论哪种异常,源信息都是绝对 0 基行号(表头占 0,第一个数据行为 1), + * 而这里要的下标是相对数据行的,故统一减 1。 + * 不减的话原因会整体下移一行:错误恰好落在最后一行时会因调用处的 clamping + * 侥幸正确,落在首行/中间行就会标到不该标的行上,误导用户去改正确的那一行。 */ private static Integer readFailureRowIndex(Throwable exception) { + Integer excelRowIndex = readFailureExcelRowIndex(exception); + return excelRowIndex == null ? null : Math.max(0, excelRowIndex - 1); + } + + /** + * 取读取阶段异常的绝对 0 基行号(表头 = 0)。 + *

+ * 两条来源统一到这里:fastexcel 自带的 {@code ExcelDataConvertException}, + * 以及模块自行解析时抛出的 {@link ImportCellReadException}。 + */ + private static Integer readFailureExcelRowIndex(Throwable exception) { cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, cn.idev.excel.exception.ExcelDataConvertException.class); - return convertException == null ? null : convertException.getRowIndex(); + if (convertException != null) { + return convertException.getRowIndex(); + } + ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class); + return cellReadException == null ? null : cellReadException.getRowIndex(); } private static final String FAILURE_REASON = "导入失败原因"; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java index e2802105..ee7af2f8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFeeReader.java @@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel; import cn.idev.excel.context.AnalysisContext; import cn.idev.excel.event.AnalysisEventListener; import cn.idev.excel.metadata.data.ReadCellData; +import org.springblade.common.excel.ImportCellReadException; import org.springblade.core.log.exception.ServiceException; import org.springframework.web.multipart.MultipartFile; @@ -60,10 +61,11 @@ public final class CargoReconciliationFeeReader { @Override public void invoke(Map> row, AnalysisContext context) { Map values = new LinkedHashMap<>(); + Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex(); if (freightColumn >= 0 && settlementColumn > freightColumn) { for (int column = freightColumn + 1; column < settlementColumn; column++) { String name = headers.get(column); - if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column))); + if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex)); } } feeItems.add(values); @@ -80,7 +82,7 @@ public final class CargoReconciliationFeeReader { .findFirst().orElse(-1); } - private BigDecimal decimal(ReadCellData cellData) { + private BigDecimal decimal(ReadCellData cellData, String columnName, Integer excelRowIndex) { if (cellData == null) return BigDecimal.ZERO.setScale(2); Object value = cellData.getData(); if (value == null) { @@ -95,7 +97,10 @@ public final class CargoReconciliationFeeReader { try { return new BigDecimal(String.valueOf(value).trim()).setScale(2); } catch (NumberFormatException exception) { - return BigDecimal.ZERO.setScale(2); + // 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配, + // 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。 + // 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。 + throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim()); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CommonCargoExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CommonCargoExcel.java index 4440dc7f..e5c26caa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CommonCargoExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CommonCargoExcel.java @@ -73,7 +73,7 @@ public class CommonCargoExcel implements Serializable { @ExcelProperty("规格") private String specification; - @ExcelProperty("型号") + @ExcelProperty("*型号") private String model; @ExcelProperty("单价") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java index e6d889a6..990e8973 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFeeReader.java @@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel; import cn.idev.excel.context.AnalysisContext; import cn.idev.excel.event.AnalysisEventListener; import cn.idev.excel.metadata.data.ReadCellData; +import org.springblade.common.excel.ImportCellReadException; import org.springblade.core.log.exception.ServiceException; import org.springframework.web.multipart.MultipartFile; @@ -60,10 +61,11 @@ public final class VehicleReconciliationFeeReader { @Override public void invoke(Map> row, AnalysisContext context) { Map values = new LinkedHashMap<>(); + Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex(); if (freightColumn >= 0 && settlementColumn > freightColumn) { for (int column = freightColumn + 1; column < settlementColumn; column++) { String name = headers.get(column); - if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column))); + if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex)); } } feeItems.add(values); @@ -80,7 +82,7 @@ public final class VehicleReconciliationFeeReader { .findFirst().orElse(-1); } - private BigDecimal decimal(ReadCellData cellData) { + private BigDecimal decimal(ReadCellData cellData, String columnName, Integer excelRowIndex) { if (cellData == null) return BigDecimal.ZERO.setScale(2); Object value = cellData.getData(); if (value == null) { @@ -95,7 +97,10 @@ public final class VehicleReconciliationFeeReader { try { return new BigDecimal(String.valueOf(value).trim()).setScale(2); } catch (NumberFormatException exception) { - return BigDecimal.ZERO.setScale(2); + // 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配, + // 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。 + // 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。 + throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim()); } } 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 9ba8a1c1..e7a5e11b 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 @@ -71,10 +71,6 @@ public class CommonCargoServiceImpl extends BaseServiceImpl