ThinkPHP实现ERP进销存库存联动的核心是业务动作触发库存实时变更,通过stock_log流水表与stock_current快照表双表结构、事务封装操作、单据与库存解耦、行锁防超卖四大机制保障一致性。

ThinkPHP 实现 ERP 进销存库存联动,核心在于“业务动作触发库存实时变更”,不是简单增删改查,而是通过统一的库存事务模型,把采购入库、销售出库、退货、调拨等操作,全部映射为“库存流水(stock_log)”并驱动“当前库存(stock_current)”自动更新。关键不在框架功能多强,而在设计是否隔离业务与库存逻辑,保证一致性。
一、库存核心表结构设计
避免在商品表里直接存 quantity 字段——这会导致并发写冲突和状态不一致。必须拆分为流水+快照双表:
- stock_log:记录每一次库存变动(谁、何时、什么商品、多少、类型如in_purchase/out_sale/in_return),带 business_id(关联采购单/销售单ID)、operator_id、remark;主键自增,加索引(goods_id + created_at)
- stock_current:仅存最新可用库存(goods_id、warehouse_id、quantity、freeze_quantity【冻结量,用于待发货】),每条记录唯一约束(goods_id + warehouse_id)
二、用事务封装库存操作
所有涉及库存变更的动作,必须走同一个服务方法,且全程事务包裹。ThinkPHP 8.x 推荐用 Db::transaction(),不要依赖模型事件自动扣减:
- 先校验可用库存是否足够(销售/出库时查 stock_current.quantity – stock_current.freeze_quantity)
- 插入 stock_log 记录(type、delta、business_id 等字段填全)
- 原子更新 stock_current:用 SQL 的 ON DUPLICATE KEY UPDATE 或 INSERT … ON CONFLICT(TP8 支持 pgsql);MySQL 下推荐:
Db::table('stock_current')->update(['quantity' => Db::raw('quantity + '.$delta)])->where(['goods_id'=>$gid, 'warehouse_id'=>$wid]); - 任何一步失败,整个事务回滚,日志不写、库存不动
三、进销存单据与库存解耦
采购单、销售单、调拨单各自独立建模,只负责业务逻辑(审批流、客户/供应商关联、金额计算)。它们提交成功后,再调用统一库存服务:
立即学习“PHP免费学习笔记(深入)”;
- 例如销售单提交后,触发
StockService::recordOutbound($saleOrderId, $items) - 该方法遍历 $items,对每个商品执行一次库存事务(不是批量一条SQL,要支持部分失败回滚)
- 单据状态变更(如销售单→已出库)放在事务 commit 后,避免库存已动但单据卡住
- 提供「反向冲正」接口:比如销售单作废,调用
StockService::revertOutbound($saleOrderId),按原 log 找出记录,生成 type=in_sale_refund 的新 log 并反向更新库存
四、防止超卖与并发安全
高并发场景下,光靠数据库事务不够,需加行锁或状态锁:
- 库存校验和更新必须在同一 SQL 中完成,避免先 SELECT 再 UPDATE(产生竞态)。TP 中可写原生 SQL:
Db::execute("UPDATE stock_current SET quantity = quantity - ? WHERE goods_id = ? AND warehouse_id = ? AND quantity >= ?", [$num, $gid, $wid, $num]); - 返回影响行数,为 0 则说明库存不足,直接抛异常
- 对关键商品(如爆款),可引入 Redis 分布式锁(key=stock_lock:{$gid}:{$wid}),但仅作为兜底,不替代数据库行锁
- 后台任务(如定时盘点同步)必须设为低优先级,并跳过已锁定记录,避免阻塞业务链路
文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/shoujipingce/124157.html