问题描述:
库存更新操作 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();
}
```
问题描述:
文档中定义唯一键为 (itemId, warehouseId, locationId, areaId, batchId),但实际代码中 vendorId 也参与了组合查询。
风险:
- 同一物料同一库位,不同供应商的入库会创建两条库存记录
- 可能导致库存记录膨胀,查询效率下降
代码证据(MesWmMaterialStockServiceImpl.java:141-146):java MesWmMaterialStockDO existStock = materialStockMapper.selectByCompositeKey( itemId, warehouseId, locationId, areaId, batchId); // 注意:vendorId 未参与唯一键查询
建议:
- 明确业务需求:是否需要按供应商区分库存?
- 如果需要,建议在唯一键中增加 vendorId
- 如果不需要,入库时不应创建新的库存记录
设计决策:
- 已完成状态(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); } // ... }
评价:这是正确的业务设计,避免了复杂的库存冲销逻辑。
问题描述:
批量创建事务时,如果中途失败,已执行的事务不会回滚。
代码证据(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 内部捕获异常未抛出,会导致部分成功
建议:
- 确保所有异常都向上抛出
- 考虑添加事务补偿机制
问题描述:
状态流转依赖方法命名约定,缺少显式的状态机校验。
风险:
- 可能出现非法状态跳转(如从草稿直接到已完成)
- 状态枚举值可能被随意设置
建议:
```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) {
// 校验逻辑
}
}
```
问题描述:
文档中提到"盘盈+库存,盘亏-库存",但代码中未找到差异处理的具体实现。
风险:
- 盘点结果可能无法正确影响库存
- 盘点流程不完整
问题描述:
系统缺少安全库存下限预警功能。
建议:
- 在物料主数据中增加 safetyStock(安全库存)字段
- 出库时检查是否低于安全库存,触发预警
问题描述:
批次管理中没有有效期字段,无法进行临期/过期预警。
建议:
- 在批次表中增加 expireDate(有效期)字段
- 增加临期预警功能
| 环节 | 当前实现 | 潜在问题 |
|---|---|---|
| 提交 | 校验至少有一行 | ✅ 正常 |
| 上架 | 校验明细数量=行数量 | ✅ 正常 |
| 入库 | 创建事务流水 | ✅ 正常 |
| 取消 | 只更新状态 | ⚠️ 待上架状态取消后,已上架的明细如何处理? |
| 环节 | 当前实现 | 潜在问题 |
|---|---|---|
| 提交 | 检查OQC需求 | ✅ 正常 |
| 拣货 | 校验每行有明细 | ⚠️ 未校验库存充足 |
| 出库 | 创建事务流水 | ✅ 正常 |
| 取消 | 只更新状态 | ⚠️ 已拣货的库存如何处理? |
建议:拣货时应冻结库存,防止被其他单据抢占。
| 环节 | 当前实现 | 潜在问题 |
|---|---|---|
| 提交 | 配送模式冻结源库存 | ✅ 正常 |
| 上架 | 校验明细数量 | ✅ 正常 |
| 执行 | 创建出库+入库事务 | ✅ 正常 |
| 取消 | 解除冻结 | ✅ 正常 |
评价:调拨流程设计较为完善。
优化内容:
- vendorId 现在参与库存唯一键
- 同一物料同一库位,不同供应商的库存分开管理
- 修改了 selectByCompositeKey 方法,增加 vendorId 参数
修改文件:
- MesWmMaterialStockMapper.java
- MesWmMaterialStockServiceImpl.java
优化内容:
- 新增 WmStatusTransitionUtils 状态流转校验工具类
- 定义了各业务单据的合法状态流转规则
- 防止非法状态跳转
新增文件:
- WmStatusTransitionUtils.java
使用示例:java WmStatusTransitionUtils.validateTransition("ITEM_RECEIPT", receipt.getStatus(), MesWmItemReceiptStatusEnum.APPROVING.getStatus(), "采购入库单");
优化内容:
- 销售出库拣货时校验库存充足
- 调用 materialStockService.validateSelectedStock() 方法
- 校验物料、批次、库位、数量是否匹配
修改文件:
- MesWmProductSalesServiceImpl.java 的 stockProductSales 方法
优化内容:
- 新增 processStockTakingDifference 方法
- 比较盘点结果与库存台账的差异
- 盘盈:库存增加,创建入库事务
- 盘亏:库存减少,创建出库事务
修改文件:
- MesWmStockTakingTaskService.java
- MesWmStockTakingTaskServiceImpl.java
- MesWmStockTakingTaskResultService.java
- MesWmStockTakingTaskResultServiceImpl.java
- MesWmStockTakingTaskResultMapper.java
- MesBizTypeConstants.java (新增 WM_STOCK_TAKING 常量)
- ErrorCodeConstants.java (新增 WM_STOCK_TAKING_TASK_NOT_FINISHED 错误码)
问题:
- 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);
```
问题:
- 删除仓库时,未检查是否存在库存记录
- 删除物料时,未检查是否存在库存记录
建议:
- 在删除前增加引用检查
- 或使用软删除,保留历史数据
| 序号 | 问题 | 解决方案 |
|---|---|---|
| 1 | 并发安全 | 增加乐观锁版本号或分布式锁 |
| 2 | 已完成单据取消 | 明确业务规则,禁止或创建冲销事务 |
| 序号 | 问题 | 解决方案 |
|---|---|---|
| 3 | 唯一键不明确 | 明确 vendorId 是否参与唯一键 |
| 4 | 状态机校验缺失 | 增加状态流转校验 |
| 5 | 盘点差异处理 | 补充差异处理逻辑 |
| 6 | 拣货库存校验 | 拣货时校验并冻结库存 |
| 序号 | 问题 | 解决方案 |
|---|---|---|
| 7 | 安全库存预警 | 增加安全库存字段和预警功能 |
| 8 | 有效期管理 | 批次增加有效期字段 |
销售出库单创建 → 预占库存(预留量)
↓
执行出库 → 扣减预留量 + 扣减实物库存
↓
取消 → 释放预留量