🐛 修复三处导入校验缺陷与失败明细行号错位
对全仓 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 项全绿,证明行号修正未破坏既有断言。
This commit is contained in:
+8
-3
@@ -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<Integer, ReadCellData<?>> row, AnalysisContext context) {
|
||||
Map<String, BigDecimal> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ public class CommonCargoExcel implements Serializable {
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
|
||||
@ExcelProperty("型号")
|
||||
@ExcelProperty("*型号")
|
||||
private String model;
|
||||
|
||||
@ExcelProperty("单价")
|
||||
|
||||
+8
-3
@@ -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<Integer, ReadCellData<?>> row, AnalysisContext context) {
|
||||
Map<String, BigDecimal> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-10
@@ -71,10 +71,6 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(CommonCargo commonCargo) {
|
||||
return submit(commonCargo, true);
|
||||
}
|
||||
|
||||
private boolean submit(CommonCargo commonCargo, boolean requireModel) {
|
||||
boolean created = Func.isEmpty(commonCargo.getId());
|
||||
if (!created) {
|
||||
CommonCargo oldRecord = loadEditable(commonCargo.getId(), true);
|
||||
@@ -82,7 +78,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
|
||||
commonCargo.setDeptName(oldRecord.getDeptName());
|
||||
}
|
||||
prepare(commonCargo);
|
||||
validate(commonCargo, requireModel);
|
||||
validate(commonCargo);
|
||||
return saveOrUpdate(commonCargo);
|
||||
}
|
||||
|
||||
@@ -173,7 +169,7 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
|
||||
commonCargo.setDescriptionOne(TransportBusinessSupport.trimToNull(excel.getDescriptionOne()));
|
||||
commonCargo.setRemark(TransportBusinessSupport.trimToNull(excel.getRemark()));
|
||||
commonCargo.setDataSource("批量导入");
|
||||
submit(commonCargo, false);
|
||||
submit(commonCargo);
|
||||
} catch (Exception exception) {
|
||||
// 除收集到 failureList 外,还必须把原因写回原行对象:
|
||||
// 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因,
|
||||
@@ -315,14 +311,15 @@ public class CommonCargoServiceImpl extends BaseServiceImpl<CommonCargoMapper, C
|
||||
return cargoValue != null && cargoValue.compareTo(BigDecimal.ONE.negate()) == 0 ? null : cargoValue;
|
||||
}
|
||||
|
||||
private void validate(CommonCargo commonCargo, boolean requireModel) {
|
||||
private void validate(CommonCargo commonCargo) {
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getFirstCargoTypeName(), "请选择一级货物类型");
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getSecondCargoTypeCode(), "请选择二级货物类型");
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getCargoName(), "请输入货物名称");
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getCargoCode(), "请输入货物编号");
|
||||
if (requireModel) {
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getModel(), "请输入型号");
|
||||
}
|
||||
// 型号必填:新增/编辑入口(/submit)一直要求,导入侧此前用 requireModel=false
|
||||
// 放过了它,导致「页面上存不进去、导入却能写进库」,与根文档
|
||||
// docs/import-spec.md 的「导入校验必须与新增/编辑一致」冲突,故统一为必填。
|
||||
TransportBusinessSupport.validateRequired(commonCargo.getModel(), "请输入型号");
|
||||
TransportBusinessSupport.validateLength(commonCargo.getFirstCargoTypeName(), 100, "一级货物类型不能超过100字");
|
||||
TransportBusinessSupport.validateLength(commonCargo.getFirstCargoTypeCode(), 100, "一级货物类型编码不能超过100字");
|
||||
TransportBusinessSupport.validateLength(commonCargo.getSecondCargoTypeName(), 100, "二级货物类型不能超过100字");
|
||||
|
||||
+5
-3
@@ -454,12 +454,14 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 校验数量和里程为正数
|
||||
// 5. 校验数量和里程非负。
|
||||
// 文案与规则必须一致:条件只拦 < 0(即 0 是合法值),就不能写「必须为正数」。
|
||||
// 新增/编辑走 TransportBusinessSupport.validateNonNegative,文案是「不能小于0」。
|
||||
if (excel.getQuantity() != null && excel.getQuantity().compareTo(BigDecimal.ZERO) < 0) {
|
||||
errors.add("数量必须为正数");
|
||||
errors.add("数量不能小于0");
|
||||
}
|
||||
if (excel.getMileage() != null && excel.getMileage().compareTo(BigDecimal.ZERO) < 0) {
|
||||
errors.add("里程必须为正数");
|
||||
errors.add("里程不能小于0");
|
||||
}
|
||||
|
||||
// 6. 校验计量单位(如果有值)
|
||||
|
||||
Reference in New Issue
Block a user