PHP项目入库单如何关联采购单更新库存数据

wen PHP项目 31

本文目录导读:

PHP项目入库单如何关联采购单更新库存数据

  1. 数据库表结构设计(核心)
  2. PHP业务逻辑核心代码(示例)
  3. 关键设计要点
  4. 如果框架是Laravel(示例)
  5. 常见注意事项

在PHP项目中实现入库单关联采购单并更新库存数据,通常涉及以下几个核心步骤和数据库设计思路,下面是一个通用的实现方案,你可以根据具体框架(如Laravel、ThinkPHP等)进行适配。

数据库表结构设计(核心)

你需要至少三张表来维护这个关系:

-- 1. 采购单主表
CREATE TABLE `purchase_orders` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(50) NOT NULL UNIQUE, -- 采购单号
  `supplier_id` INT NOT NULL,
  `status` TINYINT DEFAULT 0, -- 0:待入库, 1:部分入库, 2:全部入库, 3:已完成
  `total_quantity` DECIMAL(10,2) DEFAULT 0, -- 采购总数量
  `received_quantity` DECIMAL(10,2) DEFAULT 0, -- 已入库数量
  `created_at` DATETIME
);
-- 2. 采购单明细表
CREATE TABLE `purchase_order_items` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `purchase_order_id` INT NOT NULL, -- 关联采购单主表
  `product_id` INT NOT NULL,        -- 商品/物料ID
  `quantity` DECIMAL(10,2) NOT NULL, -- 采购数量
  `received_quantity` DECIMAL(10,2) DEFAULT 0, -- 该商品已入库数量
  `unit_price` DECIMAL(10,2),
  FOREIGN KEY (`purchase_order_id`) REFERENCES `purchase_orders`(`id`)
);
-- 3. 入库单主表
CREATE TABLE `receipt_orders` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `receipt_no` VARCHAR(50) NOT NULL UNIQUE, -- 入库单号
  `purchase_order_id` INT,                  -- 关联的采购单ID (可为NULL,允许直接入库)
  `status` TINYINT DEFAULT 1,               -- 1:待审核, 2:已审核, 3:已作废
  `created_at` DATETIME
);
-- 4. 入库单明细表
CREATE TABLE `receipt_order_items` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `receipt_order_id` INT NOT NULL,
  `purchase_order_item_id` INT DEFAULT NULL, -- 关联采购单明细ID
  `product_id` INT NOT NULL,
  `quantity` DECIMAL(10,2) NOT NULL,        -- 本次入库数量
  FOREIGN KEY (`receipt_order_id`) REFERENCES `receipt_orders`(`id`)
);
-- 5. 库存表
CREATE TABLE `inventory` (
  `product_id` INT PRIMARY KEY, -- 商品ID
  `quantity` DECIMAL(10,2) DEFAULT 0, -- 当前库存数量
  `last_updated` DATETIME
);

PHP业务逻辑核心代码(示例)

以下是一个基于 事务行级锁 的完整入库处理逻辑(以原生PHP为例,框架内可类似封装到Service层):

<?php
// 假设使用PDO连接数据库
public function createReceiptOrder($purchaseOrderId, $items, &$errorMsg) {
    $db->beginTransaction();
    try {
        // ========== 第一步:检查采购单状态 ==========
        $purchaseOrder = $db->query("SELECT * FROM purchase_orders WHERE id = ? FOR UPDATE", [$purchaseOrderId]);
        if (!$purchaseOrder || $purchaseOrder['status'] == 3) {
            throw new \Exception('采购单不存在或已完成');
        }
        // ========== 第二步:验证入库数量是否超过采购数量 ==========
        $receiptItems = [];
        foreach ($items as $item) {
            $purchaseItem = $db->query("SELECT * FROM purchase_order_items WHERE id = ? AND purchase_order_id = ? FOR UPDATE", 
                [$item['purchase_item_id'], $purchaseOrderId]);
            if (!$purchaseItem) {
                throw new \Exception("采购明细ID :{$item['purchase_item_id']} 不存在");
            }
            // 计算该商品已入库总数
            $remaining = $purchaseItem['quantity'] - $purchaseItem['received_quantity'];
            if ($item['quantity'] > $remaining) {
                throw new \Exception("商品 {$purchaseItem['product_id']} 入库数量({$item['quantity']})超出剩余可入库量({$remaining})");
            }
            $receiptItems[] = [
                'product_id' => $purchaseItem['product_id'],
                'purchase_item_id' => $purchaseItem['id'],
                'quantity' => $item['quantity'],
                'original_quantity' => $purchaseItem['quantity'],
                'current_received' => $purchaseItem['received_quantity']
            ];
        }
        // ========== 第三步:创建入库单(主表) ==========
        $receiptNo = 'RK' . date('YmdHis') . rand(1000, 9999);
        $stmt = $db->prepare("INSERT INTO receipt_orders (receipt_no, purchase_order_id, status) VALUES (?, ?, 1)");
        $stmt->execute([$receiptNo, $purchaseOrderId]);
        $receiptOrderId = $db->lastInsertId();
        // ========== 第四步:写入入库明细,并更新采购单明细的已入库数量 ==========
        foreach ($receiptItems as $item) {
            // 4.1 写入明细
            $stmt = $db->prepare("INSERT INTO receipt_order_items (receipt_order_id, purchase_order_item_id, product_id, quantity) 
                                   VALUES (?, ?, ?, ?)");
            $stmt->execute([$receiptOrderId, $item['purchase_item_id'], $item['product_id'], $item['quantity']]);
            // 4.2 更新采购单明细表的已入库数量
            $newReceived = $item['current_received'] + $item['quantity'];
            $db->update("UPDATE purchase_order_items SET received_quantity = ? WHERE id = ?", 
                       [$newReceived, $item['purchase_item_id']]);
        }
        // ========== 第五步:更新采购主表的已入库总数和状态 ==========
        // 重新计算采购单所有明细的已入库总和
        $totalReceived = $db->query("SELECT SUM(received_quantity) as total 
                                      FROM purchase_order_items 
                                      WHERE purchase_order_id = ?", [$purchaseOrderId])['total'];
        $newStatus = ($totalReceived >= $purchaseOrder['total_quantity']) ? 2 : 1; // 2=全部入库
        $db->update("UPDATE purchase_orders SET received_quantity = ?, status = ? WHERE id = ?", 
                   [$totalReceived, $newStatus, $purchaseOrderId]);
        // ========== 第六步:更新库存表(关键步骤) ==========
        foreach ($receiptItems as $item) {
            // 使用行锁防止并发,检查库存是否存在
            $inventory = $db->query("SELECT * FROM inventory WHERE product_id = ? FOR UPDATE", [$item['product_id']]);
            if ($inventory) {
                // 更新现有库存
                $db->update("UPDATE inventory SET quantity = quantity + ?, last_updated = NOW() WHERE product_id = ?", 
                           [$item['quantity'], $item['product_id']]);
            } else {
                // 首次入库,创建库存记录
                $db->insert("INSERT INTO inventory (product_id, quantity, last_updated) VALUES (?, ?, NOW())", 
                           [$item['product_id'], $item['quantity']]);
            }
        }
        $db->commit();
        return $receiptOrderId;
    } catch (\Exception $e) {
        $db->rollback();
        $errorMsg = $e->getMessage();
        return false;
    }
}

关键设计要点

  • 事务保护:入库、更新采购单、更新库存必须在同一个数据库事务中,防止数据不一致。
  • 行级锁(SELECT ... FOR UPDATE)
    • 查询采购单明细时加锁,防止多人同时对该采购单进行重复入库。
    • 库存表加锁,防止并发超卖。
  • 状态追踪:采购单应设计 statusreceived_quantity,便于判断是“部分入库”还是“全部入库”,入库单也应能追溯到对应的采购单明细。
  • 冗余设计:采购单主表 received_quantity 是冗余字段(可以实时计算),但可以提高查询效率,避免每次都要从明细表汇总。

如果框架是Laravel(示例)

在Laravel中,可以使用Eloquent模型 + 事务 + 悲观锁,代码会更简洁:

use Illuminate\Support\Facades\DB;
use App\Models\PurchaseOrder;
use App\Models\Inventory;
DB::transaction(function () use ($purchaseOrderId, $items) {
    $order = PurchaseOrder::lockForUpdate()->findOrFail($purchaseOrderId);
    foreach ($items as $item) {
        $purchaseItem = $order->items()->lockForUpdate()->findOrFail($item['purchase_item_id']);
        $remaining = $purchaseItem->quantity - $purchaseItem->received_quantity;
        if ($item['quantity'] > $remaining) {
            throw new \Exception("超出可入库数量");
        }
        // 创建入库明细...
        $receiptItem = $order->receiptItems()->create([...]);
        // 更新采购明细已入库数
        $purchaseItem->increment('received_quantity', $item['quantity']);
        // 更新库存
        Inventory::lockForUpdate()
            ->firstOrCreate(['product_id' => $purchaseItem->product_id])
            ->increment('quantity', $item['quantity']);
    }
    // 更新采购单状态
    $order->syncStatus();
});

常见注意事项

  1. 单位转换:如果存在不同计量单位(如采购按“箱”,库存按“个”),入库时要乘上换算系数。
  2. 价格/成本更新:如果是加权平均法,入库时需要同时更新 inventory 表的成本价。
  3. 入库单审核流程:建议入库单创建时 不直接更新库存,而是等到审核通过后才更新库存,如果采用该方案,上述的逻辑应放到“审核”操作中执行。
  4. 日志记录:建议记录库存变动日志表(inventory_logs),记录发生时间、操作人、变动数量、关联单号。

如果需要处理非采购入库(如盘盈、直接入库),可以在 receipt_orders 表中增加一个 type 字段(如“purchase”或“direct”),并单独处理库存更新逻辑。

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