fix(oms): 物料解析防张冠李戴+导入三类校验合并报错,客户编码只认USER_MEMBER_CODE
一、出库业务单导入(ReservationStockInOrderImpl) - 物料不存在/客户不存在/批次库存不足三类校验全部收集完成后统一报错,不再分阶段中断 - materialCache 取用统一 trim,防 Excel 编号带空格导致校验被跳过 二、物料三级解析防张冠李戴(MaterialBaseInfoResolveService) - baseId 按主键查到的档案与明细编码不一致时不再信任 baseId,转库存ID反查/编码+组织兜底,两级落空才退回按ID档案,命中打WARN日志 - 背景:前端把库存ID传进 materialBaseInfoId,撞上编码=名称的脏档案主键后整行物料被覆盖成错误物料 三、客户编码口径(reservationStockOutOrder/ReservationStockOutOrderMapper.xml) - queryCustomerByCode 只认 USER.USER_MEMBER_CODE,不再放行 USER_SHIPPER.CUSTOMER_NC_CODE 四、明细查询补字段(reserveStockOutOrder/ReserveStockOutOrderMapper.xml) - resultMap 补 status/issue_status 映射,detailsList 查询补查 a.status, a.issue_status
This commit is contained in:
+22
-4
@@ -5,6 +5,7 @@ import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMateria
|
||||
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.mapper.ReservationStockInOrderMapper;
|
||||
import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,6 +14,7 @@ import org.springframework.stereotype.Service;
|
||||
* 解决客户端推单把 materialInventoryId 传进 materialBaseInfoId 字段、或漏传导致明细存脏ID的问题。
|
||||
* 三级解析:1.按baseId直查 → 2.按库存ID反查纠正 → 3.按物料编码+组织兜底
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MaterialBaseInfoResolveService {
|
||||
|
||||
@@ -30,11 +32,23 @@ public class MaterialBaseInfoResolveService {
|
||||
* @return 解析成功返回物料信息(调用方需用 po.getMaterialBaseInfoId() 回填纠正后的ID);全部失败返回 null
|
||||
*/
|
||||
public MaterialBaseInfoPO resolve(Long materialBaseInfoId, Long materialInventoryId, String materialCode, Long organizationId) {
|
||||
// 1. 优先按 baseId 直查(有效ID不受影响)
|
||||
// 1. 优先按 baseId 直查(有效ID不受影响)。
|
||||
// 防张冠李戴:客户端曾把库存ID传进 materialBaseInfoId,若该库存ID恰好撞上另一条有效档案的主键
|
||||
// (如库存1201撞上编码=名称的脏档案"水箱"),按ID查到的档案编码会与明细编码对不上。
|
||||
// 此时 baseId 不可信,继续走库存反查/编码兜底;两级都落空时才退回按ID查到的档案
|
||||
MaterialBaseInfoPO byId = null;
|
||||
if (materialBaseInfoId != null && materialBaseInfoId > 0) {
|
||||
MaterialBaseInfoPO po = reservationStockInOrderMapper.getinfo(materialBaseInfoId);
|
||||
if (po != null) {
|
||||
return po;
|
||||
if (StringUtils.isNotBlank(materialCode) && po.getMaterialCode() != null
|
||||
&& !po.getMaterialCode().trim().equals(materialCode.trim())) {
|
||||
log.warn("物料ID疑似误传:baseInfoId={} 按主键查到档案[{}({})],与明细编码[{}]不一致," +
|
||||
"转库存反查/编码兜底(客户端可能把库存ID传进了materialBaseInfoId)",
|
||||
materialBaseInfoId, po.getMaterialCode(), po.getMaterialName(), materialCode);
|
||||
byId = po;
|
||||
} else {
|
||||
return po;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. baseId 无效但带了库存ID → 反查库存表拿正确 baseInfoId(修复"库存ID传进baseId"场景)
|
||||
@@ -50,8 +64,12 @@ public class MaterialBaseInfoResolveService {
|
||||
}
|
||||
// 3. 兜底:按物料编码+组织查(修复 baseId=0 漏传场景)
|
||||
if (StringUtils.isNotBlank(materialCode) && organizationId != null) {
|
||||
return reservationStockInOrderMapper.getByCode(materialCode.trim(), organizationId);
|
||||
MaterialBaseInfoPO po = reservationStockInOrderMapper.getByCode(materialCode.trim(), organizationId);
|
||||
if (po != null) {
|
||||
return po;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// 编码不一致已证明 baseId 不可信,但库存反查/编码兜底都落空时,退回按ID查到的档案,保持原有兜底行为
|
||||
return byId;
|
||||
}
|
||||
}
|
||||
|
||||
+34
-32
@@ -297,16 +297,39 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 第三步:物料/客户存在性校验全部完成后,统一提示两类编号各自有哪些不存在 ======
|
||||
if (!missingMaterials.isEmpty() || !missingCustomers.isEmpty()) {
|
||||
List<String> problems = new ArrayList<>();
|
||||
if (!missingMaterials.isEmpty()) {
|
||||
problems.add("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息");
|
||||
// ====== 第三步:物料/客户/库存三类校验全部完成后统一报错(不再分阶段各自中断) ======
|
||||
List<String> problems = new ArrayList<>();
|
||||
if (!missingMaterials.isEmpty()) {
|
||||
problems.add("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息");
|
||||
}
|
||||
if (!missingCustomers.isEmpty()) {
|
||||
problems.add("客户[" + String.join("、", missingCustomers) + "]不存在,请先维护客户信息");
|
||||
}
|
||||
|
||||
// 库存预检:只对已存在的物料比较批次库存(不存在的物料问题已收集),不足的同样收集
|
||||
if (!productDetailList.isEmpty()) {
|
||||
Set<String> insufficientMaterials = new HashSet<>();
|
||||
for (ProductDetail productDetail : productDetailList) {
|
||||
// materialCache 的 key 是 trim 过的编码,取用时同样 trim,避免 Excel 编号带空格导致校验被静默跳过
|
||||
String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim();
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productCode);
|
||||
if (materialBaseInfoPO == null) {
|
||||
continue; // 物料不存在的问题已收集
|
||||
}
|
||||
BigDecimal requireQty = new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP);
|
||||
List<ReservationMaterialInventoryPO> inventoryList = stockInOrderMapper.getkcId(
|
||||
materialBaseInfoPO.getMaterialBaseInfoId(), requireQty, warehouseId);
|
||||
if (inventoryList == null || inventoryList.isEmpty()) {
|
||||
insufficientMaterials.add(productCode);
|
||||
}
|
||||
}
|
||||
if (!missingCustomers.isEmpty()) {
|
||||
problems.add("客户[" + String.join("、", missingCustomers) + "]不存在,请先维护客户信息");
|
||||
if (!insufficientMaterials.isEmpty()) {
|
||||
problems.add("物料[" + String.join("、", insufficientMaterials) + "]"
|
||||
+ (warehouseId != null && warehouseId > 0 ? "在当前仓库" : "") + "批次库存数量少于出库数量");
|
||||
}
|
||||
throw new Exception(String.join(";", problems) + ",维护完成后重新导入");
|
||||
}
|
||||
if (!problems.isEmpty()) {
|
||||
throw new Exception(String.join(";", problems) + ",请处理后重新导入");
|
||||
}
|
||||
|
||||
// ====== 后续处理(编号均已存在):货主归属校验、建单落库 ======
|
||||
@@ -332,28 +355,6 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 库存预检:所有物料的批次库存全部比较完成后再统一报错 ======
|
||||
// (原为逐行分配时立即抛错,首个不足即中断,排在后面的物料不再参与比较)
|
||||
if (!productDetailList.isEmpty()) {
|
||||
Set<String> insufficientMaterials = new HashSet<>();
|
||||
for (ProductDetail productDetail : productDetailList) {
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productDetail.getProductCode());
|
||||
if (materialBaseInfoPO == null) {
|
||||
continue; // 存在性已在前面统一校验
|
||||
}
|
||||
BigDecimal requireQty = new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP);
|
||||
List<ReservationMaterialInventoryPO> inventoryList = stockInOrderMapper.getkcId(
|
||||
materialBaseInfoPO.getMaterialBaseInfoId(), requireQty, warehouseId);
|
||||
if (inventoryList == null || inventoryList.isEmpty()) {
|
||||
insufficientMaterials.add(productDetail.getProductCode());
|
||||
}
|
||||
}
|
||||
if (!insufficientMaterials.isEmpty()) {
|
||||
throw new Exception("物料[" + String.join("、", insufficientMaterials) + "]"
|
||||
+ (warehouseId != null && warehouseId > 0 ? "在当前仓库" : "") + "批次库存数量少于出库数量!");
|
||||
}
|
||||
}
|
||||
|
||||
ReservationStockOutOrderDO reservationStockOutOrderDO = new ReservationStockOutOrderDO();
|
||||
// 生成出库业务单号
|
||||
String orderNumber = reservationStockOutOrderApplicationService.generateReserveOrderNumber();
|
||||
@@ -407,8 +408,9 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
for (ProductDetail productDetail : productDetailList) {
|
||||
ReservationOutMaterialDetailDO reservationOutMaterialDetailDO = new ReservationOutMaterialDetailDO();
|
||||
reservationOutMaterialDetailDO.setOutOrderNumber(reservationStockOutOrderDO.getOutOrderNumber());
|
||||
// 物料信息取自前置校验的缓存(已确认存在,避免逐行重复查库)
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productDetail.getProductCode());
|
||||
// 物料信息取自前置校验的缓存(已确认存在,避免逐行重复查库);key 与存入侧一致使用 trim 后的编码
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(
|
||||
productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim());
|
||||
reservationOutMaterialDetailDO.setQuantity(new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
|
||||
if (materialBaseInfoPO != null) {
|
||||
Long materialBaseInfoId = materialBaseInfoPO.getMaterialBaseInfoId();
|
||||
|
||||
+3
-5
@@ -471,15 +471,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<!-- 按客户编号+组织查询客户(用户表):出入库导入校验客户存在性并自动匹配客户名称,结果为空表示客户不存在。
|
||||
客户编码有两处维护:USER.USER_MEMBER_CODE(客户编码)与 USER_SHIPPER.CUSTOMER_NC_CODE(NC客户代码,即单据khdm),
|
||||
模板填写的客户编号两者任一命中即可匹配到客户 -->
|
||||
客户编码只认 USER.USER_MEMBER_CODE(客户编码);
|
||||
2026-09-03 起不再放行 USER_SHIPPER.CUSTOMER_NC_CODE(NC客户代码),避免会员/NC编码撞车导致不存在的客户编号误通过 -->
|
||||
<select id="queryCustomerByCode" resultType="com.mhd.common.core.domain.po.UserPo">
|
||||
select max(u.user_id) as user_id, max(u.user_name) as user_name, max(u.user_member_code) as user_member_code
|
||||
from NGWL_TEST_USER."USER" u
|
||||
left join NGWL_TEST_USER.USER_SHIPPER us
|
||||
on us.USER_ID = u.USER_ID and us.DEL_FLAG = 1
|
||||
where u.ORGANIZATION_ID = #{organizationId} and u.DEL_FLAG = 1
|
||||
and (u.USER_MEMBER_CODE = #{userMemberCode} or us.CUSTOMER_NC_CODE = #{userMemberCode})
|
||||
and u.USER_MEMBER_CODE = #{userMemberCode}
|
||||
</select>
|
||||
|
||||
<select id="countByOrderNumberPrefix" resultType="java.lang.Integer" parameterType="java.lang.String">
|
||||
|
||||
+3
-1
@@ -361,6 +361,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="topOrganizationId" column="top_organization_id" />
|
||||
<result property="uniqueId" column="unique_id" />
|
||||
<result property="outOrderNumber" column="out_order_number" />
|
||||
<result property="status" column="status" />
|
||||
<result property="issueStatus" column="issue_status" />
|
||||
<result property="materialBaseInfoId" column="material_base_info_id" />
|
||||
<result property="materialCode" column="material_code" />
|
||||
<result property="materialName" column="material_name" />
|
||||
@@ -452,7 +454,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="detailsList" parameterType="com.mhd.oms.domain.reserveStockOutOrder.repository.todo.ReserveStockOutOrderDO" resultMap="MaterialDetailResult">
|
||||
select
|
||||
b.*
|
||||
b.*, a.status, a.issue_status
|
||||
from reserve_stock_out_order a left join reserve_out_material_detail b on a.OUT_ORDER_NUMBER = b.OUT_ORDER_NUMBER
|
||||
<where>
|
||||
<include refid="selectStockOutOrderPo1"/>
|
||||
|
||||
Reference in New Issue
Block a user