本文目录导读:

在PHP项目中实现跨仓库调拨单的商品库存数据转移,核心难点在于保证库存数据的一致性和事务性(防止超卖、数据错乱),以下是标准的实现方案和关键代码逻辑:
数据库设计核心表
库存表 stock (仓库-商品库存)
CREATE TABLE `stock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `warehouse_id` int(11) NOT NULL COMMENT '仓库ID', `product_id` int(11) NOT NULL COMMENT '商品ID', `quantity` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT '可用库存', `lock_quantity` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT '锁定库存(调拨中)', `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), UNIQUE KEY `uk_warehouse_product` (`warehouse_id`,`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
调拨单表 transfer_order
CREATE TABLE `transfer_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '调拨单号', `from_warehouse_id` int(11) NOT NULL COMMENT '调出仓库', `to_warehouse_id` int(11) NOT NULL COMMENT '调入仓库', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0=待审核 1=审核通过 2=调拨中 3=已完成 4=已取消', `total_amount` decimal(15,2) DEFAULT 0.00, `create_time` datetime DEFAULT NULL, `complete_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
调拨单明细表 transfer_order_item
CREATE TABLE `transfer_order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `quantity` decimal(15,2) NOT NULL COMMENT '调拨数量', `from_warehouse_stock_before` decimal(15,2) DEFAULT NULL COMMENT '调出前库存', `to_warehouse_stock_before` decimal(15,2) DEFAULT NULL COMMENT '调入前库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心业务流程(含PHP代码)
创建调拨单(只锁定库存,不扣减)
// 使用事务 + 行级锁(MySQL InnoDB)
$db->beginTransaction();
try {
foreach ($items as $item) {
// 锁定调出仓库的库存行(悲观锁)
$fromStock = $db->query("
SELECT * FROM stock
WHERE warehouse_id = :from_warehouse_id
AND product_id = :product_id
FOR UPDATE
", ['from_warehouse_id' => $fromId, 'product_id' => $item['product_id']]);
// 检查可用库存是否充足
if ($fromStock['quantity'] - $fromStock['lock_quantity'] < $item['quantity']) {
throw new Exception("商品 {$item['product_id']} 在仓库{$fromId}库存不足");
}
// 增加锁定库存(调拨中占用)
$db->execute("
UPDATE stock
SET lock_quantity = lock_quantity + :qty,
version = version + 1
WHERE id = :id
AND version = :old_version -- 乐观锁二次校验
", [
'qty' => $item['quantity'],
'id' => $fromStock['id'],
'old_version' => $fromStock['version']
]);
}
// 插入调拨单主表和明细表(状态为“待审核”)
$orderId = insertOrder(...);
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
审核/执行调拨(真正扣减+增加库存)
$db->beginTransaction();
try {
// 再次锁定调出、调入仓库的库存行
$fromStock = $db->query("SELECT * FROM stock WHERE ... FOR UPDATE");
$toStock = $db->query("SELECT * FROM stock WHERE warehouse_id = :to_warehouse_id AND product_id = :product_id FOR UPDATE");
// 1. 扣减调出仓库:可用库存 - 数量,锁定库存 - 数量
$db->execute("
UPDATE stock
SET quantity = quantity - :qty,
lock_quantity = lock_quantity - :qty,
version = version + 1
WHERE id = :id
AND quantity >= :qty -- 防止并发超卖
AND lock_quantity >= :qty
", ['qty' => $item['quantity'], 'id' => $fromStock['id']]);
// 2. 增加调入仓库:可用库存 + 数量
if ($toStock) {
$db->execute("
UPDATE stock
SET quantity = quantity + :qty, version = version + 1
WHERE id = :id
", ['qty' => $item['quantity'], 'id' => $toStock['id']]);
} else {
// 如果调入仓库无此商品记录,则插入新行
$db->execute("
INSERT INTO stock (warehouse_id, product_id, quantity, lock_quantity)
VALUES (:wid, :pid, :qty, 0)
", ['wid' => $toWarehouseId, 'pid' => $item['product_id'], 'qty' => $item['quantity']]);
}
// 3. 更新调拨单状态为“已完成”
$db->execute("UPDATE transfer_order SET status = 3, complete_time = NOW() WHERE id = :id", ['id' => $orderId]);
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
取消调拨单(释放锁定库存)
$db->beginTransaction();
try {
// 锁定调出仓库库存行
$fromStock = $db->query("SELECT * FROM stock WHERE ... FOR UPDATE");
// 释放锁定库存
$db->execute("
UPDATE stock
SET lock_quantity = lock_quantity - :qty,
version = version + 1
WHERE id = :id
AND lock_quantity >= :qty
", ['qty' => $item['quantity'], 'id' => $fromStock['id']]);
// 更新调拨单状态为“已取消”
$db->execute("UPDATE transfer_order SET status = 4 WHERE id = :id", ['id' => $orderId]);
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
关键设计原则
| 原则 | 说明 | 代码体现 |
|---|---|---|
| 事务性 | 扣减A仓库 + 增加B仓库必须在同一事务中 | beginTransaction() + commit() |
| 行级锁 | 防止并发操作导致超卖 | SELECT ... FOR UPDATE |
| 乐观锁版本号 | 二次校验,避免不可重复读 | WHERE version = :old_version |
| 库存分离 | 可用库存与锁定库存分开管理 | quantity / lock_quantity |
| 幂等性 | 确保同一调拨单不会重复执行 | 状态机控制(status字段) |
进阶优化建议
-
使用消息队列
若调拨单量大,可将扣库存操作放入RabbitMQ/Redis队列,异步处理,提高响应速度。// 创建调拨单仅做记录,异步执行库存转移 Queue::push('transferJob', ['order_id' => $orderId]); -
库存快照与对账
每天跑定时任务,统计实际库存与调拨单差异,生成对账报告。 -
分库分表场景
若仓库分布在不同的数据库,需使用分布式事务(如TCC模式或Seata框架),不能依赖本地事务。 -
回滚机制
调拨执行失败时,自动回滚库存(A仓库扣了但B仓库插入失败,需补偿A仓库)。
完整流程图(简化版)
创建调拨单 → 锁定A仓库库存(lock_quantity+) → 待审核
↓
审核通过 → 事务开始
→ 扣减A仓库(quantity-,lock_quantity-)
→ 增加B仓库(quantity+)
→ 更新调拨单状态为完成
→ 事务提交
↓
取消调拨 → 释放A仓库锁定库存(lock_quantity-)
通过以上方案,即可在PHP项目中实现安全、可靠的跨仓库库存调拨,核心是数据库事务+行级锁+状态机控制。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播