简介这是一套基于PHPMySQL的网页版进销存ERP多仓库管理系统源码面向需要快速搭建或二次开发中小型进销存业务的开发者可覆盖多仓库商品管理、采购销售记录、库存流水、单据及后台权限等常见管理需求。压缩包共1423个文件约20.67MB其中包含489个PHP文件、198个JS脚本、42个CSS样式、151张PNG图片及SQL数据库文件等代码结构与前端资源划分清晰便于按模块查看和扩展。配套提供vikeji.sql数据库文件及安装配置说明修改database.php并导入数据后即可使用默认后台账号admin/admin888登录体验系统中涉及商品分类、仓库、单位、往来单位、单据等多种数据表适合作为PHP实战项目、毕业设计或企业进销存系统的改造基础。目前已有216人学习下载可供正在选型或研究轻量级Web进销存方案的开发者参考。1. 为什么多仓库进销存值得用 PHP 源码改而不是买 SaaS拿到这套 PHP 网页版进销存 ERP 源码时真正的门槛不在功能而在部署入口很多人上传到网站根目录、导入vikeji.sql、改了/application/config/database.php却被后台验证码卡住。究其原因基于 CodeIgniter 3 的源码对 PHP 版本、字符集和 session 保存目录都很敏感一个 UTF-8 BOM 就能让验证码图片直接黑屏。这类源码适合维护两三个仓库、又不想被 SaaS 按月收租的团队。仓库、SKU、库存流水之间只要关联清楚采购入库、销售出库、多仓调拨、盘点差异本质都只是在流水表里写入不同类型的单据。下面按部署、表结构、登录态、对账验证四个方向拆。2. CodeIgniter 3 目录结构与 database.php 多环境配置2.1 上传前先看入口文件PHP 版本决定能不能跑先确认 PHP 版本。CI3 在 PHP 5.6 到 PHP 7.4 上最稳如果服务器是 PHP 8.1 以上each()、mysql_real_escape_string这类老函数会直接报错。可以用php -v查看然后解压上传。常见路径是/var/www/html上传后需要保证 Web 用户对application、system目录有写权限。unzip vikeji-multi-warehouse.zip -d /var/www/html cd /var/www/html chown -R www-data:www-data application/ system/ static/ chmod -R 755 application/ system/这里www-data是 Nginx/Apache 的默认运行用户。如果你这台机器的 Web 用户是nginx或daemon要对应替换否则后面上传商品、写缓存、生成验证码时都会出现权限类报错而且报错信息经常被框架吞掉只留下一个空白页面。上传后的目录结构大致如下路径作用/index.phpCodeIgniter 3 唯一入口所有请求经它路由/application/config/database.php数据库连接参数部署时第一个要改的文件/application/config/config.php项目 base_url、session 等全局配置/application/controllers/admin、入库、出库、调拨等业务控制器/application/views/PHP 模板页面/vikeji.sql数据库备份文件需要导入 MySQL如果上传到子目录例如http://域名/erp/还要在application/config/config.php里把base_url改成http://域名/erp/否则页面里的 CSS、JS、登录跳转都会找错路径。2.2 database.php 关键参数与 pconnect 陷阱打开/application/config/database.php找到$db[default]数组。这套进销存源码是 CI3 固定格式所有连接参数集中在这个数组里。部署时我一般改成下面这样$db[default] array( dsn , hostname 127.0.0.1, username vikeji_user, password YourPassword, database vikeji, dbdriver mysqli, dbprefix v_, pconnect FALSE, db_debug (ENVIRONMENT ! production), cache_on FALSE, char_set utf8mb4, dbcollat utf8mb4_general_ci, );hostname如果 MySQL 和 Web 同机填127.0.0.1而不是localhost能减少 PHP 走 socket 时的权限问题。dbprefix常见是v_不要乱改除非你连 SQL 文件里的表名一起改。pconnect在 PHP-FPM 场景下连接长期不释放MySQL 的wait_timeout把连接断开后业务请求可能直接报MySQL server has gone away生产环境建议保持FALSE。char_set用utf8mb4而不是utf8因为老程序遇到 emoji 或生僻字库存名称时utf8会报字符集长度不够。常用参数选择参数建议值说明hostname127.0.0.1 或内网 IPWeb 必须能访问到 MySQLdbdrivermysqliCI3 不推荐再使用老 mysql 驱动dbprefixv_与 SQL 导入后的表前缀保持一致pconnectFALSE避免连接泄漏和断连char_setutf8mb4与导入的库字符集对齐改完配置先跑一句php -l application/config/database.php确认没有语法错误再进浏览器能省掉很多“页面白屏”的排查时间。2.3 文件编码与目录权限这里埋着第一个部署坑摘要里特别提醒“不要用记事本修改”。Windows 记事本保存 UTF-8 文件时会在头部写入三个字节的 BOMEF BB BF。PHP 执行该文件时这三个字节会被当作输出内容发送给浏览器。CodeIgniter 的控制器一旦调用header()或输出验证码图片都会被这三个字节污染最终表现就是后台打开白屏或验证码不显示。推荐用 VS Code 或 Notepad保存时选择“UTF-8 无 BOM”。如果手头文件已经被改过可以这样检查head -c 3 application/config/database.php | xxd输出若是ef bb bf就说明带 BOM需要用下一章的命令清理。这一步做完后再去看数据库导入否则后面登录验证码通不过你会误以为是 PHP 没装 GD 扩展。3. 导入 vikeji.sql 后看清多仓库库存表如何 JOIN3.1 用命令行导入比 phpMyAdmin 更可控phpMyAdmin 导入小 SQL 没问题但vikeji.sql里一旦有触发器、存储过程或大批量 INSERT浏览器上传会超时或触发max_allowed_packet。我更倾向于命令行导入mysql -u root -p -e CREATE DATABASE IF NOT EXISTS vikeji DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p vikeji vikeji.sql mysql -u root -p vikeji -e SHOW TABLES;第一句创建字符集为utf8mb4的库字符集必须和database.php里的char_set对齐否则后续 JOIN 查询时排序规则冲突。第二句导入表结构和数据第三句确认表是否进来。如果执行中间报max_allowed_packet需要在my.cnf的[mysqld]段把值调大到64M重启 MySQL 后再导入。图形界面操作也一样先新建数据库选择utf8mb4_general_ci再在 Import 页选择vikeji.sql。注意不要勾选“创建数据库”选项避免 SQL 文件里的创建语句把库建到错误位置。3.2 多仓库存的模型通常是四张表联动这类进销存源码的关键不是把库存存在一张大宽表里而是拆成四张表。我以最常见的v_前缀为例表名如果与你源码里不一致先用SHOW TABLES对照再替换表名职责关键字段v_warehouse仓库主数据id, name, addressv_product商品/SKU 主数据id, sku, name, cost_price, sale_pricev_stock当前库存结存id, warehouse_id, product_id, quantityv_stock_movement库存流水id, warehouse_id, product_id, change_type, quantity, before_qty, after_qtyv_stock是“结果表”每多一个仓库和商品的组合就多一行。v_stock_movement是“事实表”每一次采购入库、销售出库、调拨、盘点都写一行并记录变动前后的数量。只要流水完整v_stock里的结存可以通过对流水SUM重新推算出来这也是后面做库存对账的基础。如果源码里没有给v_stock加唯一索引我一般会补一个ALTER TABLE v_stock ADD UNIQUE KEY uk_warehouse_product (warehouse_id, product_id);这个索引防止同一个仓库里出现两行相同商品。没有它程序 bug 或手工改库时重复插入库存汇总会瞬间翻倍。加了之后库存更新逻辑可以改成INSERT ... ON DUPLICATE KEY UPDATE quantity VALUES(quantity)保证同一个 SKU 在一个仓库只有一行记录。3.3 按仓库查库存的查询骨架多仓系统最常见的动作是选一个仓库看到该仓所有商品数量。SQL 骨架如下SELECT w.name AS warehouse_name, p.sku, p.name AS product_name, s.quantity, COALESCE(p.cost_price, 0) AS cost_price FROM v_stock s INNER JOIN v_warehouse w ON s.warehouse_id w.id INNER JOIN v_product p ON s.product_id p.id WHERE s.warehouse_id 2 ORDER BY w.id, p.sku;INNER JOIN会把不存在仓库或商品信息的库存记录过滤掉。如果查询结果比后台界面少了几行说明v_stock里存在孤儿数据需要检查v_warehouse和v_product是否有记录被人删除过。COALESCE(p.cost_price, 0)是为了让成本价为空时也能显示 0避免在 PHP 层再判空。初始化库存时我一般不走直接改v_stock.quantity的老路而是往流水表插入一条期初记录INSERT INTO v_stock_movement (warehouse_id, product_id, change_type, quantity, before_qty, after_qty, created_at) VALUES (2, 101, opening, 100, 0, 100, NOW());before_qty0表示仓库里原本没有这批货after_qty100表示期初之后的结存。同时把v_stock的 quantity 同步为 100。这样做以后对账时期初数也是流水的一部分不会被漏掉。4. 验证码 Session、UTF-8 BOM 污染与登录态排错4.1 验证码写入 Session 的标准流程后台登录流程一般拆成三个动作打开登录页时输出验证码图片、表单提交时校验验证码、校验通过后建立 session。验证码控制器代码大致是public function captcha() { $code strtoupper((string) mt_rand(1000, 9999)); $this-session-set_userdata(login_captcha, $code); $im imagecreatetruecolor(100, 30); $bg imagecolorallocate($im, 245, 245, 245); $txt imagecolorallocate($im, 30, 30, 30); imagefilledrectangle($im, 0, 0, 100, 30, $bg); imagestring($im, 5, 15, 5, $code, $txt); header(Content-Type: image/png); imagepng($im); imagedestroy($im); exit; }mt_rand(1000, 9999)生成四位随机数转大写字符串写入 sessionheader(Content-Type: image/png)必须在任何输出之前调用imagepng($im)输出 PNG 二进制。如果 PHP 文件带 BOMheader()之前的空输出会导致这里直接报headers already sent。表单提交时后台比较 session 里的login_captcha和用户输入忽略大小写。默认账号 admin/admin888 是开源资源里公开的部署后要立刻改掉否则验证码挡不住任何人登录。4.2 BOM 字节如何破坏验证码图片输出当一个 PHP 文件以 UTF-8 BOM 保存时文件头部有三个不可见字节。PHP 解析时会把它当作纯文本输出于是请求验证码图片时图片二进制流前面多了EF BB BF浏览器无法识别为 PNG表现就是图片裂开或完全空白。注意这不代表 GD 库没装也不代表 PHP 版本太新。定位方法很简单head -c 3 application/config/database.php | xxd head -c 3 application/controllers/admin.php | xxd只要输出包含ef bb bf都要清理。常用的一次性处理命令sed -i 1s/^\xEF\xBB\xBF// application/config/database.php执行后再跑php -l检查语法。为了避免反复踩坑开发机上统一用 VS Code右下角把编码切到“UTF-8”并关闭自动添加 BOM 的选项。最简单的验证方式是在浏览器里直接访问验证码 URL如果页面源码第一个字符是而不是乱码说明 BOM 问题已解决。4.3 Session 文件权限与多仓库可见范围CI3 默认用文件保存 session保存路径由 PHP 的session.save_path决定。如果登录后马上跳回登录页八成是 session 保存目录不可写。先用命令确认php -r echo session_save_path(), PHP_EOL;再把该目录权限放开给 Web 用户。也可以在application/config/config.php里直接指定到项目内$config[sess_driver] files; $config[sess_save_path] FCPATH . application/cache/sessions/; $config[sess_expiration] 7200;指定到项目内部目录避免系统临时目录权限不稳定的问题。sess_expiration控制登录态存活时间办公室内勤给 7200 秒仓库扫码终端给 14400 秒太长则存在账号被他人使用的风险。多仓系统里登录后通常会把用户能看到的仓库 ID 写入 session。例如只负责华南仓的账号库存列表应只显示华南仓数据$this-db-where(warehouse_id, $this-session-userdata(warehouse_id)); $list $this-db-get(v_stock)-result();这里的 session 变量名取决于源码常见是warehouse_id或wh_id。如果要支持“一个用户看多个仓库”可以把 session 里的标量改成数组再用where_in查询不要拼一串OR条件。常见登录问题现象最常见原因定位手段验证码图片不显示PHP 文件带 BOM / GD 扩展未开xxd查头部、php -m查 gd登录成功立刻跳回登录页session 保存目录不可写查看session_save_path()权限白屏无任何输出database.php 语法错误php -l检查CSS 加载失败base_url 没改浏览器 F12 看静态资源域名提示改完任何 PHP 文件上传服务器前先执行php -l。这一步能拦截八成的语法和白屏问题。5. 用流水表反向校验多仓库存二十分钟替代手工对账多仓系统最容易出错的是跨仓库调拨时只更新了目标仓、忘了减源仓。月底手工对账慢更靠谱的做法是先按流水表把入库类单据求和再减掉出库类单据得到“推导结存”最后和v_stock当前数量对比差异行就是要回查的单据。SELECT t.product_id, t.warehouse_id, t.derived_qty, COALESCE(s.quantity, 0) AS actual_qty, t.derived_qty - COALESCE(s.quantity, 0) AS diff_qty FROM ( SELECT product_id, warehouse_id, SUM(CASE WHEN change_type IN (opening, purchase_in, transfer_in, sale_return_in, adjust_in) THEN quantity ELSE 0 END) AS derived_qty FROM v_stock_movement GROUP BY product_id, warehouse_id ) t LEFT JOIN v_stock s ON t.product_id s.product_id AND t.warehouse_id s.warehouse_id HAVING diff_qty 0 ORDER BY diff_qty DESC;change_type是流水类型不同源码的英文命名可能不同先把后台单据类型枚举值拉出来对照再替换。COALESCE(s.quantity, 0)保证库存表缺失时也能输出负差。HAVING diff_qty 0只显示不平的行查询结果直接给出 SKU 和仓库。对完数量后还可以按月核对成本SELECT warehouse_id, DATE_FORMAT(created_at, %Y-%m) AS ym, SUM(CASE WHEN quantity 0 THEN quantity * cost_price ELSE 0 END) AS in_cost, SUM(CASE WHEN quantity 0 THEN -quantity * cost_price ELSE 0 END) AS out_cost FROM v_stock_movement m JOIN v_product p ON m.product_id p.id GROUP BY warehouse_id, ym ORDER BY warehouse_id, ym DESC;quantity 0表示入库侧quantity 0表示出库侧。用DATE_FORMAT(created_at, %Y-%m)把明细归到月月底看每个仓库的入库成本与出库成本是否和采购、销售报表一致。如果对不上再锁定某个月份查流水。把第一条校验 SQL 存成定时任务每天早上七点跑一次即可让库存差异当天暴露。同时调拨单的代码逻辑应包成事务同一次调拨要么同时更新源仓和目标仓流水要么一起回滚这是多仓进销存系统里最值得优先加固的代码位置。本文还有配套的精品资源点击获取