本文目录导读:

我来详细介绍如何用PHP实现到货验收功能,这是一个完整的解决方案,包含数据库设计、前端界面和业务逻辑。
数据库设计
首先创建必要的数据表:
-- 采购订单表
CREATE TABLE purchase_orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) UNIQUE NOT NULL,
supplier_name VARCHAR(200) NOT NULL,
order_date DATETIME NOT NULL,
expected_delivery_date DATE,
status ENUM('pending','partial','completed') DEFAULT 'pending',
total_amount DECIMAL(10,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 采购订单详情表
CREATE TABLE purchase_order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_name VARCHAR(200) NOT NULL,
specification VARCHAR(500),
quantity_ordered INT NOT NULL,
quantity_received INT DEFAULT 0,
unit VARCHAR(20),
unit_price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES purchase_orders(id)
);
-- 验收单表
CREATE TABLE receiving_notes (
id INT PRIMARY KEY AUTO_INCREMENT,
receiving_no VARCHAR(50) UNIQUE NOT NULL,
order_id INT NOT NULL,
received_date DATETIME NOT NULL,
receiver VARCHAR(100),
remarks TEXT,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES purchase_orders(id)
);
-- 验收详情表
CREATE TABLE receiving_items (
id INT PRIMARY KEY AUTO_INCREMENT,
receiving_id INT NOT NULL,
order_item_id INT NOT NULL,
quantity_received INT NOT NULL,
qualified_quantity INT DEFAULT 0,
defective_quantity INT DEFAULT 0,
remark TEXT,
FOREIGN KEY (receiving_id) REFERENCES receiving_notes(id),
FOREIGN KEY (order_item_id) REFERENCES purchase_order_items(id)
);
-- 质量问题表
CREATE TABLE quality_issues (
id INT PRIMARY KEY AUTO_INCREMENT,
receiving_item_id INT NOT NULL,
issue_type VARCHAR(50),
description TEXT,
severity ENUM('minor','major','critical'),
handling_method VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (receiving_item_id) REFERENCES receiving_items(id)
);
PHP后端核心代码
验收主类 ReceivingManager.php
<?php
class ReceivingManager {
private $db;
public function __construct($db) {
$this->db = $db;
}
/**
* 生成验收单号
*/
public function generateReceivingNo() {
$prefix = 'RECV';
$date = date('Ymd');
// 查询当天最大序号
$stmt = $this->db->prepare(
"SELECT COUNT(*) FROM receiving_notes WHERE DATE(created_at) = CURDATE()"
);
$stmt->execute();
$count = $stmt->fetchColumn() + 1;
return sprintf('%s-%s-%04d', $prefix, $date, $count);
}
/**
* 创建验收单
*/
public function createReceivingNote($orderId, $receiver, $items, $remarks = '') {
try {
$this->db->beginTransaction();
// 创建验收单
$receivingNo = $this->generateReceivingNo();
$stmt = $this->db->prepare(
"INSERT INTO receiving_notes (receiving_no, order_id, received_date, receiver, remarks)
VALUES (?, ?, NOW(), ?, ?)"
);
$stmt->execute([$receivingNo, $orderId, $receiver, $remarks]);
$receivingId = $this->db->lastInsertId();
// 处理验收明细
foreach ($items as $item) {
$stmt = $this->db->prepare(
"INSERT INTO receiving_items (receiving_id, order_item_id, quantity_received,
qualified_quantity, defective_quantity, remark)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$receivingId,
$item['order_item_id'],
$item['quantity_received'],
$item['qualified_quantity'],
$item['defective_quantity'],
$item['remark'] ?? ''
]);
// 更新采购订单明细的已收数量
$stmt = $this->db->prepare(
"UPDATE purchase_order_items SET quantity_received = quantity_received + ? WHERE id = ?"
);
$stmt->execute([$item['quantity_received'], $item['order_item_id']]);
// 如果存在质量问题,记录质量异常
if (!empty($item['quality_issues'])) {
$this->recordQualityIssues(
$this->db->lastInsertId(),
$item['quality_issues']
);
}
}
// 更新采购订单状态
$this->updateOrderStatus($orderId);
$this->db->commit();
return ['success' => true, 'receiving_id' => $receivingId, 'receiving_no' => $receivingNo];
} catch (Exception $e) {
$this->db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* 记录质量问题
*/
private function recordQualityIssues($receivingItemId, $issues) {
foreach ($issues as $issue) {
$stmt = $this->db->prepare(
"INSERT INTO quality_issues (receiving_item_id, issue_type, description, severity, handling_method)
VALUES (?, ?, ?, ?, ?)"
);
$stmt->execute([
$receivingItemId,
$issue['type'],
$issue['description'],
$issue['severity'],
$issue['handling_method']
]);
}
}
/**
* 更新订单状态
*/
private function updateOrderStatus($orderId) {
$stmt = $this->db->prepare(
"SELECT
SUM(quantity_ordered) as total_ordered,
SUM(quantity_received) as total_received
FROM purchase_order_items
WHERE order_id = ?"
);
$stmt->execute([$orderId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result['total_ordered'] == $result['total_received']) {
$status = 'completed';
} elseif ($result['total_received'] > 0) {
$status = 'partial';
} else {
$status = 'pending';
}
$stmt = $this->db->prepare("UPDATE purchase_orders SET status = ? WHERE id = ?");
$stmt->execute([$status, $orderId]);
}
/**
* 获取待验收订单
*/
public function getPendingOrders() {
$stmt = $this->db->query(
"SELECT po.*,
(SELECT COUNT(*) FROM purchase_order_items WHERE order_id = po.id AND quantity_received < quantity_ordered) as pending_items
FROM purchase_orders po
WHERE po.status IN ('pending', 'partial')
ORDER BY po.expected_delivery_date ASC"
);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 获取订单详细商品
*/
public function getOrderItems($orderId) {
$stmt = $this->db->prepare(
"SELECT * FROM purchase_order_items
WHERE order_id = ? AND quantity_received < quantity_ordered"
);
$stmt->execute([$orderId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 获取验收历史
*/
public function getReceivingHistory($orderId = null) {
$sql = "SELECT rn.*, po.order_no, po.supplier_name
FROM receiving_notes rn
JOIN purchase_orders po ON rn.order_id = po.id";
if ($orderId) {
$sql .= " WHERE rn.order_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$orderId]);
} else {
$stmt = $this->db->query($sql);
}
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
验收控制器 ReceivingController.php
<?php
class ReceivingController {
private $receivingManager;
private $twig;
public function __construct($receivingManager, $twig) {
$this->receivingManager = $receivingManager;
$this->twig = $twig;
}
/**
* 显示待验收订单列表
*/
public function index() {
$pendingOrders = $this->receivingManager->getPendingOrders();
echo $this->twig->render('receiving/index.html.twig', [
'orders' => $pendingOrders
]);
}
/**
* 显示验收表单
*/
public function create($orderId) {
$orderItems = $this->receivingManager->getOrderItems($orderId);
echo $this->twig->render('receiving/create.html.twig', [
'order_id' => $orderId,
'items' => $orderItems
]);
}
/**
* 保存验收结果
*/
public function store($request) {
$result = $this->receivingManager->createReceivingNote(
$request['order_id'],
$request['receiver'],
$request['items'],
$request['remarks'] ?? ''
);
if ($result['success']) {
header('Location: /receiving/' . $result['receiving_id'] . '/success');
} else {
// 显示错误信息
echo $this->twig->render('receiving/create.html.twig', [
'error' => $result['error']
]);
}
}
/**
* 显示验收单详情
*/
public function show($receivingId) {
$stmt = $this->db->prepare(
"SELECT ri.*, poi.product_name, poi.specification
FROM receiving_items ri
JOIN purchase_order_items poi ON ri.order_item_id = poi.id
WHERE ri.receiving_id = ?"
);
$stmt->execute([$receivingId]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo $this->twig->render('receiving/show.html.twig', [
'items' => $items
]);
}
}
前端界面示例
验收表单 create.html.twig
{% extends 'layout.html.twig' %}
{% block content %}
<div class="container mt-4">
<h2>到货验收</h2>
<form id="receivingForm" method="POST" action="/receiving/store">
<input type="hidden" name="order_id" value="{{ order_id }}">
<div class="form-group">
<label>验收人</label>
<input type="text" name="receiver" class="form-control" required>
</div>
<div class="card mt-3">
<div class="card-header">
<h5>验收商品明细</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>商品名称</th>
<th>规格</th>
<th>应到数量</th>
<th>实收数量</th>
<th>合格数量</th>
<th>不合格数量</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.product_name }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.quantity_ordered - item.quantity_received }}</td>
<td>
<input type="number"
name="items[{{ item.id }}][quantity_received]"
class="form-control received-qty"
min="0"
max="{{ item.quantity_ordered - item.quantity_received }}"
required>
<input type="hidden"
name="items[{{ item.id }}][order_item_id]"
value="{{ item.id }}">
</td>
<td>
<input type="number"
name="items[{{ item.id }}][qualified_quantity]"
class="form-control qualified-qty"
min="0">
</td>
<td>
<input type="number"
name="items[{{ item.id }}][defective_quantity]"
class="form-control defective-qty"
min="0">
</td>
<td>
<input type="text"
name="items[{{ item.id }}][remark]"
class="form-control">
</td>
<td>
<button type="button" class="btn btn-sm btn-warning quality-issue-btn"
data-item-id="{{ item.id }}">
质量问题
</button>
</td>
</tr>
<tr id="quality-issues-{{ item.id }}" style="display:none;">
<td colspan="8">
<div class="quality-issue-section">
<h6>质量问题记录</h6>
<div class="issue-list">
<div class="issue-item">
<select name="items[{{ item.id }}][quality_issues][0][type]"
class="form-control mb-2">
<option value="">选择问题类型</option>
<option value="damage">损坏</option>
<option value="expired">过期</option>
<option value="wrong_spec">规格不符</option>
<option value="quality_defect">质量缺陷</option>
</select>
<textarea name="items[{{ item.id }}][quality_issues][0][description]"
class="form-control mb-2"
placeholder="问题描述"></textarea>
<select name="items[{{ item.id }}][quality_issues][0][severity]"
class="form-control mb-2">
<option value="minor">轻微</option>
<option value="major">严重</option>
<option value="critical">致命</option>
</select>
<input type="text"
name="items[{{ item.id }}][quality_issues][0][handling_method]"
class="form-control mb-2"
placeholder="处理方式(退货/换货/降级使用)">
</div>
</div>
<button type="button" class="btn btn-sm btn-info add-issue-btn"
data-item-id="{{ item.id }}">添加问题</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="form-group mt-3">
<label>验收备注</label>
<textarea name="remarks" class="form-control" rows="3"></textarea>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">确认验收</button>
<a href="/receiving" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
<script>
// 实收数量自动同步到合格数量
document.querySelectorAll('.received-qty').forEach(input => {
input.addEventListener('input', function() {
const row = this.closest('tr');
const qualifiedInput = row.querySelector('.qualified-qty');
if (!qualifiedInput.value || qualifiedInput.value === '') {
qualifiedInput.value = this.value;
}
});
});
// 显示/隐藏质量问题输入
document.querySelectorAll('.quality-issue-btn').forEach(btn => {
btn.addEventListener('click', function() {
const itemId = this.dataset.itemId;
const issuesRow = document.getElementById('quality-issues-' + itemId);
issuesRow.style.display = issuesRow.style.display === 'none' ? '' : 'none';
});
});
// 添加质量问题项
document.querySelectorAll('.add-issue-btn').forEach(btn => {
btn.addEventListener('click', function() {
const itemId = this.dataset.itemId;
const issueList = this.previousElementSibling;
const issueCount = issueList.children.length;
const newIssue = document.createElement('div');
newIssue.className = 'issue-item';
newIssue.innerHTML = `
<select name="items[${itemId}][quality_issues][${issueCount}][type]"
class="form-control mb-2">
<option value="">选择问题类型</option>
<option value="damage">损坏</option>
<option value="expired">过期</option>
<option value="wrong_spec">规格不符</option>
<option value="quality_defect">质量缺陷</option>
</select>
<textarea name="items[${itemId}][quality_issues][${issueCount}][description]"
class="form-control mb-2"
placeholder="问题描述"></textarea>
<select name="items[${itemId}][quality_issues][${issueCount}][severity]"
class="form-control mb-2">
<option value="minor">轻微</option>
<option value="major">严重</option>
<option value="critical">致命</option>
</select>
<input type="text"
name="items[${itemId}][quality_issues][${issueCount}][handling_method]"
class="form-control mb-2"
placeholder="处理方式(退货/换货/降级使用)">
`;
issueList.appendChild(newIssue);
});
});
</script>
{% endblock %}
路由配置
// routes.php 或 index.php
$router = new Router();
// 验收相关路由
$router->get('/receiving', 'ReceivingController@index');
$router->get('/receiving/{order_id}/create', 'ReceivingController@create');
$router->post('/receiving/store', 'ReceivingController@store');
$router->get('/receiving/{id}', 'ReceivingController@show');
注意事项
-
安全考虑:
- 使用参数化查询防止SQL注入
- 实现CSRF令牌验证
- 添加用户权限控制(验收人必须有相应权限)
-
业务逻辑:
- 验收数量不能大于订单剩余数量
- 合格数量 + 不合格数量 = 实收数量
- 支持分批验收
- 记录验收人和验收时间
-
扩展功能:
- 添加附件上传功能(验收照片)
- 打印验收单PDF
- 与库存系统集成(自动入库)
- 验收流程审批(需要审批的验收)
这个实现提供了一个完整的到货验收系统,涵盖了从订单查看、验收录入到质量管理的全过程,你可以根据实际业务需求进行调整和扩展。