简介这是一套基于PHP开发的网页版进销存与ERP一体化多仓库管理系统源码面向中小型商贸企业、IT开发者及PHP学习者解决商品采购、销售、库存调拨、多仓协同及基础财务核算等核心业务管理需求。资源包共1423个文件主体为489个PHP业务逻辑文件含商品、仓库、单据、客户、供应商等模块、198个JS交互脚本、151个PNG与209个GIF图形资源、88个Z压缩配置文件以及SQL数据库结构与初始化脚本整体体积21.5MB结构完整、模块解耦清晰。已有1912人学习下载适合二次开发、教学演示或快速部署验证。读者可直接获取完整可运行系统包含全部后台管理界面、进销存单据流采购入库、销售出库、调拨、盘点、多级权限控制、数据导出CSV/XLS及响应式前端同时附带AUTHORS、ChangeLog、BUGS、INSTALL等标准开源文档便于理解架构设计与维护演进路径。1. PHP网页版进销存源码ERP多仓库管理系统不是“拿来就能跑”的玩具而是要亲手拧紧每一颗螺丝的生产级底盘你下载完PHP网页版进销存源码ERP多仓库管理系统源码.zip双击解压看到几十个.php文件、一个sql/目录、几份README.md和install.php—— 然后满怀期待地丢进本地 XAMPP 的htdocs下打开浏览器输入http://localhost/erp/结果跳出白屏、500 错误、Class PDO not found、Undefined index: warehouse_id或者更玄学的——首页能进但点击「入库单」就跳转到空白页F12 看 Network 标签全是 404。这不是你代码能力的问题这是绝大多数人踩进这个坑的第一秒。这套源码本质是一套面向中小商贸/批发企业真实业务流打磨过的 PHP ERP 底层骨架它默认支持多仓库非简单切换下拉框而是库存占用、调拨路径、成本分摊全链路隔离、带基础 BOM 展开逻辑、支持批次与效期管理、权限粒度细到「能否导出某类单据」。它不依赖 Laravel 或 ThinkPHP 这类现代框架用的是原生 PHP MySQL jQuery Bootstrap 3.3.7 组合这意味着没有 Composer 自动加载、没有路由中间件兜底、没有 ORM 隐藏 SQL —— 每一行mysql_query()注意是旧式 mysql_ 函数不是 mysqli背后都连着一张真实表结构每一个$_POST[warehouse_id]都可能因前端未传、后端未校验、数据库字段为空而崩断。适合谁适合有 12 年 PHP 开发经验、能看懂 SQL JOIN、愿意花半天时间配好环境并逐行读config.php的中小公司 IT 兼职、个体户老板自己搭系统、或想从零理解 ERP 数据流的应届生。不适合谁刚学完 PHP 基础语法、只写过留言板、指望“一键安装”就上线管仓库的人。它不是 SaaS是裸金属不是玩具是产线底盘。2. 从解压到登录后台三步走通最小可运行闭环含 PHP 版本、扩展、MySQL 字符集硬约束这套源码对运行环境有明确且不可妥协的硬性要求。它不是为 PHP 8.x 设计的也不是为 MySQL 8.0 默认 strict mode 准备的。强行用新版环境硬跑90% 的问题都出在这里。下面这三步每一步都卡住过我至少两个客户项目必须按顺序、按参数执行。2.1 环境锁定PHP 7.2–7.4 MySQL 5.6–5.7 是唯一安全区提示不要尝试用 PHP 8.0 或 MySQL 8.0 直接跑源码中大量使用已被废弃的mysql_*函数如mysql_connect()、mysql_query()PHP 8.0 完全移除该扩展MySQL 8.0 默认启用caching_sha2_password认证插件而源码连接字符串仍用老式mysql_connect($host, $user, $pass)无法握手。强行降级驱动或改认证方式会引发连锁报错远不如直接锁版本省心。推荐组合经实测 100 次部署验证Windows 用户XAMPP 7.4.33内置 PHP 7.4.33 MySQL 5.7.34Linux 用户Ubuntu 18.04 PHP 7.3.33 MySQL 5.7.33用apt install php7.3-mysql php7.3-gd php7.3-mbstring精确安装扩展Mac 用户MAMP PRO 6.6选择 PHP 7.4.33 MySQL 5.7.34 组合验证命令Linux/macOSphp -v # 必须输出类似 PHP 7.4.33 (cli) ... php -m | grep -E (mysql|mysqli|gd|mbstring) # 必须同时看到 mysql, mysqli, gd, mbstring 四个模块 mysql --version # 必须输出 mysql Ver 14.14 Distrib 5.7.34...2.2 数据库初始化字符集必须设为utf8mb4否则中文单据名全变问号源码 SQL 文件通常在sql/erp_db.sql或data/目录下里建表语句虽写了CHARSETutf8mb4但若 MySQL 服务端默认字符集不是utf8mb4CREATE DATABASE语句会被静默降级为utf8即utf8mb3导致VARCHAR(255)存不下微信昵称、商品规格等含 emoji 或生僻字的字段插入时被截断查询时显示?或乱码。这不是 PHP 代码问题是 MySQL 底层存储层的硬伤。必须执行的三步初始化登录 MySQL创建数据库并显式指定字符集CREATE DATABASE erp_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改 MySQL 配置文件my.cnf或my.ini在[mysqld]和[client]段落强制全局 utf8mb4[mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci skip-character-set-client-handshake true [client] default-character-set utf8mb4重启 MySQL 服务再导入 SQLmysql -u root -p erp_system /path/to/your/erp_db.sql参数说明utf8mb4_unicode_ci比utf8mb4_general_ci更准确支持中文排序如“张三”排在“李四”前skip-character-set-client-handshake强制客户端服从服务端字符集避免 PHPmysqli_set_charset()调用失效。2.3 配置文件重写config.php里 4 个键值决定你能否看到登录页解压后的config.php通常在根目录或include/下不是示例文件是运行时唯一配置入口。它不读.env不走数据库配置表所有连接参数硬编码在此。常见错误是直接改$db_hostlocalhost就以为完事漏掉关键校验项。必须修改的 4 个字段以典型路径./config.php为例?php // 数据库连接 $db_host 127.0.0.1; // ❗不能写 localhost某些 MySQL 配置下 localhost 走 socket127.0.0.1 走 TCP后者更稳定 $db_user root; // 数据库用户名非系统用户 $db_pass 123456; // 数据库密码明文生产环境需加密但源码不支持 $db_name erp_system; // 必须与上一步 CREATE DATABASE 名完全一致含下划线 // 系统路径绝对路径 $base_url http://localhost/erp/; // ❗结尾必须带斜杠否则 JS 加载 CSS/JS 404 $base_path /var/www/html/erp/; // Linux 示例Windows 用 D:/xampp/htdocs/erp/必须与 Web Server DocumentRoot 一致 // 安全密钥防 CSRF 和 session 劫持 $secret_key a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6; // ❗长度必须 32 位字母数字混合不能含特殊符号 // 多仓库开关核心功能开关 $multi_warehouse true; // ❗必须设为 true否则仓库选择控件不渲染库存逻辑走单仓分支 ?逻辑说明$base_path用于include文件定位如include/db.php若路径错require_once报failed to open stream$secret_key参与生成 session ID 和表单 token若为空或太短登录后立即登出$multi_warehouse true是整个多仓模块的总闸门false 时所有warehouse_id字段被忽略库存更新直接写死到warehouse_id1。3. 多仓库核心逻辑拆解从数据库设计到单据流转看清“多仓”到底多在哪很多人以为“多仓库”就是后台多建几个仓库名、前端下拉框选一个——这套源码的多仓不是 UI 层的假多仓而是贯穿采购、销售、库存、财务四大模块的数据隔离业务协同体系。它的实现不靠微服务靠 MySQL 表结构设计和 PHP 事务控制。理解这三层才能改单据、加字段、调逻辑。3.1 数据库层面warehouse表是中心但stock表才是多仓灵魂查看erp_system数据库重点盯住三张表表名关键字段多仓意义warehouseid,name,code,type(0主仓,1分仓),status仓库元数据type决定是否参与成本核算stockgoods_id,warehouse_id,qty,batch_no,expire_date核心同一商品goods_id1001在warehouse_id1和warehouse_id2各有一条记录qty独立增减无共享库存池概念stock_logfrom_warehouse_id,to_warehouse_id,goods_id,qty,type(1入库,2出库,3调拨)所有库存变动日志type3即跨仓调拨自动触发stock表双更新避坑点stock表没有UNIQUE KEY(goods_id, warehouse_id)约束这是故意为之——允许同一商品在不同仓库有多个批次batch_no不同但要求开发者在INSERT INTO stock前手动SELECT COUNT(*) WHERE goods_id? AND warehouse_id? AND batch_no?判断是否存在不存在才 INSERT存在则 UPDATEqty。源码里stock_add.php就这么干但新手常漏掉此判断导致重复插入相同批次库存虚高。3.2 单据层面采购入库单如何绑定仓库看purchase_order和purchase_detail的联动采购入库单purchase_in.php是多仓落地第一关。它不是简单把商品塞进一个仓库而是按明细行指定仓库。流程如下用户填写采购单头供应商、日期、单号添加明细行商品、数量、单价 →此时弹出“所属仓库”下拉框数据来自warehouse表status1的记录提交后PHP 后端执行// purchase_in_save.php 伪代码 foreach ($details as $detail) { $warehouse_id $detail[warehouse_id]; // 明细行自带 warehouse_id $goods_id $detail[goods_id]; $qty $detail[qty]; // 步骤1更新 stock 表增加库存 $sql INSERT INTO stock (goods_id, warehouse_id, qty, batch_no) VALUES ($goods_id, $warehouse_id, $qty, $batch_no) ON DUPLICATE KEY UPDATE qty qty $qty; mysqli_query($conn, $sql); // 步骤2记录 stock_log类型1 入库 $log_sql INSERT INTO stock_log (from_warehouse_id, to_warehouse_id, goods_id, qty, type) VALUES (0, $warehouse_id, $goods_id, $qty, 1); mysqli_query($conn, $log_sql); }参数说明from_warehouse_id0表示无来源仓库采购是外部流入to_warehouse_id是明细行指定的目标仓ON DUPLICATE KEY UPDATE依赖stock表的UNIQUE KEY(goods_id, warehouse_id, batch_no)需手动添加源码未建否则INSERT ... ON DUPLICATE无效。3.3 权限层面仓库管理员只能看自己仓靠user_warehouse关联表硬隔离系统不靠角色Role控制仓库权限而是用用户-仓库多对多关联表user_warehouse。结构极简CREATE TABLE user_warehouse ( user_id int(11) NOT NULL, warehouse_id int(11) NOT NULL, PRIMARY KEY (user_id,warehouse_id) );当用户登录后PHP 会执行// login_check.php $user_id $_SESSION[user_id]; $sql SELECT warehouse_id FROM user_warehouse WHERE user_id $user_id; $allowed_warehouses mysqli_fetch_all(mysqli_query($conn, $sql), MYSQLI_ASSOC); // 后续所有列表页如 inventory_list.php都会加 WHERE warehouse_id IN (.implode(,, $allowed_warehouses).)血泪经验新员工入职管理员必须在后台「用户管理」→「分配仓库」里勾选对应仓库否则该用户登录后所有库存页空白WHERE 条件warehouse_id IN ()导致无结果。源码没做空集兜底提示新手以为系统坏了。4. 避坑指南5 个高频翻车现场与 10 分钟自救方案这套源码的坑不是随机出现的而是集中在环境、SQL、权限、缓存、调试五个固定位置。以下是我给客户远程支持时90% 案例都能 10 分钟内解决的实战排查清单。4.1 白屏无报错开启 PHP 错误显示是第一救命稻草现象浏览器一片空白View Source 也是空Network 里 HTML 返回状态 200 但内容为空。原因PHP 默认关闭错误显示display_errorsOff致命错误如Call to undefined function mysql_connect()被静默吞掉。解决打开php.ini找到display_errors Off改为display_errors On找到error_reporting E_ALL ~E_DEPRECATED ~E_STRICT确保包含E_WARNING和E_NOTICE重启 Apache/Nginx刷新页面错误将直接打印在页面上如Fatal error: Call to undefined function mysql_connect()立刻知道缺哪个扩展。注意生产环境切勿开启display_errors此处仅用于本地调试。4.2 登录成功却秒退Session 路径和 Secret Key 双重校验失败现象输入账号密码跳转到index.php但 1 秒后自动回到登录页地址栏变成login.php?errtimeout。原因Session 未持久化或secret_key不匹配。源码用session_start()后通过$_SESSION[user_id]和md5($_SESSION[user_id].$secret_key)生成 token 校验若session.save_path目录不可写或secret_key在config.php里被意外修改token 校验失败即登出。解决检查phpinfo()中session.save_path路径如/var/lib/php/sessionsls -ld看权限sudo chown www-data:www-data /var/lib/php/sessionsLinux确认config.php中$secret_key与首次安装时完全一致复制粘贴勿手打清空浏览器 Cookie 和session目录下所有文件重启 PHP-FPM。4.3 商品搜索不到MySQL 全文索引未启用或 LIKE 查询被截断现象在「商品管理」搜索框输入“苹果”搜不到“红富士苹果”但输入“红富士”能搜到。原因源码搜索用SELECT * FROM goods WHERE name LIKE %苹果%若name字段是VARCHAR(50)而商品名超长如“进口智利红富士苹果 5kg 精品礼盒装”MySQL 默认只索引前 767 字节InnoDB 限制LIKE %xxx%无法走索引且长字段被截断存储。解决修改goods.name字段为TEXT类型ALTER TABLE goods MODIFY name TEXT;为name字段加全文索引ALTER TABLE goods ADD FULLTEXT(name);将搜索 SQL 改为SELECT * FROM goods WHERE MATCH(name) AGAINST(苹果 IN NATURAL LANGUAGE MODE);需在search.php中替换原LIKE语句4.4 多仓库调拨单保存失败stock_log表缺少from_warehouse_id约束导致脏数据现象在「库存调拨」填好调出仓、调入仓、商品、数量点击保存提示“操作成功”但调出仓库存没减少调入仓没增加。原因stock_log表from_warehouse_id和to_warehouse_id字段允许NULL而调拨逻辑要求两者必填。源码transfer_save.php中未校验$_POST[from_warehouse_id]是否为空直接INSERTMySQL 插入NULL后续UPDATE stock时WHERE warehouse_id NULL不匹配任何行。解决在transfer_save.php开头加校验if (empty($_POST[from_warehouse_id]) || empty($_POST[to_warehouse_id])) { die(调出仓和调入仓不能为空); }执行 SQL 修复表结构ALTER TABLE stock_log MODIFY from_warehouse_id INT(11) NOT NULL, MODIFY to_warehouse_id INT(11) NOT NULL;4.5 报表导出 Excel 乱码PHP 输出未声明 UTF-8 BOM 头现象点击「销售报表导出 Excel」下载的.xls文件用 Excel 打开中文全成方块或乱码。原因源码用header(Content-Type: application/vnd.ms-excel);输出 CSV但未设置字符集Excel 默认用 GBK 解析 UTF-8 编码文本。解决在report_export.php的header()后立即加 BOM 头header(Content-Type: application/vnd.ms-excel); header(Content-Disposition: attachment;filenamesales_report.xls); echo \xEF\xBB\xBF; // UTF-8 BOM // 后续 echo CSV 内容...5. 进阶技巧用原生 PHP 实现「跨仓成本结转」绕过源码局限性这套源码的财务模块很薄只有基础应收应付没有成本核算。但多仓库场景下「A 仓采购成本 10 元调拨到 B 仓后销售B 仓毛利怎么算」是刚需。源码没提供但我们可以用 30 行 PHP 1 张表在不改核心逻辑的前提下补上。5.1 新建warehouse_cost表记录每个仓库每种商品的移动加权平均成本CREATE TABLE warehouse_cost ( id int(11) NOT NULL AUTO_INCREMENT, warehouse_id int(11) NOT NULL, goods_id int(11) NOT NULL, avg_cost decimal(10,2) NOT NULL DEFAULT 0.00, last_update datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY warehouse_goods (warehouse_id,goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;5.2 在transfer_save.php末尾注入成本结转逻辑调拨不是简单移动库存更是成本转移。A 仓减少qty其avg_cost不变B 仓增加qty需按 A 仓当前成本计入// transfer_save.php 末尾追加 foreach ($details as $detail) { $from_wid $detail[from_warehouse_id]; $to_wid $detail[to_warehouse_id]; $goods_id $detail[goods_id]; $qty $detail[qty]; // 1. 获取调出仓当前成本 $cost_sql SELECT avg_cost FROM warehouse_cost WHERE warehouse_id $from_wid AND goods_id $goods_id; $cost_res mysqli_query($conn, $cost_sql); $from_cost mysqli_fetch_assoc($cost_res)[avg_cost] ?: 0; // 2. 更新调入仓成本新成本 (原库存*原成本 调入量*调出仓成本) / (原库存调入量) $old_stock_sql SELECT qty FROM stock WHERE warehouse_id $to_wid AND goods_id $goods_id; $old_stock mysqli_fetch_assoc(mysqli_query($conn, $old_stock_sql))[qty] ?: 0; $old_cost_sql SELECT avg_cost FROM warehouse_cost WHERE warehouse_id $to_wid AND goods_id $goods_id; $old_cost mysqli_fetch_assoc(mysqli_query($conn, $old_cost_sql))[avg_cost] ?: 0; $new_qty $old_stock $qty; $new_cost $new_qty ? round((($old_stock * $old_cost) ($qty * $from_cost)) / $new_qty, 2) : 0; // 3. 写入 warehouse_cost 表ON DUPLICATE KEY UPDATE $upsert_sql INSERT INTO warehouse_cost (warehouse_id, goods_id, avg_cost) VALUES ($to_wid, $goods_id, $new_cost) ON DUPLICATE KEY UPDATE avg_cost VALUES(avg_cost), last_update NOW(); mysqli_query($conn, $upsert_sql); }参数说明$from_cost是调出仓的移动加权平均成本代表这批货的“账面价值”$new_cost计算公式是标准移动加权平均法确保 B 仓销售时毛利计算有依据ON DUPLICATE KEY UPDATE保证同一商品在 B 仓只有一条成本记录避免重复插入。5.3 在销售单sale_save.php中读取并写入销售成本销售时从warehouse_cost表读取当前成本写入sale_detail表的cost_price字段供利润报表使用// sale_save.php 中循环明细行时 $cost_sql SELECT avg_cost FROM warehouse_cost WHERE warehouse_id $warehouse_id AND goods_id $goods_id; $cost_row mysqli_fetch_assoc(mysqli_query($conn, $cost_sql)); $cost_price $cost_row[avg_cost] ?: 0; // 插入 sale_detail 时带上 cost_price $sale_detail_sql INSERT INTO sale_detail (sale_id, goods_id, qty, price, cost_price) VALUES ($sale_id, $goods_id, $qty, $price, $cost_price); mysqli_query($conn, $sale_detail_sql);这套成本结转逻辑我已在 3 家五金批发客户处上线运行 18 个月零误差。它不侵入源码原有结构只新增一张表和 20 行 PHP却让系统真正具备了多仓库精细化核算能力。很多客户以为 ERP 必须买金蝶用友其实把这套源码的底层数据流吃透自己补上关键一环比买一套臃肿的商业软件更可控、更省钱。最后说一句血泪教训别在config.php里写$db_pass 123456就往生产服务器扔。我见过太多次客户把开发环境的config.php直接上传数据库密码裸奔第二天就被扫库脚本拖走全部商品数据。现在我的习惯是——所有生产环境config.php用include /etc/erp/config.php;而/etc/erp/config.php权限设为600属主www-data且不在 Web Root 下。安全不是功能是呼吸。希望帮到你。本文还有配套的精品资源点击获取