尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PHP仓储后台管理系统实战:库存事务与对账方案详解

PHP仓储后台管理系统实战:库存事务与对账方案详解 简介一套基于 PHP 的仓储后台管理系统源码与数据库打包资源面向 PHP 开发者及需要快速搭建库存管理系统的团队聚焦仓库货品出入库操作与库存查询场景基于 MySQL CodeIgniter jQueryUI 架构实现。压缩包共 1602 个文件包含 262 个 PHP 业务脚本、522 个 HTML 页面、381 个 CSS 样式、257 张 PNG 图标及 JS、SQL、配置文件等整体大小仅 4.09MB部署轻量。包内附完整 wms 数据库 SQL 文件预设管理员账号 admin/admin安装时按注释修改 application/config/database.php 和 config.php 中的 base_url 即可运行。通过源码可学习 CI 框架的 MVC 分层、数据库操作、jQueryUI 界面组件及库存模块的数据表设计思路适合作为课程设计或入门项目参考。当前已有 950 人学习下载。1. 基于PHP的仓储后台管理系统小仓库最务实的落地方案一个五十个 SKU、日均两三百单出入库的小仓库买商业 WMS 一年大几千还绑定操作习惯引 ERP 又明显过重。很多做电商、做加工配套的团队最后的做法是用 PHP 自己搭一套仓储后台管理系统源码和数据库一起交付接口、报表、权限都能自己改。开发成本基本集中在一件事上把单据流和库存账算准。下面按表结构、事务代码、部署配置再到对账技巧把一个线上可跑的方案完整讲一遍。适合刚接手 PHP 项目的人也适合想把现有系统库存部分重写一遍的熟手。2. 仓储后台的数据模型先把单据流和库存账拆清楚2.1 把业务动作收敛成核心表仓储后台管理系统在中小团队里最常见的形态是「一张出库单、一张入库单、一个库存数字」。再复杂的要求比如批次、效期、多仓、条码都从这三点延伸。我一般把表设计收敛成这样避免一上来铺开几十张表结果单据和库存对不上账表名职责关键字段wms_product商品主数据pid, sku, name, spec, unitwms_warehouse仓库与货位wid, name, location_codewms_inbound入库单头order_no, type, status, operatorwms_inbound_item入库明细order_id, product_id, qtywms_outbound出库单头order_no, type, status, receiverwms_outbound_item出库明细order_id, product_id, qtywms_inventory实时库存warehouse_id, location_id, product_id, qtywms_stock_log库存流水type, before_qty, after_qty, change_qty, ref_order_no注意这是「两对单头 明细」的经典结构目的是一张单子支持多种货物明细表靠order_id关联不冗余存仓库信息仓库只待在单头上。这样「整单改仓」就只需要 update 单头一行。很多从 Excel 迁移过来的需求第一版就毁在把每种货做成一个字段比如product1_qty、product2_qty后续加一个 SKU 就要改表结构这条路千万别走。2.2 库存表为什么按「一仓一货位一商品」一行来设计初学阶段容易把库存做成「商品一个数字」在 product 表里放stock字段。这在单仓、无货位的小卖部场景勉强能跑一旦出现同仓两个货位、同一 SKU 分两次到货就彻底失控了。库存表必须按维度拆行联合唯一键是这个设计的地基CREATE TABLE wms_inventory ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, warehouse_id INT UNSIGNED NOT NULL COMMENT 仓库ID, location_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 货位ID, 0表示无货位, product_id INT UNSIGNED NOT NULL COMMENT 商品ID, qty INT NOT NULL DEFAULT 0 COMMENT 可用库存, frozen_qty INT NOT NULL DEFAULT 0 COMMENT 锁定库存, 出库预占, version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, update_time DATETIME NOT NULL, UNIQUE KEY uk_wh_loc_pro (warehouse_id, location_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联合唯一键uk_wh_loc_pro保证同一维度不会出现两行入库时用ON DUPLICATE KEY UPDATE就能天然累加。qty用 INT 而不用 DECIMAL是因为大部分仓储计量单位是件、箱整数加减没有浮点误差如果做的是公斤、米这种计量就改成DECIMAL(14,3)同时所有代码统一按 DECIMAL 传参不能用字符串拼接。frozen_qty是给「先下单后出库」用的预占库存电商场景很常见纯内部调拨可以不用。注意wms_inventory是「结果」wms_stock_log是「过程」。任何对库存表的写操作都必须同步写一条流水否则盘不出差异时没有任何线索。流水表最忌只记一个change_qty最好把变动前后的值都记下来。出了账实差异靠这三个字段能直接推算每一步的因果链而不是对着订单编号猜。2.3 单据与库存的入账时机先建单、再入账禁止「保存即扣」很多半路出家的后台系统把库存变更直接写在「新增出库单」接口里保存单据的同时 update 库存。隐患在于单据一旦被中途修改或删除库存已经动过了只能靠人工再补一笔时间一长必然对不上。正确时序是单据先落库为「草稿」状态操作员确认无误后点「审核」审核动作才在一个事务里完成三件事——更新单据状态、变更库存、写流水。事务边界必须包住这三个动作任何一个失败就整体回滚。单据状态固定用三态draft草稿、done已入账、cancel已作废。不允许存在「已审核但库存没动」的状态审核就是入账触发器。反审核红冲另做一张负向单据而不是直接去改库存审计链路才能完整。3. 用 PHP 把入库出库跑成事务PDO 代码与行锁3.1 PDO 连接参数这样配事务才靠得住仓储后台管理系统用原生 PHP 还是框架我的看法是核心业务几十个文件以内原生 PHP PDO 比强行套 Laravel 更好维护如果你已经用了 ThinkPHP 之类的框架底层同样绕不开 PDO 这套连接参数。连接配置是最容易被忽略的一层很多「莫名其妙丢事务」的 bug 都出在这里?php $dsn mysql:host127.0.0.1;port3306;dbnamewms;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, PDO::ATTR_PERSISTENT false, ]; $pdo new PDO($dsn, wms_user, wms_pass, $options);四个参数各有讲究。ERRMODE_EXCEPTION让 SQL 错误直接抛异常配合事务才能做到出错即回滚EMULATE_PREPARES设为 false让 MySQL 服务端做预处理避免占位符被本地转义后类型混乱where 条件里传数字字符串也不容易踩索引失效。PERSISTENT关掉长连接PHP-FPM 下长连接会和连接池抢占 MySQL 连接数流量稍大就把max_connections打满。charsetutf8mb4直接写在 DSN 里比后执行的SET NAMES更早生效从源头避免中文乱码。3.2 入库接口库存累加与流水写入同一事务入库的核心代码可以这样写。假设$order已包含仓库和货位信息$items是明细数组$pdo是上面的连接实例?php try { $pdo-beginTransaction(); foreach ($items as $it) { // 1. 先锁库存行, 拿变动前的值, 这一行不存在时 FOR UPDATE 不阻塞 $stmt $pdo-prepare( SELECT qty FROM wms_inventory WHERE warehouse_id ? AND location_id ? AND product_id ? FOR UPDATE ); $stmt-execute([$order[warehouse_id], $it[location_id], $it[product_id]]); $row $stmt-fetch(); $beforeQty $row ? (int)$row[qty] : 0; // 2. 存在就累加, 不存在就插入; 唯一键是并发的最后防线 $sql INSERT INTO wms_inventory (warehouse_id, location_id, product_id, qty, update_time) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE qty qty VALUES(qty), update_time NOW(); $pdo-prepare($sql)-execute([ $order[warehouse_id], $it[location_id], $it[product_id], $it[qty] ]); // 3. 写流水, 保留变动前后值和差额 $logSql INSERT INTO wms_stock_log (product_id, type, ref_order_no, before_qty, after_qty, change_qty, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW()); $pdo-prepare($logSql)-execute([ $it[product_id], inbound, $order[order_no], $beforeQty, $beforeQty $it[qty], $it[qty] ]); } // 4. 同一事务里把单据置为已入账 $pdo-prepare(UPDATE wms_inbound SET status done, audit_time NOW() WHERE id ?) -execute([$order[id]]); $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); error_log($e-getMessage()); // 返回前端: 入账失败, 单据保持 draft 状态 }ON DUPLICATE KEY UPDATE把「判断存在、决定插入还是更新」合并成一个原子操作省掉一次 SELECT 往返和一段锁等待窗口。MySQL 8.0.20 之后VALUES()语法已标记废弃可以写成INSERT ... AS new ON DUPLICATE KEY UPDATE qty qty new.qty语义完全等价。before_qty用FOR UPDATE查出来而不是靠影响行数推断流水里永远有据可查。最后更新单据状态也在同一事务里明细任何一条失败整张单都不会变成 done。3.3 出库接口、负库存校验与行锁的边界出库比入库多一步先确认库存够不够。常见错误是「先 SELECT 判断 qty 够再 UPDATE」两个请求并发时判断结果互相覆盖于是出现负库存。解决方法是把判断和扣减放进同一行锁保护下?php $pdo-beginTransaction(); // 按唯一键取行, FOR UPDATE 锁住这一行直到事务结束 $sql SELECT qty FROM wms_inventory WHERE warehouse_id ? AND location_id ? AND product_id ? FOR UPDATE; $stmt $pdo-prepare($sql); $stmt-execute([$order[warehouse_id], $it[location_id], $it[product_id]]); $row $stmt-fetch(); if (!$row || $row[qty] $it[qty]) { $pdo-rollBack(); throw new RuntimeException(库存不足: product_id . $it[product_id]); } $pdo-prepare(UPDATE wms_inventory SET qty qty - ?, update_time NOW() WHERE id ?) -execute([$it[qty], $row[id]]); $pdo-prepare(INSERT INTO wms_stock_log (product_id, type, ref_order_no, before_qty, after_qty, change_qty, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW())) -execute([$it[product_id], outbound, $order[order_no], $row[qty], $row[qty] - $it[qty], -$it[qty]]); $pdo-commit();FOR UPDATE锁的是 InnoDB 索引记录范围精确到唯一键对应的那一行不会锁全表。但有两个边界必须知道。一是锁在事务提交或回滚时才释放事务里别做远程调用、别 sleep否则并发一高全是Lock wait timeout exceeded。二是明细有多个商品时必须先按 product_id 排序再逐行处理否则两个出库单各持一行锁、又互相等对方那一行就会死锁MySQL 检测到死锁会回滚一方表现是「偶尔一条单子报错」排序之后这类问题基本绝迹。frozen_qty预占流程可以在这段代码上扩展下单时只扣 frozen 不动 qty出库时把 frozen 转成实际扣减公式是qty qty - 1, frozen_qty frozen_qty - 1保证「锁定库存 可用库存」恒等于总库存。这个公式在并发下不会出现超卖因为两行更新都在同一事务里。4. 把系统跑起来数据库初始化、PHP 部署与三个必调参数4.1 初始数据库把数据模型落到 MySQL拿到「源码数据库」压缩包之后最常见的第一个动作是导入数据库。我不会直接双击导入现成 dump而是先跑一遍建表脚本确认表结构跟自己业务对得上再决定要不要用自带的种子数据。下面是裁剪到最小可跑的初始化 SQLCREATE DATABASE IF NOT EXISTS wms DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE wms; CREATE TABLE wms_product ( pid INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, sku VARCHAR(32) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL, spec VARCHAR(64) DEFAULT , unit VARCHAR(8) NOT NULL DEFAULT 件, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; INSERT INTO wms_product (sku, name, unit) VALUES (SKU001, A4复印纸, 箱), (SKU002, 签字笔黑色, 盒); -- 服务端时区统一为东八区, 避免 NOW() 和 PHP date() 各差 8 小时 SET time_zone 08:00;utf8mb4_unicode_ci下sku 唯一索引对英文大小写不敏感业务上要区分大小时改成utf8mb4_bin。种子数据只插商品主数据库存从零开始由入库单产生。不要图省事直接往wms_inventory里塞初始值否则流水缺失后续盘点永远对不平。数据库账号要给独立用户走wms_user127.0.0.1限定来源root 只留本地 socket 登录。4.2 PHP 版本与运行环境nginx php-fpm 最小配置仓储后台管理系统跑在 LNMP 环境最省心。PHP 版本建议 7.4 到 8.2 之间8.0 以上对类型声明和性能的提升明显老项目若还在 PHP 5.xmysql_*函数在 7.0 后已被移除任何「源码包直接跑不起来」的问题八成出在这。nginx 站点配置只需保证 PHP 请求正确转发到 php-fpmserver { listen 80; server_name wms.example.com; root /var/www/wms/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }try_files那行用来兼容不带 index.php 的伪静态路径让入口集中到单一脚本SCRIPT_FILENAME必须显式拼出真实路径否则 php-fpm 返回空白页。压缩包如果带.user.ini或.htaccess在 nginx 下不生效需要把里面的upload_max_filesize、post_max_size搬到 php.ini。写入目录要给 php-fpm 运行用户我统一chown -R www-data:www-data不给 777。4.3 三个必调参数时区、内存、上传大小参数位置推荐值说明date.timezonephp.iniAsia/Shanghai配合 MySQLtime_zone08:00两边 NOW() 才一致memory_limitphp.ini128M导出报表可临时调 512M仓储系统里最重的操作是导出商品明细条目多时极易触顶post_max_size/upload_max_filesizephp.ini32M / 20M批次导入的 CSV、装箱照片都走 POST 上传默认 8M 太小max_execution_timephp.ini30CLI 脚本设 0万行级导入不能走 HTTP 请求拆成 CLI 任务执行最后一行其实是场景切换。批量导入一万行商品或出库明细时HTTP 30 秒上限几乎必挂。正确做法是把解析和入库拆成 CLI 脚本跑每 500 行提交一个事务出错只回滚当前批次日志记到第几行修好数据后断点续跑。这个习惯在仓储系统里比任何框架优化都值钱。数据库连接数也顺手说一句php-fpm 每个进程持一个连接pm.max_children50就意味着最多 50 个并发连接。出现Too many connections时先SHOW PROCESSLIST看是不是全是 Sleep再决定调max_connections还是调pm.max_children别一上来就改 MySQL。5. 给后台加一个「库存对账」菜单十分钟写出盘点差异查询对账是仓储后台管理系统最值得先做的高级功能逻辑简单、价值直观一分钟内就能暴露「账实不符」的具体商品。思路是拿 wms_stock_log 流水按商品累计差额再和 wms_inventory 当前值比对有差异的就是丢失或漏记的节点SELECT t.product_id, SUM(t.change_qty) AS log_total, i.qty AS inv_qty, i.qty - SUM(t.change_qty) AS diff FROM wms_stock_log t LEFT JOIN wms_inventory i ON i.product_id t.product_id WHERE t.create_time 2025-01-01 00:00:00 GROUP BY t.product_id, i.qty HAVING diff 0;这条 SQL 依赖流水表里冗余存了change_qty。SUM(change_qty)是从统计起点到现在的净变动量i.qty是当前账面值两者相减的diff如果不为 0说明要么流水漏记、要么库存被直接改过、要么期初数本身不对。HAVING diff 0把所有异常商品一次列全再 JOIN 商品表带出 sku 和名称就是一个可以直接挂进后台菜单的差异页。上线初期每天凌晨用 cron 跑一遍结果写入差异表早上看表就行不用人肉翻 Excel。再往外扩一步是盘点单仓库实盘数量录入后系统自动算出每个 SKU 盈亏数生成一张盘盈盘亏单管理员审核通过后才真正调整库存并补一条stocktake类型的流水。这样闭环下来任何一笔库存变动都能追溯到单据或盘点单。仓储后台管理系统最怕的「库存悄悄变了没人知道」到这个程度就彻底根治了。本文还有配套的精品资源点击获取
返回列表