怎样在PHP项目中实现盘点管理?

wen java案例 4

本文目录导读:

怎样在PHP项目中实现盘点管理?

  1. 数据库设计(核心表结构)
  2. PHP业务逻辑层(推荐使用MVC架构)
  3. 前端交互示例(简单的盘点操作页面)
  4. 关键设计要点
  5. 安全注意事项
  6. 测试建议

在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>

关键设计要点

  1. 数据一致性

    • 使用数据库事务保证盘点结果与库存更新的原子性
    • 盘点期间锁定该仓库的库存变动(可在任务创建时设置库存锁定状态)
  2. 差异处理策略

    • 盘点差异需要经过审核流程(建议增加approved_byapproved_at字段)
    • 重大差异(如超过10%)可以设置预警通知
  3. 性能优化

    • 大批量盘点时,采用分批提交(每次100-200条)
    • 盘点明细表建议建立索引:(task_id, status)
  4. 扩展功能

    • 支持条形码/二维码扫描录入(集成扫码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缓存盘点进度,提升用户体验。

抱歉,评论功能暂时关闭!