编辑 | blame | 历史 | 原始文档

仓储物流模块业务分析报告

一、发现的问题与漏洞

1. 并发安全问题 🔴 高风险

问题描述
库存更新操作 updateQuantity 使用了数据库层面的 CAS(Compare And Set)防负库存,但缺乏分布式锁保护。

风险场景
场景:同一库存记录被两个出库单同时操作 时间线: T1: 线程A读取库存 quantity=100 T2: 线程B读取库存 quantity=100 T3: 线程A执行出库50,quantity=50 T4: 线程B执行出库60,quantity=-10(CAS会拦截)

当前实现(MesWmMaterialStockMapper.java:78-86):
java default int updateQuantity(Long id, BigDecimal count, boolean checkFlag) { LambdaUpdateWrapper<MesWmMaterialStockDO> updateWrapper = new LambdaUpdateWrapper<>() .eq(MesWmMaterialStockDO::getId, id) .setSql("quantity = quantity + " + count); if (checkFlag && count.compareTo(BigDecimal.ZERO) < 0) { updateWrapper.ge(MesWmMaterialStockDO::getQuantity, count.abs()); // CAS 防负库存 } return update(null, updateWrapper); }

问题
- CAS可以防止负库存,但无法防止超卖(两个线程同时检查库存充足,然后同时扣减)
- 缺少乐观锁版本号机制

建议修复
```java
// 方案1:添加乐观锁版本号字段
ALTER TABLE mes_wm_material_stock ADD COLUMN version INT DEFAULT 0;

// 方案2:使用 Redis 分布式锁(推荐高并发场景)
String lockKey = "stock:" + itemId + ":" + areaId;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
// 执行库存更新
} finally {
lock.unlock();
}
```


2. 库存唯一键设计问题 🟡 中风险

问题描述
文档中定义唯一键为 (itemId, warehouseId, locationId, areaId, batchId),但实际代码中 vendorId 也参与了组合查询。

风险
- 同一物料同一库位,不同供应商的入库会创建两条库存记录
- 可能导致库存记录膨胀,查询效率下降

代码证据(MesWmMaterialStockServiceImpl.java:141-146):
java MesWmMaterialStockDO existStock = materialStockMapper.selectByCompositeKey( itemId, warehouseId, locationId, areaId, batchId); // 注意:vendorId 未参与唯一键查询

建议
- 明确业务需求:是否需要按供应商区分库存?
- 如果需要,建议在唯一键中增加 vendorId
- 如果不需要,入库时不应创建新的库存记录


2. 单据取消后的库存处理 ✅ 已确认设计正确

设计决策
- 已完成状态(FINISHED)的单据**不允许取消**
- 只能在草稿、待审批、待执行等未完成状态取消
- 如需回退已完成的库存操作,应创建对应的冲销单据(如退货单)

代码证据
java // MesWmItemReceiptServiceImpl.java @Override public void cancelItemReceipt(Long id) { MesWmItemReceiptDO receipt = validateItemReceiptExists(id); // 已完成和已取消不允许取消 if (ObjectUtils.equalsAny(receipt.getStatus(), MesWmItemReceiptStatusEnum.FINISHED.getStatus(), MesWmItemReceiptStatusEnum.CANCELED.getStatus())) { throw exception(WM_ITEM_RECEIPT_CANCEL_NOT_ALLOWED); } // ... }

评价:这是正确的业务设计,避免了复杂的库存冲销逻辑。


4. 缺少事务回滚机制 🟡 中风险

问题描述
批量创建事务时,如果中途失败,已执行的事务不会回滚。

代码证据(MesWmTransactionServiceImpl.java:100-107):
java @Override @Transactional(rollbackFor = Exception.class) public void createTransactionList(List<MesWmTransactionSaveReqDTO> reqDTOs) { if (CollUtil.isEmpty(reqDTOs)) { throw exception(WM_TRANSACTION_LIST_EMPTY); } reqDTOs.forEach(this::createTransaction); // 逐个执行,部分成功不会回滚 }

分析
- 方法有 @Transactional 注解,理论上有事务保护
- 但如果 createTransaction 内部捕获异常未抛出,会导致部分成功

建议
- 确保所有异常都向上抛出
- 考虑添加事务补偿机制


5. 状态机缺失严格校验 🟡 中风险

问题描述
状态流转依赖方法命名约定,缺少显式的状态机校验。

风险
- 可能出现非法状态跳转(如从草稿直接到已完成)
- 状态枚举值可能被随意设置

建议
```java
// 添加状态流转校验
public enum ItemReceiptStateTransition {
PREPARE_TO_APPROVING(PREPARE, APPROVING),
APPROVING_TO_APPROVED(APPROVING, APPROVED),
APPROVED_TO_FINISHED(APPROVED, FINISHED),
ANY_TO_CANCELED(ANY, CANCELED);

public static boolean isValidTransition(Integer from, Integer to) {
    // 校验逻辑
}

}
```


6. 盘点差异处理逻辑未实现 🟡 中风险

问题描述
文档中提到"盘盈+库存,盘亏-库存",但代码中未找到差异处理的具体实现。

风险
- 盘点结果可能无法正确影响库存
- 盘点流程不完整


7. 缺少安全库存预警 🟢 低风险

问题描述
系统缺少安全库存下限预警功能。

建议
- 在物料主数据中增加 safetyStock(安全库存)字段
- 出库时检查是否低于安全库存,触发预警


8. 缺少库存有效期管理 🟢 低风险

问题描述
批次管理中没有有效期字段,无法进行临期/过期预警。

建议
- 在批次表中增加 expireDate(有效期)字段
- 增加临期预警功能


二、业务流程漏洞

2.1 采购入库流程

环节 当前实现 潜在问题
提交 校验至少有一行 ✅ 正常
上架 校验明细数量=行数量 ✅ 正常
入库 创建事务流水 ✅ 正常
取消 只更新状态 ⚠️ 待上架状态取消后,已上架的明细如何处理?

2.2 销售出库流程

环节 当前实现 潜在问题
提交 检查OQC需求 ✅ 正常
拣货 校验每行有明细 ⚠️ 未校验库存充足
出库 创建事务流水 ✅ 正常
取消 只更新状态 ⚠️ 已拣货的库存如何处理?

建议:拣货时应冻结库存,防止被其他单据抢占。

2.3 调拨流程

环节 当前实现 潜在问题
提交 配送模式冻结源库存 ✅ 正常
上架 校验明细数量 ✅ 正常
执行 创建出库+入库事务 ✅ 正常
取消 解除冻结 ✅ 正常

评价:调拨流程设计较为完善。


三、已完成的优化项

1. 库存唯一键设计 ✅ 已优化

优化内容
- vendorId 现在参与库存唯一键
- 同一物料同一库位,不同供应商的库存分开管理
- 修改了 selectByCompositeKey 方法,增加 vendorId 参数

修改文件
- MesWmMaterialStockMapper.java
- MesWmMaterialStockServiceImpl.java

2. 状态流转校验 ✅ 已优化

优化内容
- 新增 WmStatusTransitionUtils 状态流转校验工具类
- 定义了各业务单据的合法状态流转规则
- 防止非法状态跳转

新增文件
- WmStatusTransitionUtils.java

使用示例
java WmStatusTransitionUtils.validateTransition("ITEM_RECEIPT", receipt.getStatus(), MesWmItemReceiptStatusEnum.APPROVING.getStatus(), "采购入库单");

3. 拣货库存校验 ✅ 已优化

优化内容
- 销售出库拣货时校验库存充足
- 调用 materialStockService.validateSelectedStock() 方法
- 校验物料、批次、库位、数量是否匹配

修改文件
- MesWmProductSalesServiceImpl.javastockProductSales 方法

4. 盘点差异处理 ✅ 已优化

优化内容
- 新增 processStockTakingDifference 方法
- 比较盘点结果与库存台账的差异
- 盘盈:库存增加,创建入库事务
- 盘亏:库存减少,创建出库事务

修改文件
- MesWmStockTakingTaskService.java
- MesWmStockTakingTaskServiceImpl.java
- MesWmStockTakingTaskResultService.java
- MesWmStockTakingTaskResultServiceImpl.java
- MesWmStockTakingTaskResultMapper.java
- MesBizTypeConstants.java (新增 WM_STOCK_TAKING 常量)
- ErrorCodeConstants.java (新增 WM_STOCK_TAKING_TASK_NOT_FINISHED 错误码)

3.1 缺少唯一约束

问题
- mes_wm_material_stock 表可能缺少唯一索引
- mes_wm_item_receipt.code 等单据编号应建立唯一索引

建议
```sql
ALTER TABLE mes_wm_material_stock
ADD UNIQUE INDEX uk_stock (item_id, warehouse_id, location_id, area_id, IFNULL(batch_id, 0));

ALTER TABLE mes_wm_item_receipt ADD UNIQUE INDEX uk_code (code);
ALTER TABLE mes_wm_product_sales ADD UNIQUE INDEX uk_code (code);
ALTER TABLE mes_wm_transfer ADD UNIQUE INDEX uk_code (code);
```

3.2 缺少级联删除控制

问题
- 删除仓库时,未检查是否存在库存记录
- 删除物料时,未检查是否存在库存记录

建议
- 在删除前增加引用检查
- 或使用软删除,保留历史数据


四、改进建议汇总

高优先级 🔴

序号 问题 解决方案
1 并发安全 增加乐观锁版本号或分布式锁
2 已完成单据取消 明确业务规则,禁止或创建冲销事务

中优先级 🟡

序号 问题 解决方案
3 唯一键不明确 明确 vendorId 是否参与唯一键
4 状态机校验缺失 增加状态流转校验
5 盘点差异处理 补充差异处理逻辑
6 拣货库存校验 拣货时校验并冻结库存

低优先级 🟢

序号 问题 解决方案
7 安全库存预警 增加安全库存字段和预警功能
8 有效期管理 批次增加有效期字段

五、架构优化建议

5.1 增加库存预占机制

销售出库单创建 → 预占库存(预留量)
        ↓
    执行出库 → 扣减预留量 + 扣减实物库存
        ↓
    取消 → 释放预留量

5.2 增加库存操作日志

  • 记录每次库存变更的操作人、操作时间、操作原因
  • 便于问题追溯和审计

5.3 增加库存快照功能

  • 每日定时生成库存快照
  • 支持历史库存查询和对比分析