本文目录导读:

在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”),并单独处理库存更新逻辑。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播