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