本文目录导读:

在PHP项目中实现盘点管理,通常需要构建一个包含数据模型、业务逻辑和用户界面的完整模块,以下是实现步骤和关键设计思路:
数据库设计(核心表结构)
需要至少两张核心表:盘点任务表 和 盘点明细表。
-- 盘点任务表 (stocktaking_tasks)
CREATE TABLE `stocktaking_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`task_no` VARCHAR(50) NOT NULL UNIQUE COMMENT '盘点单号',
`warehouse_id` INT NOT NULL COMMENT '仓库ID',
`status` ENUM('pending','in_progress','completed','cancelled') DEFAULT 'pending' COMMENT '状态',
`created_by` INT NOT NULL COMMENT '创建人',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`completed_at` DATETIME NULL COMMENT '完成时间',
`remark` TEXT COMMENT '备注'
) ENGINE=InnoDB;
-- 盘点明细表 (stocktaking_details)
CREATE TABLE `stocktaking_details` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`task_id` INT NOT NULL COMMENT '任务ID',
`product_id` INT NOT NULL COMMENT '商品ID',
`expected_qty` DECIMAL(10,2) DEFAULT 0 COMMENT '系统账面数量',
`actual_qty` DECIMAL(10,2) DEFAULT 0 COMMENT '实盘数量',
`difference_qty` DECIMAL(10,2) GENERATED ALWAYS AS (actual_qty - expected_qty) STORED COMMENT '差异数量',
`status` ENUM('pending','counted','confirmed') DEFAULT 'pending' COMMENT '明细状态',
`operator_id` INT COMMENT '盘点人',
`counted_at` DATETIME COMMENT '盘点时间',
`remark` TEXT,
FOREIGN KEY (`task_id`) REFERENCES `stocktaking_tasks`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
PHP业务逻辑层(推荐使用MVC架构)
1 盘点任务控制器(StocktakingController.php)
<?php
class StocktakingController {
private $db;
public function __construct($database) {
$this->db = $database;
}
// 创建盘点任务
public function createTask($warehouseId, $userId, $remark = '') {
$taskNo = $this->generateTaskNo();
$sql = "INSERT INTO stocktaking_tasks (task_no, warehouse_id, created_by, remark)
VALUES (:task_no, :warehouse_id, :created_by, :remark)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':task_no' => $taskNo,
':warehouse_id' => $warehouseId,
':created_by' => $userId,
':remark' => $remark
]);
$taskId = $this->db->lastInsertId();
// 自动生成盘点明细(获取该仓库所有商品库存)
$this->generateTaskDetails($taskId, $warehouseId);
return $taskId;
}
// 生成盘点明细(从库存表同步账面数据)
private function generateTaskDetails($taskId, $warehouseId) {
// 假设有一个 inventory 表存储实时库存
$sql = "SELECT product_id, quantity AS expected_qty
FROM inventory WHERE warehouse_id = :warehouse_id";
$stmt = $this->db->prepare($sql);
$stmt->execute([':warehouse_id' => $warehouseId]);
$inventories = $stmt->fetchAll();
$insertSql = "INSERT INTO stocktaking_details (task_id, product_id, expected_qty)
VALUES (:task_id, :product_id, :expected_qty)";
$insertStmt = $this->db->prepare($insertSql);
foreach ($inventories as $inv) {
$insertStmt->execute([
':task_id' => $taskId,
':product_id' => $inv['product_id'],
':expected_qty' => $inv['expected_qty']
]);
}
}
// 录入盘点数据
public function countItem($detailId, $actualQty, $operatorId) {
$sql = "UPDATE stocktaking_details
SET actual_qty = :actual_qty,
status = 'counted',
operator_id = :operator_id,
counted_at = NOW()
WHERE id = :id AND status = 'pending'";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':actual_qty' => $actualQty,
':operator_id' => $operatorId,
':id' => $detailId
]);
}
// 提交盘点结果(生成差异报告并更新库存)
public function completeTask($taskId) {
$this->db->beginTransaction();
try {
// 1. 检查是否所有明细都已盘点
$sql = "SELECT COUNT(*) FROM stocktaking_details
WHERE task_id = :task_id AND status = 'pending'";
$stmt = $this->db->prepare($sql);
$stmt->execute([':task_id' => $taskId]);
if ($stmt->fetchColumn() > 0) {
throw new Exception("还有未盘点的商品,无法完成任务");
}
// 2. 更新库存(根据盘点差异进行增减)
$sql = "SELECT product_id, difference_qty FROM stocktaking_details
WHERE task_id = :task_id AND difference_qty != 0";
$stmt = $this->db->prepare($sql);
$stmt->execute([':task_id' => $taskId]);
// 假设有一个库存调整表 stock_adjustments,或者直接更新inventory表
$updateSql = "UPDATE inventory SET quantity = quantity + :diff_qty
WHERE product_id = :product_id AND warehouse_id = :warehouse_id";
// 获取该任务的仓库ID
$warehouseId = $this->getTaskWarehouseId($taskId);
while ($row = $stmt->fetch()) {
$updateStmt = $this->db->prepare($updateSql);
$updateStmt->execute([
':diff_qty' => $row['difference_qty'],
':product_id' => $row['product_id'],
':warehouse_id' => $warehouseId
]);
}
// 3. 更新任务状态
$sql = "UPDATE stocktaking_tasks
SET status = 'completed', completed_at = NOW()
WHERE id = :id";
$this->db->prepare($sql)->execute([':id' => $taskId]);
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
private function generateTaskNo() {
return 'PD' . date('YmdHis') . rand(100, 999);
}
}
前端交互示例(简单的盘点操作页面)
<!-- 盘点任务列表 -->
<div class="task-list">
<?php foreach ($tasks as $task): ?>
<div class="task-card">
<h3>盘点单: <?= $task['task_no'] ?></h3>
<p>状态: <span class="badge badge-<?= $task['status'] ?>"><?= $task['status'] ?></span></p>
<p>仓库: <?= $task['warehouse_name'] ?></p>
<p>创建时间: <?= $task['created_at'] ?></p>
<?php if ($task['status'] === 'pending' || $task['status'] === 'in_progress'): ?>
<a href="/stocktaking/count/<?= $task['id'] ?>" class="btn btn-primary">开始盘点</a>
<a href="/stocktaking/complete/<?= $task['id'] ?>" class="btn btn-success">完成盘点</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- 盘点录入页面(按商品逐条录入) -->
<form action="/stocktaking/saveCount" method="POST">
<input type="hidden" name="task_id" value="<?= $taskId ?>">
<table>
<thead>
<tr>
<th>商品名称</th>
<th>SKU</th>
<th>账面数量</th>
<th>实盘数量</th>
<th>差异</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($details as $detail): ?>
<tr>
<td><?= $detail['product_name'] ?></td>
<td><?= $detail['sku'] ?></td>
<td><?= $detail['expected_qty'] ?></td>
<td>
<input type="number" name="actual_qty[<?= $detail['id'] ?>]"
value="<?= $detail['actual_qty'] ?: '' ?>"
step="0.01" min="0">
</td>
<td class="diff-<?= $detail['difference_qty'] > 0 ? 'positive' : 'negative' ?>">
<?= $detail['difference_qty'] !== null ? number_format($detail['difference_qty'], 2) : '—' ?>
</td>
<td>
<button type="submit" formaction="/stocktaking/countSingle/<?= $detail['id'] ?>">
录入
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</form>
关键设计要点
-
数据一致性
- 使用数据库事务保证盘点结果与库存更新的原子性
- 盘点期间锁定该仓库的库存变动(可在任务创建时设置库存锁定状态)
-
差异处理策略
- 盘点差异需要经过审核流程(建议增加
approved_by和approved_at字段) - 重大差异(如超过10%)可以设置预警通知
- 盘点差异需要经过审核流程(建议增加
-
性能优化
- 大批量盘点时,采用分批提交(每次100-200条)
- 盘点明细表建议建立索引:
(task_id, status)
-
扩展功能
- 支持条形码/二维码扫描录入(集成扫码API)
- 生成盘点报告PDF(使用TCPDF或FPDF库)
- 部分盘点(仅盘点特定货架或商品类别)
安全注意事项
// 输入验证示例
public function validateInput($data, $detailId) {
if (!is_numeric($data['actual_qty']) || $data['actual_qty'] < 0) {
throw new InvalidArgumentException("实盘数量必须为非负数");
}
// 权限检查:仅允许指定盘点员操作
if (!in_array($_SESSION['role'], ['stock_manager', 'stock_keeper'])) {
throw new AccessDeniedException("无盘点权限");
}
}
测试建议
# 单元测试类
class StocktakingTest extends PHPUnit\Framework\TestCase {
public function testCreateTaskGeneratesDetails() {
// 创建测试用仓库和商品
// 调用 createTask 方法
// 断言 stocktaking_details 表有对应记录
}
public function testCompleteTaskUpdatesInventory() {
// 模拟盘点数据
// 调用 completeTask
// 验证 inventory 表数量已更新
}
}
使用Laravel框架时,可以利用其Eloquent ORM简化数据库操作,并配合队列系统处理大批量盘点任务,建议配合使用Redis缓存盘点进度,提升用户体验。