PHP项目调拨单如何跨仓库转移商品库存数据

wen PHP项目 29

本文目录导读:

PHP项目调拨单如何跨仓库转移商品库存数据

  1. 数据库设计核心表
  2. 核心业务流程(含PHP代码)
  3. 关键设计原则
  4. 进阶优化建议
  5. 完整流程图(简化版)

在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字段)

进阶优化建议

  1. 使用消息队列
    若调拨单量大,可将扣库存操作放入RabbitMQ/Redis队列,异步处理,提高响应速度。

    // 创建调拨单仅做记录,异步执行库存转移
    Queue::push('transferJob', ['order_id' => $orderId]);
  2. 库存快照与对账
    每天跑定时任务,统计实际库存与调拨单差异,生成对账报告。

  3. 分库分表场景
    若仓库分布在不同的数据库,需使用分布式事务(如TCC模式或Seata框架),不能依赖本地事务。

  4. 回滚机制
    调拨执行失败时,自动回滚库存(A仓库扣了但B仓库插入失败,需补偿A仓库)。


完整流程图(简化版)

创建调拨单 → 锁定A仓库库存(lock_quantity+) → 待审核
    ↓
审核通过 → 事务开始 
         → 扣减A仓库(quantity-,lock_quantity-)
         → 增加B仓库(quantity+)
         → 更新调拨单状态为完成
         → 事务提交
    ↓
取消调拨 → 释放A仓库锁定库存(lock_quantity-)

通过以上方案,即可在PHP项目中实现安全、可靠的跨仓库库存调拨,核心是数据库事务+行级锁+状态机控制

抱歉,评论功能暂时关闭!