# 仓储物流模块业务分析报告 ## 一、发现的问题与漏洞 ### 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 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 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.java` 的 `stockProductSales` 方法 ### 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 增加库存快照功能 - 每日定时生成库存快照 - 支持历史库存查询和对比分析