本文目录导读:

在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缓存盘点进度,提升用户体验。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播