本文目录导读:

在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):
- 查询采购单明细时加锁,防止多人同时对该采购单进行重复入库。
- 库存表加锁,防止并发超卖。
- 状态追踪:采购单应设计
status和received_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();
});
常见注意事项
- 单位转换:如果存在不同计量单位(如采购按“箱”,库存按“个”),入库时要乘上换算系数。
- 价格/成本更新:如果是加权平均法,入库时需要同时更新
inventory表的成本价。 - 入库单审核流程:建议入库单创建时 不直接更新库存,而是等到审核通过后才更新库存,如果采用该方案,上述的逻辑应放到“审核”操作中执行。
- 日志记录:建议记录库存变动日志表(
inventory_logs),记录发生时间、操作人、变动数量、关联单号。
如果需要处理非采购入库(如盘盈、直接入库),可以在 receipt_orders 表中增加一个 type 字段(如“purchase”或“direct”),并单独处理库存更新逻辑。