本文目录导读:

在PHP项目中实现采购订单功能,通常需要构建一个完整的增删改查(CRUD) 流程,并涉及数据建模、业务逻辑和前端交互。
下面是一个从零开始的、结构化的实现方案,适用于中小型项目(如ERP、进销存系统)。
第一阶段:数据库设计与建模
采购订单的核心是一对多关系:一笔订单(主表)对应多个商品明细(子表)。
主表 purchase_orders
CREATE TABLE `purchase_orders` ( `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号(自动生成)', `supplier_id` INT(11) UNSIGNED NOT NULL COMMENT '供应商ID', `warehouse_id` INT(11) UNSIGNED DEFAULT NULL COMMENT '目标仓库ID', `order_date` DATE NOT NULL COMMENT '下单日期', `expected_delivery_date` DATE DEFAULT NULL COMMENT '预计到货日期', `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '状态: 0=待审核, 1=已审核, 2=已收货, 3=已完成, 9=已取消', `total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', `created_by` INT(11) UNSIGNED DEFAULT NULL COMMENT '创建人ID', `remark` TEXT COMMENT '备注', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
明细表 purchase_order_items
CREATE TABLE `purchase_order_items` ( `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, `order_id` INT(11) UNSIGNED NOT NULL COMMENT '订单主表ID', `product_id` INT(11) UNSIGNED NOT NULL COMMENT '商品ID', `product_name` VARCHAR(100) NOT NULL COMMENT '冗余商品名(防止商品修改)', `product_sku` VARCHAR(50) DEFAULT NULL COMMENT '冗余SKU', `quantity` INT(11) NOT NULL DEFAULT 1 COMMENT '采购数量', `unit_price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '采购单价', `subtotal` DECIMAL(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED COMMENT '小计(可计算字段)', `received_quantity` INT(11) NOT NULL DEFAULT 0 COMMENT '已收货数量', `remark` TEXT COMMENT '明细备注', FOREIGN KEY (`order_id`) REFERENCES `purchase_orders`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二阶段:后端PHP业务逻辑(以MVC为例)
创建订单(核心业务)
流程: 验证输入 → 生成订单号 → 计算总金额 → 事务写入主表和明细表。
// Controller: PurchaseOrderController.php
public function store(Request $request)
{
// 1. 验证数据
$validated = $request->validate([
'supplier_id' => 'required|integer|exists:suppliers,id',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|integer|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'items.*.unit_price' => 'required|numeric|min:0',
]);
// 2. 开启数据库事务
DB::beginTransaction();
try {
// 3. 生成订单号:PO + 年月日 + 5位流水号
$orderNo = 'PO' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
// 4. 计算总金额
$totalAmount = 0;
foreach ($validated['items'] as $item) {
$totalAmount += $item['quantity'] * $item['unit_price'];
}
// 5. 插入主表
$order = PurchaseOrder::create([
'order_no' => $orderNo,
'supplier_id' => $validated['supplier_id'],
'order_date' => date('Y-m-d'),
'status' => 0, // 待审核
'total_amount'=> $totalAmount,
'created_by' => auth()->id(),
]);
// 6. 插入明细表
foreach ($validated['items'] as $item) {
$product = Product::find($item['product_id']);
$order->items()->create([
'product_id' => $item['product_id'],
'product_name' => $product->name,
'product_sku' => $product->sku,
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
]);
}
DB::commit();
return response()->json(['message' => '订单创建成功', 'data' => $order]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => '创建失败: ' . $e->getMessage()], 500);
}
}
审核与收货(状态流转)
采购订单有生命周期,典型的状态机如下:
// 审核操作
public function approve($id)
{
$order = PurchaseOrder::findOrFail($id);
if ($order->status != 0) {
return response()->json(['error' => '仅待审核订单可审核'], 400);
}
$order->status = 1; // 已审核
$order->save();
// 可选择:通知采购员/供应商
}
// 收货入库(最复杂,会涉及库存变化)
public function receive($id, Request $request)
{
$order = PurchaseOrder::findOrFail($id);
if ($order->status != 1) {
return response()->json(['error' => '仅已审核订单可收货'], 400);
}
DB::transaction(function () use ($order, $request) {
foreach ($request->input('items') as $item) {
// 1. 更新订单明细的已收货数量
$orderItem = $order->items()->where('product_id', $item['product_id'])->first();
$newReceived = $orderItem->received_quantity + $item['receive_quantity'];
if ($newReceived > $orderItem->quantity) {
throw new \Exception("收货数量超过采购数量");
}
$orderItem->update(['received_quantity' => $newReceived]);
// 2. 增加库存(假设有 inventory_movements 表或直接更新库存表)
Inventory::increase($item['product_id'], $order->warehouse_id, $item['receive_quantity']);
}
// 3. 判断是否全部收货,若是则更新订单状态
$allReceived = $order->items->every(function ($item) {
return $item->received_quantity >= $item->quantity;
});
if ($allReceived) {
$order->status = 2; // 已收货
$order->save();
}
});
}
第三阶段:前端交互
推荐使用前后端分离(Vue/React + API)或 服务端渲染(Blade/Twig)。
典型前端流程:
- 选择供应商 → 下拉框异步加载。
- 添加商品行:
- 输入商品名称或扫码,通过AJAX搜索返回商品价格、单位。
- 每行可修改数量、单价,实时计算小计和合计。
- 提交前校验:数量>0,单价>0,至少一行。
- 提交订单:发送JSON到后端
/api/purchase-orders。
示例(Vue3 + Axios):
// 提交采购订单
async function submitOrder() {
const formData = {
supplier_id: selectedSupplier.value.id,
items: orderLines.value.map(line => ({
product_id: line.product.id,
quantity: line.quantity,
unit_price: line.price
}))
};
const res = await axios.post('/api/purchase-orders', formData);
if (res.data.message) {
alert('创建成功');
// 清空表单或跳转
}
}
第四阶段:关键注意事项
数据一致性(必须使用事务)
采购订单涉及主表+明细表+库存操作,必须用 DB::transaction() 或 beginTransaction/commit/rollback 保证原子性。
防止并发问题
高并发场景下,库存扣减和状态更新可能冲突,建议:
- 使用数据库行锁 (
SELECT ... FOR UPDATE)。 - 或者使用Redis分布式锁。
金额精度
使用 DECIMAL(10,2) 或 DECIMAL(12,2),绝对不要用float/double。
历史追溯
采购单一旦创建,商品名称/单价应冗余存储(如明细表中的product_name),防止未来商品信息变更导致历史单据混乱。
权限控制
- 普通员工:可创建、查看自己的订单。
- 主管:可审核。
- 仓库人员:可收货。
与其他模块联动
- 供应商模块:选择供应商时报出历史采购价。
- 库存模块:收货后自动增加库存。
- 财务模块:生成应付账款记录。
最小可行实现步骤
- 设计数据库表(主表 + 明细表)。
- 用框架(Laravel/ThinkPHP/Symfony)搭建模型和迁移。
- 实现核心API:
POST /api/purchase-orders(创建)GET /api/purchase-orders/{id}(详情)PUT /api/purchase-orders/{id}/approve(审核)PUT /api/purchase-orders/{id}/receive(收货)
- 前端实现表单、列表页、状态管理。
- 加入日志、权限、异常处理。
这个结构适用于90%的中小型采购管理需求,如果需要更复杂的场景(如分批收货、退货、采购合同关联),可以在明细表增加更多字段(如已退货数量、合同ID)来扩展。