🐛 导入单元格无法转换时改为返回可读失败明细,兜底 26 个导入接口

问题(用户实测反馈):金额列填「一万二千元」这类无法转换的内容时,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 断言)固化该场景。
This commit is contained in:
2026-09-26 11:37:07 +08:00
parent be02a5e942
commit 2d185f0de6
26 changed files with 576 additions and 0 deletions
@@ -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("操作成功");
}
@@ -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("导入数据成功");
}
@@ -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("操作成功");
}
@@ -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("导入数据成功");
}
@@ -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("操作成功");
}
@@ -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("操作成功");
}