ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP云进销存源码深度拆解:部署、库存联动与权限设计

PHP云进销存源码深度拆解:部署、库存联动与权限设计 简介一套以PHPMYSQL构建的云进销存ERP系统源码面向中小型企业的销售、库存与员工管理场景适合有PHP开发基础的技术人员直接部署或二次开发。系统采用B/S架构覆盖购货、销货、仓库、商品、供应商、职员等模块支持出入库、多仓库调拨、库存盘点、收款付款单据、采购/仓库/资金报表以及权限设置2022独家版本还加入合同报价单打印并修复了子账号无法查看新加客户的BUG。压缩包共1229个文件其中501个PHP文件承载核心业务逻辑191个JS脚本和49个HTML、42个CSS组成前端界面另有图片、SQL数据库脚本及说明文档等压缩后约24.39MB目录结构清晰便于按模块快速定位。已有92人学习资源内包含可运行的完整源码、数据库脚本、功能清单与更新说明可帮助读者快速搭建一套进销存系统或作为ERP相关开发学习的参考案例。1. 这套2022版PHP云进销存为什么还值得从源码层面拆一遍很多公司的进销存还停留在Excel加微信汇报的阶段库存账、应收账、员工提成三套数对不上。这套PHP云进销存本质上是一个B/S结构的ERP用PHP加MySQL跑起来浏览器打开就能用天然适合云部署。和网上流传的老版本相比2022版多了合同报价单打印修掉了子账号看不到新增客户的Bug也补了一些报表维度适合LNMP或宝塔环境下快速支起一套内部系统也适合有PHP基础的工程师拿源码改成自己的进销存底座。我拆这套源码时最深的印象是单表设计并不复杂但库存联动和权限边界是决定系统能不能长期跑下去的关键。下面按环境部署、数据库、权限、排错四条线展开。2. 环境搭建与源码部署PHP版本兼容、目录权限和伪静态2.1 部署环境选型宝塔面板还是手工LNMP这套源码的常见运行环境是Nginx PHP MySQL很多生产环境跑在宝塔面板里。宝塔的好处是PHP扩展和站点配置都能在可视化界面里改遇到PHP版本切换、伪静态配置、目录权限问题比命令行快很多。安装源码前先把PHP扩展补齐pdo_mysql、mysqli、gd、mbstring、curl、fileinfo。GD扩展缺失最典型的表现是登录页验证码不显示这跟很多宝塔PHP验证码报错是同一个原因在PHP设置里找到扩展点开安装然后重启PHP-FPM即可。location / { if (!-e $request_filename){ rewrite ^(.*)$ /index.php?s$1 last; } }上面是Nginx的伪静态配置作用是把不存在的文件或目录请求全部Rewrite到index.php入口由PHP框架解析路由参数s。源码如果基于ThinkPHP之类的框架这条规则是标配如果换成Apache则要写.htaccess文件。配置完伪静态后一定要重启Nginx否则页面会一直404。不要漏掉location /这段的作用域站点配置里可能还有其他location规则顺序靠前的规则会先匹配。2.2 导入数据库与修改数据库连接配置源码包里一般会带.sql文件或在安装向导里自动导入。用phpMyAdmin导入时先确认数据库字符集是utf8mb4避免中文乱码。导入之后找到配置文件常见路径是application/database.php也可能是config/db.php。数据库连接配置就像这样return [ hostname 127.0.0.1, database erp_cloud, username erp_user, password 你的密码, hostport 3306, charset utf8mb4, prefix erp_, ];配置项里最容易被忽略的是prefix。安装时如果改过表前缀这里必须同步否则系统会提示数据表不存在。部署中常见的问题是数据库密码里带了特殊字符比如#、、$在配置文件里需要根据语法规则处理。数组配置用单引号包裹就没有字符串转义问题如果是PHP变量拼接方式特殊字符很容易导致连接失败。配置保存后用命令行验证能更清楚定位错误php -r new PDO(mysql:host127.0.0.1;dbnameerp_cloud, erp_user, 你的密码); echo ok;执行后输出ok说明数据库连接成功。如果抛PDOException先检查账号权限是不是只允许了localhost访问远程连接请把MySQL绑定地址设为0.0.0.0并调整账号host。2.3 初始化数据与管理员账号数据库导入完成后访问站点根目录通常会出现安装引导页面按步骤填写管理员账号和公司信息。如果源码直接给定了初始账号那就登录后台做一次基础数据初始化。建议先停掉线上流量把基础设置里的默认仓库、默认供应商、默认结算方式、税率都填好。这些基础数据决定了后续单据的带出值现在不配置后面每次录单都要手选效率非常低。2.4 部署验证与日志定位部署成功后用curl验证首页是否正常curl -I http://你的域名或IP/erp返回200说明Nginx和PHP链路通了返回301/302则要核对站点根目录和伪静态。返回500时去PHP错误日志查看宝塔面板里能直接看到错误日志路径常见问题是php.ini里display_errors被关闭导致白屏但运行环境里更建议关闭display_errors把错误写入日志文件方便按时间点定位。日志级别建议调到E_ALL然后看PHP版本是否提示某个函数被废弃。3. 核心模块与数据库设计库存流水、多仓库调拨与盘点3.1 关键数据表结构商品、仓库、库存如何联动进销存系统的表数量不会少但核心关系其实很收敛商品表管理产品主数据仓库表管理物理或逻辑库位库存表记录每个商品在每个仓库的数量。库存表的设计最值得注意建议用唯一键保证同一商品在同一仓库只能存在一条记录。CREATE TABLE erp_stock ( id INT NOT NULL AUTO_INCREMENT, warehouse_id INT NOT NULL COMMENT 仓库ID, product_id INT NOT NULL COMMENT 商品ID, stock_qty DECIMAL(15,3) NOT NULL DEFAULT 0 COMMENT 当前库存, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_wh_prod (warehouse_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;stock_qty用DECIMAL(15,3)而不用FLOAT是因为金额和数量的累加过程如果走浮点很可能会出现0.10.2不等于0.3的误差。库存字段宁可多保留三位小数也不要为了省空间用整型。唯一键uk_wh_prod既避免了重复数据也让后面的INSERT...ON DUPLICATE KEY UPDATE可以直接依赖它来做原子更新。3.2 入库出库的库存更新事务和原子操作缺一不可采购入库的业务流程是先写单据主表再写单据明细最后更新库存。如果只更新库存不写单据报表里查不到入库来源如果先写单据再更新库存时失败库存账就和单据账不一致。所以整个流程必须放在一个事务里。常见做法是把库存操作封装成一个PHP类类里的入库方法执行下面的SQLBEGIN; INSERT INTO erp_purchase_head (order_no, supplier_id, warehouse_id, biz_date, status) VALUES (RK20241009-001, 2, 1, 2024-10-09, 1); SET head_id LAST_INSERT_ID(); INSERT INTO erp_purchase_item (head_id, product_id, qty, price) VALUES (head_id, 101, 10, 12.50); INSERT INTO erp_stock (warehouse_id, product_id, stock_qty) VALUES (1, 101, 10) ON DUPLICATE KEY UPDATE stock_qty stock_qty 10; COMMIT;注意这三条SQL在同一个事务里。先用LAST_INSERT_ID()拿到刚写入的主表ID再写明细最后用INSERT...ON DUPLICATE KEY UPDATE处理库存。这个写法的妙处在于如果库存表里没有该商品对应仓库的记录就直接插入如果已有记录会执行UPDATE把库存加10。相比SELECT后判断再INSERT不仅少一次查询还避免了并发下两个请求同时SELECT到不存在记录然后同时插入导致唯一键冲突的问题。3.3 多仓库调拨一减一加不能分开执行调拨单本质上是把库存从调出仓转移到调入仓。如果拆成两条独立SQL调出扣减成功、调入新增失败总库存就平白少了一截。所以调拨必须在一个事务里做双向更新推荐把扣减条件带上库存下限约束。/** * 执行调拨 * param int $fromWarehouseId 调出仓库 * param int $toWarehouseId 调入仓库 * param array $items 明细列表 */ function doTransfer($fromWarehouseId, $toWarehouseId, $items) { try { pdo()-beginTransaction(); foreach ($items as $item) { // 调出仓库扣减库存带上 stock_qty :qty 防止负库存 $sqlMinus UPDATE erp_stock SET stock_qty stock_qty - :qty WHERE warehouse_id :from_wh AND product_id :pid AND stock_qty :qty; $affected pdo()-execute($sqlMinus, [ :qty $item[qty], :from_wh $fromWarehouseId, :pid $item[product_id], ]); if ($affected 0) { throw new \RuntimeException(商品ID . $item[product_id] . 库存不足); } // 调入仓库库存增加 $sqlPlus INSERT INTO erp_stock (warehouse_id, product_id, stock_qty) VALUES (:to_wh, :pid, :qty) ON DUPLICATE KEY UPDATE stock_qty stock_qty :qty; pdo()-execute($sqlPlus, [ :to_wh $toWarehouseId, :pid $item[product_id], :qty $item[qty], ]); } // 写调拨单主表和明细表这里省略 pdo()-commit(); } catch (\Throwable $e) { pdo()-rollBack(); throw $e; } }这个函数里的UPDATE返回影响行数是关键。如果库存只有5件但调拨单要求10件WHERE条件中的stock_qty :qty不成立影响行数为0函数直接抛异常事务回滚不会出现负库存。调用方在编辑器或API层捕获RuntimeException后把“库存不足”作为业务提示返回给用户。3.4 库存盘点和差异调整不要直接修改库存表盘点功能通常先按仓库和商品导出账面库存再录入实际盘点的数量。系统根据“实盘数量 - 账面数量”计算差异差异数量不为0时生成一张盘点差异单。正向差异做入库调整负向差异做出库调整。差异单的本质是一张普通出入库单唯一的区别是关联了盘点单号这样报表里能追溯差异来源。下面这张表是盘点差异记录常用的字段说明源码里字段名可能有差异但含义基本一致。字段名含义说明stocktake_id盘点单号关联盘点主表warehouse_id仓库ID盘点发生的仓库product_id商品ID被盘点的商品book_qty账面数量盘点时系统里的库存量real_qty实盘数量人工清点的数量diff_qty差异数量实盘-账面正数盈负数亏status单据状态0草稿1已审核2已冲销reviewed_by审核人记录是谁审核的差异单实现时盘点单审核后调用一次库存调整方法不要直接UPDATE库存表。直接改库存表会留下口径黑洞资金报表和采购报表会差出莫名其妙的数字。审核动作和库存调整也要放在一个事务里避免审核成功了但库存没调或者库存调了但盘点单还在草稿状态。4. 权限控制、子账号客户数据隔离与打印模板改造4.1 权限模型菜单权限和数据权限分开看很多进销存系统的权限管理只做了菜单权限也就是先给角色勾选能访问哪些菜单再把角色绑定到账号上。但菜单权限控制不了“某一个子账号能看到哪些客户”这类数据级问题。真正要跑通业务流程数据权限必须单独考虑。比如销售员角色能看客户管理菜单但只能看到自己名下的客户仓库文员能看到所有库存记录但不能看成本价和毛利。这套源码里如果沿用网上老版本数据权限通常是按字段过滤实现的比如客户表里保存owner_id。权限逻辑一般写在控制器的查询方法里判断当前登录用户的角色后追加条件。遇到性能问题不要用PHP循环去过滤结果集而是在SQL的WHERE里直接用IN子查询或用JOIN关联业务员客户关系表。4.2 子账号看不到新加客户BUG根因和修复方法网上流传的老版常见问题是子账号登录后老客户显示正常新建的客户却看不到。问题出在新增客户时没有把当前子账号ID写入客户所属人字段。列表查询按所属人过滤新客户的所属人为空子账号自然查不到。修复方式是在新增客户的控制器里明确写入当前登录用户ID$data[customer_owner] session(user_id); $data[created_by] session(user_id);这里的session(user_id)是当前登录账号的ID具体函数名以源码为准可能是$_SESSION[admin_id]或helper函数user_id()。写入created_by是为了保留创建人方便后续追溯。如果系统已经运行一段时间库里堆积了owner为0的客户需要回填。回填前先确认这些客户是由谁创建SELECT created_by, COUNT(*) AS cnt FROM erp_customer WHERE customer_owner 0 OR customer_owner IS NULL GROUP BY created_by;如果created_by也没有值那只能根据操作日志或人工判断归属了。确认规则后执行回填UPDATE erp_customer SET customer_owner created_by WHERE customer_owner 0 OR customer_owner IS NULL;回填后的下一步是修改列表查询过滤逻辑建议用双条件子账号只查owner等于自己的客户管理员不加该条件。不要直接删掉过滤条件否则子账号的客户数据会互相串比看不到更麻烦。4.3 子账号和角色授权的最佳实践创建子账号时建议按岗位角色分配而不是一个账号一个权限。可以先用系统初始化的管理员账号登录在系统设置-账户管理里创建“销售员”“仓库文员”“财务”三个角色角色绑菜单和操作权限再在员工管理里新增子账号并绑定角色。销售员角色的数据权限设为“仅本人客户”仓库文员角色则不需要看金额字段财务角色不保留单据审核权限避免采购员既录单又审核的情况。下面是常见的角色权限参考你可以按公司实际流程调整角色客户管理采购管理销货管理仓库盘点收款付款报表查看销售员仅本人可查看新增编辑不可不可可看毛利仓库文员不可不可出库确认可操作不可可看库存财务只读只读只读不可审核全部4.4 合同报价单打印模板标签和美工样式踩坑点新版源码强调合同报价单打印这个功能通常在单据详情页点“打印”按钮调用浏览器的打印功能。打印模板里是服务端渲染的HTML常见的标签替换类似于table classprint-table trth序号/thth商品编码/thth商品名称/thth数量/thth单价/thth金额/th/tr {loop $items $key $item} tr td{$key1}/td td{$item.product_code}/td td{$item.product_name}/td td{$item.qty}/td td{$item.price}/td td{$item.amount}/td /tr {/loop} /table{$item.product_code}是模板引擎的变量占位符循环标签{loop $items $key $item}会遍历商品明细数组。打印时最怕把整个HTML页面里导航栏都打印出来解决方法是单独给打印区域加media print样式常见的写法是media print { body * { visibility: hidden; } #printArea, #printArea * { visibility: visible; } #printArea { position: absolute; left: 0; top: 0; width: 100%; } }这段样式先把body下所有元素隐藏再单独显示#printArea及其子元素。注意这里不能用display:none来隐藏其他元素因为display:none会被继承影响打印区域。打印样式加上page指令可以控制纸张大小和页边距报价单一般用A4纸需要在一页里放下的表细节数有限明细超过20行时建议在模板里控制字号。4.5 报表统计成本ERP数据跑不通的常见排查采购报表和资金报表对不上通常是过滤条件不一致造成的。采购报表里用LEFT JOIN保留无供应商数据如果汇总出来的total_amount为NULL多半是明细表和主表关联字段写错或单据状态没有设置成已审核。下面是一条典型的按月汇总采购金额的SQLSELECT s.supplier_name, DATE_FORMAT(h.biz_date, %Y-%m) AS month, COALESCE(SUM(i.qty * i.price), 0) AS total_amount FROM erp_purchase_head h LEFT JOIN erp_purchase_item i ON h.id i.head_id LEFT JOIN erp_supplier s ON h.supplier_id s.id WHERE h.status 1 GROUP BY s.supplier_name, DATE_FORMAT(h.biz_date, %Y-%m);COALESCE函数把NULL转为0便于前端展示避免出现没有采购金额的供应商记录。汇总结果如果和业务对不上第一件事检查erp_purchase_head里status字段是否把草稿单也置为1第二件事看有没有直接改过erp_stock表但没有生成对应单据。这个排查思路也适用于成本ERP数据跑不通的情况先在源单和流水之间找断点而不是去改报表SQL。5. 进阶排错库存负数、PHP兼容性修复与定时备份5.1 库存出现负数怎么快速定位库存出现负数大多数不是系统算错了而是历史数据本身存在不平记录或某个调拨单在旧逻辑下绕过了库存拦截。排查时先找出该商品最近每一笔库存变化流水按时间顺序检查是什么单据导致库存穿过零点SELECT * FROM erp_stock_log WHERE product_id 101 ORDER BY log_id ASC;erp_stock_log表如果源码没有就去查看单据明细表里的出入库记录重点看审核时间、仓库ID和单据类型。一般逻辑是找到负数发生前后两笔流水如果前一条流水是手动UPDATE造成的说明有人直接改了库存表如果前一条是调拨单就检查调拨单有没有完整走事务。应急时可以用一条盘点差异单把负数调整回零不要直接UPDATE库存字段。5.2 PHP 7.4兼容性修复mysql_*函数迁移到PDO老版本源码常出现Fatal error: Uncaught Error: Call to undefined function mysql_connect()这是PHP 7.4移除mysql_*扩展导致的。正确做法是把数据库操作全部迁移到PDO或mysqli不用mysql_query。如果源码里数据库封装类很集中替换成本低很多。下面是一个最小化的PDO查询封装function db_query($sql, $params []) { static $pdo null; if ($pdo null) { $pdo new PDO( mysql:host127.0.0.1;dbnameerp_cloud;charsetutf8mb4, erp_user, 你的密码, [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); } $stmt $pdo-prepare($sql); $stmt-execute($params); return $stmt-fetchAll(PDO::FETCH_ASSOC); }这里的PDO::ERRMODE_EXCEPTION会让SQL报错直接抛异常方便定位问题。参数绑定用$params数组避免直接拼SQL导致注入。替换完mysql_query后还要把原来的$rs mysql_fetch_array($result)改成foreach (db_query(...) as $row)的写法很多老代码的循环体要跟着改。PHP错误处理上建议在入口文件里加上set_exception_handler把未捕获异常写入运行时日志这样上线后遇到白屏不用再翻Nginx错误日志。5.3 定时备份mysqldump加--single-transaction进销存数据直接关系到钱必须做定时备份。备份命令要加--single-transaction这个参数在InnoDB下启用一致性快照备份过程中不会锁表业务继续写也不会导致备份失败。错误示例是用--lock-tables在凌晨跑批时会把正在写库的表锁住导致前台卡单。mysqldump -uerp_user -p密码 \ --default-character-setutf8mb4 \ --single-transaction \ --quick \ erp_cloud /backup/erp_$(date %Y%m%d_%H%M).sql命令里--quick让逐行输出而不是一次性载入内存--default-character-setutf8mb4确保备份文件的字符集和库一致恢复时不容易乱码。把这段命令写进crontab每天凌晨2点执行再结合宝塔面板的数据库备份功能保留最近7天版本。恢复时先创建空库再执行mysql -uerp_user -p密码 erp_cloud 备份文件.sql恢复后记得进入系统检查库存总数和报表数据是否和备份前一致。对于这套PHP云进销存源码改完上面几个问题点后再用管理员身份走一遍采购入库、销售出库、调拨、盘点四个核心流程基本就能验证新旧版本的差异是否真正修到位了。本文还有配套的精品资源点击获取
返回列表