简介:一套可直接部署的轻量级仓库管理Web系统,后端用PHP编写,数据存储基于MySQL,覆盖商品信息维护、入库登记、出库开单、销售记录追踪、定期库存盘点及管理员分级权限管理。系统采用经典frame框架布局,通过left.php、top.php、main.php实现模块化页面结构,登录验证由verify.php配合session完成,退出操作由exit.php统一处理。附带ck.sql数据库脚本,导入即可初始化全部表结构;核心功能分模块存放——ckgl负责基础仓管逻辑,admin提供后台管理入口,product管理商品资料,out_product专用于出库单据处理,img目录存放图标与界面资源,conn集中配置数据库连接参数。所有PHP文件均内置基础路径防护和安全校验机制,适配中小型企业或个体仓储场景,支持快速上线与按需二次开发。
我用这套系统在三家小型五金批发商和两家社区生鲜仓配点实际跑过将近两年,从最初部署调试到后来加报表、改权限、接扫码枪,踩过的坑比写的代码还多。今天就把它掰开揉碎,把那些文档里不会写、但你上线第一天就会撞上的细节全掏出来——不是教你怎么复制粘贴,而是告诉你每一步背后“为什么这么设计”“不这么干会出什么问题”。
这套系统名字叫“CKGL”,是典型的中小仓储场景轻量级解决方案:不追求ERP那种复杂流程,也不搞微服务拆分,就是用最朴素的PHP+MySQL组合,把入库、出库、盘点、商品、权限这五根主梁搭稳,再用frame布局把界面钉死,让老板、仓管、销售员三类人各走各的门,各看各的屏。它没用任何现代框架(Laravel/ThinkPHP),所有逻辑都压在原生PHP里,好处是服务器只要支持PHP 7.2+、MySQL 5.7+就能跑,连宝塔面板都不用装;坏处是你得真懂$_SESSION怎么续命、include_once和require_once的区别、SQL注入在哪种拼接里最致命——这些不是考点,是每天登录失败三次后你必须翻源码查的现场。
关键词里“仓库系统”“PHP源码”“库存管理”“销售出库”“权限控制”五个词,每一个都对应一个生死线:
- “仓库系统”意味着它必须扛住“同一商品多批次入库、不同库位混存、临期预警缺失”这类真实业务压力;
- “PHP源码”不是指能运行就行,而是指你能一眼看懂verify.php里那行if (!isset($_SESSION['admin_id']))到底拦住了谁、放过了谁;
- “库存管理”的核心不在增删改查,而在“实时性”——出库单提交瞬间,product表里的stock_num必须原子性扣减,否则两人同时点单,库存就变负数;
- “销售出库”不是简单填个数量,它要联动生成销售记录、更新商品毛利字段、触发低库存告警,甚至预留了对接快递单号的钩子;
- “权限控制”更不是后台勾几个复选框,而是left.php里根据$_SESSION['role_level']动态渲染菜单、admin/目录下每个PHP文件开头都有if ($_SESSION['role_level'] < 3) die('无权访问');这种硬闸。
它适合谁?不是SaaS服务商,也不是IT部门健全的中型企业,而是:
- 个体户老板自己管仓,想甩掉Excel手工记账;
- 小型贸易公司刚起步,没预算买商用系统,但又怕用免费版被锁功能或导不出数据;
- 技术外包团队接单后需要一个干净底板,快速套壳改UI、加微信通知、对接硬件扫码器。
不适合谁?
- 需要WMS级波次拣货、RFID批量扫描、AGV调度接口的智能仓;
- 要求等保三级、审计日志留痕到毫秒、操作全程录像的国企或金融机构;
- PHP零基础、连phpinfo()都不会调的纯小白——这不是拖拽建站工具,你得愿意打开Notepad++逐行读conn/db_config.php里的mysqli_connect()参数。
下面我就按真实部署动线来拆解:从数据库初始化开始,到登录页第一眼看到什么,再到仓管员点“出库”按钮时后台发生了什么,最后落到你二次开发时最容易崩的三个雷区。所有内容,都是我在客户现场重启过十七次Apache、重刷过五次数据库、对着Chrome开发者工具Network标签页盯了八小时后总结出来的。
1. 系统整体架构与设计逻辑拆解
1.1 为什么坚持用Frame布局而非现代SPA?
看到frame.php、left.php、top.php、main.php这一套,很多新接触的人第一反应是:“这太老了吧?现在谁还用frameset?”——这话没错,但从仓储场景的实际需求出发,这个选择非常务实。
Frame布局的本质,是状态隔离 + 局部刷新 + 权限粒度控制。我们来对比两种典型操作:
- 场景A:仓管员在
left.php点击“出库单录入”,main.php加载out_product/add.php表单;他填完商品、数量、去向,点“提交”,页面局部刷新只重载main.php区域,left.php菜单栏和top.php顶部导航栏完全不动。这意味着: - 他不用重新登录(session未中断);
- 左侧菜单保持展开状态(比如“出库管理”节点仍高亮),避免反复找入口;
-
top.php里显示的当前用户姓名、角色、在线时长持续更新,无需JS轮询。 -
场景B:如果改成Vue单页应用,每次路由跳转都要重新拉取菜单权限、校验token、初始化全局状态。而中小仓储的电脑往往是五六年前的i3台式机,Chrome开三个tab就卡顿,JavaScript bundle一过500KB,首次加载白屏超过3秒,仓管员直接关网页去翻Excel。
更关键的是权限控制粒度。left.php不是静态HTML,它里面有一段核心逻辑:
<?php
$role = $_SESSION['role_level'];
$menu_items = [
['name' => '首页', 'url' => 'main.php?m=home'],
['name' => '商品管理', 'url' => 'main.php?m=product_list', 'level' => 2],
['name' => '入库登记', 'url' => 'main.php?m=ck_add', 'level' => 2],
['name' => '出库单据', 'url' => 'main.php?m=out_list', 'level' => 2],
['name' => '库存盘点', 'url' => 'main.php?m=ckpd_list', 'level' => 3],
['name' => '管理员设置', 'url' => 'main.php?m=admin_user', 'level' => 4]
];
foreach ($menu_items as $item) {
if ($item['level'] <= $role) {
echo "<li><a href='{$item['url']}'>{$item['name']}</a></li>";
}
}
?>
这段代码决定了:普通仓管(role_level=2)看不到“库存盘点”和“管理员设置”菜单项;而财务人员(role_level=3)能看到盘点但不能进管理员后台。这种控制发生在服务端模板渲染阶段,不是靠前端JS隐藏DOM元素——后者极易被F12绕过。Frame布局让这种服务端权限控制天然落地,因为left.php每次被iframe加载时都会重新执行这段PHP。
所以,它“老”,但老得有道理:不是技术债,而是针对低配置终端、高频操作、强权限隔离场景做的主动收敛。
1.2 权限模型为何采用role_level整数分级而非RBAC?
系统里所有权限判断都基于$_SESSION['role_level']这个整数变量,比如:
- role_level = 1:普通销售员(只能查商品、录销售出库单)
- role_level = 2:仓管员(可操作入库、出库、查看库存)
- role_level = 3:财务/主管(可做盘点、导出报表、审核单据)
- role_level = 4:超级管理员(可增删用户、改权限、看所有日志)
为什么不采用标准RBAC(基于角色的访问控制),比如定义“仓管角色”拥有“入库权限”“出库权限”,再把用户绑定到角色?原因很现实:降低维护成本,避免权限爆炸。
中小仓储的真实情况是:人员流动快、一人多岗普遍。上周还是销售员的小王,这周兼任仓管,老板一句话就要给他开通入库权限。如果走RBAC,你得:
1. 进入后台 → 角色管理 → 找到“仓管角色” → 编辑权限 → 勾选“入库单新增”;
2. 再进用户管理 → 找到小王 → 解绑原“销售角色” → 绑定新“仓管角色”。
而本系统只需一条SQL:
UPDATE admin_user SET role_level = 2 WHERE user_name = '小王';
或者直接在admin/user_edit.php里改个下拉框选项,提交即生效。没有缓存、没有中间层、没有角色继承关系需要同步——所有权限判断都在if ($role >= 2)这一行完成。
当然,这也带来代价:无法实现“销售员A可看A客户订单,销售员B可看B客户订单”这种数据级权限。但对目标场景而言,这是可接受的简化。系统设计的第一法则是:先解决80%的共性问题,再用插件或定制补剩下的20%。
1.3 数据库脚本ck.sql的设计哲学:宁可冗余,不可缺失
打开ck.sql,你会看到21张表,远超一般理解的“商品、库存、出入库”三张表。典型如:
product:商品主表(id, name, spec, unit, price_in, price_out, stock_num…)ck_record:入库记录(id, product_id, batch_no, in_num, in_date, operator_id…)out_record:出库记录(id, product_id, out_num, out_date, sale_order_no, customer_name…)ck_pd_record:盘点记录(id, product_id, before_num, after_num, diff_num, pd_date, pd_user…)admin_user:用户表(id, user_name, password_md5, role_level, last_login_time…)log_operate:操作日志(id, user_id, operate_type, content, ip_addr, create_time…)
初看会觉得“一张record表加个type字段不就行了?干嘛拆成三张?”——这是为查询性能与业务语义清晰度做的刻意冗余。
举个例子:仓管员每天要查“昨天所有出库单”,SQL是:
SELECT o.id, p.name, o.out_num, o.out_date, o.customer_name
FROM out_record o
JOIN product p ON o.product_id = p.id
WHERE DATE(o.out_date) = DATE_SUB(CURDATE(), INTERVAL 1 DAY);
如果所有记录挤在一张record表里,就得加WHERE type = 'out',索引效率下降;更麻烦的是,out_record里有sale_order_no(销售单号)、customer_name(客户名),而ck_record里有batch_no(批次号)、supplier_name(供应商名),字段语义完全不同,硬塞一起会导致大量NULL值,违反数据库范式第三定律。
ck.sql还做了两处关键设计:
1. 所有数字字段设为NOT NULL + DEFAULT 0:比如product.stock_num默认0,避免因NULL参与计算导致库存扣减异常(stock_num = stock_num - 1,若原值为NULL,结果仍是NULL);
2. 关键时间字段统一用DATETIME而非TIMESTAMP:out_date、pd_date等全部用DATETIME,避免MySQL 5.6以下版本TIMESTAMP自动更新的陷阱——曾有客户反馈“盘点时间总变成当前时间”,查了一天发现是TIMESTAMP字段定义漏写了DEFAULT CURRENT_TIMESTAMP。
这些不是炫技,是血泪教训换来的防御性设计。
2. 核心模块解析与实操要点
2.1 登录验证链:verify.php + session + 路径防护的三层防线
登录流程表面简单:login.php表单POST到verify.php → 校验账号密码 → 设置session → 跳转frame.php。但真正保障安全的,是藏在背后的三层防护。
第一层:verify.php的输入净化与SQL防注入
verify.php开头就有:
$username = trim($_POST['username']);
$password = trim($_POST['password']);
// 强制转义,杜绝' OR '1'='1'注入
$username = mysqli_real_escape_string($conn, $username);
$password = mysqli_real_escape_string($conn, $password);
$sql = "SELECT * FROM admin_user WHERE user_name = '$username' AND password_md5 = '" . md5($password) . "'";
注意:这里用的是md5($password)而非password_hash(),是历史兼容性选择(老系统迁移)。但mysqli_real_escape_string必须存在,否则用户名输admin' --就能绕过密码直接登录。
第二层:session的严格绑定与生命周期控制
verify.php成功后设置session:
session_start();
$_SESSION['admin_id'] = $row['id'];
$_SESSION['user_name'] = $row['user_name'];
$_SESSION['role_level'] = $row['role_level'];
$_SESSION['login_time'] = time();
// 关键:绑定IP和User-Agent,防session劫持
$_SESSION['client_ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
然后在conn/db_config.php里,所有需要权限的页面(如admin/user_list.php)开头都有:
session_start();
if (!isset($_SESSION['admin_id'])) {
header("Location: login.php");
exit;
}
// 二次校验:IP和UA是否匹配
if ($_SESSION['client_ip'] !== $_SERVER['REMOTE_ADDR'] ||
$_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
session_destroy();
header("Location: login.php?msg=登录异常,请重新登录");
exit;
}
这就是为什么你在办公室登录后,用手机4G网络访问同一个后台会立刻被踢——session被销毁。这是有意为之的安全策略,牺牲一点便利性,换取账户安全。
第三层:路径防护与目录遍历拦截
所有PHP文件都包含类似代码:
// 防止直接访问
if (!defined('IN_CKGL')) {
exit('Access Denied');
}
而frame.php、main.php等入口文件开头定义:
define('IN_CKGL', true);
这样,当黑客尝试直接请求http://yourdomain.com/admin/user_edit.php?id=1时,因IN_CKGL未定义,页面直接输出Access Denied。同时,conn/目录下db_config.php被.htaccess禁止外部访问(若用Nginx,则需在server块加location ~ ^/conn/ { deny all; })。
这三层防线缺一不可:第一层防SQL注入,第二层防session盗用,第三层防目录遍历和敏感文件泄露。
2.2 出库单据处理:out_product模块的事务与并发控制
out_product/目录是销售出库的核心,包含add.php(录单)、list.php(查单)、detail.php(单据详情)、del.php(作废单据)。其中add.php的库存扣减逻辑,是整个系统最脆弱也最关键的环节。
原始代码是这样的:
// 获取商品当前库存
$sql = "SELECT stock_num FROM product WHERE id = {$product_id}";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_assoc($result);
$current_stock = $row['stock_num'];
// 检查库存是否足够
if ($current_stock < $out_num) {
die("库存不足!当前库存:{$current_stock}");
}
// 扣减库存
$new_stock = $current_stock - $out_num;
$sql = "UPDATE product SET stock_num = {$new_stock} WHERE id = {$product_id}";
mysqli_query($conn, $sql);
// 插入出库记录
$sql = "INSERT INTO out_record (product_id, out_num, out_date, sale_order_no, customer_name) VALUES ({$product_id}, {$out_num}, NOW(), '{$order_no}', '{$customer}')";
mysqli_query($conn, $sql);
这段代码在单用户测试时完全没问题,但上线第一天就出事:两个仓管员同时给同一商品(螺丝钉)录出库单,A录500个,B录300个,结果product.stock_num只扣了500,而不是800。原因?非原子操作 + 无事务。
修复方案必须上数据库事务:
mysqli_autocommit($conn, FALSE); // 关闭自动提交
try {
// 1. 加行锁:SELECT ... FOR UPDATE
$sql = "SELECT stock_num FROM product WHERE id = {$product_id} FOR UPDATE";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_assoc($result);
$current_stock = $row['stock_num'];
if ($current_stock < $out_num) {
throw new Exception("库存不足!当前库存:{$current_stock}");
}
// 2. 更新库存
$new_stock = $current_stock - $out_num;
$sql = "UPDATE product SET stock_num = {$new_stock} WHERE id = {$product_id}";
mysqli_query($conn, $sql);
// 3. 插入出库记录
$sql = "INSERT INTO out_record (product_id, out_num, out_date, sale_order_no, customer_name) VALUES ({$product_id}, {$out_num}, NOW(), '{$order_no}', '{$customer}')";
mysqli_query($conn, $sql);
mysqli_commit($conn); // 提交事务
} catch (Exception $e) {
mysqli_rollback($conn); // 回滚
die($e->getMessage());
}
mysqli_autocommit($conn, TRUE); // 恢复自动提交
关键变化有三处:
- SELECT ... FOR UPDATE:在查询库存时对这一行加写锁,其他事务必须等待锁释放才能读取该商品库存;
- mysqli_autocommit(FALSE):关闭自动提交,确保三步操作要么全成功,要么全失败;
- try/catch包裹:任何一步失败立即回滚,避免库存扣减了但单据没生成,造成账实不符。
这就是为什么我在部署时一定要求客户MySQL引擎用InnoDB——MyISAM不支持行锁和事务,强行加FOR UPDATE会升级为表锁,整个商品表被锁死,其他人连查都查不了。
2.3 库存盘点模块:ckpd_record与差异处理的业务逻辑闭环
ckgl/ckpd_list.php是盘点入口,但真正的业务闭环在ckgl/ckpd_do.php。这里藏着一个容易被忽略的设计:盘点不是覆盖写,而是生成差异记录,并触发后续动作。
原始流程:
1. 仓管员在ckpd_list.php点击“新建盘点”,系统自动生成盘点任务号(如PD20240520001);
2. 他拿着纸质清单去货架清点,回到系统在ckpd_do.php里逐条录入product_id、actual_num(实盘数);
3. 提交后,系统自动计算diff_num = actual_num - before_num(实盘数减账面数),并插入ck_pd_record表。
但仅仅记录差异远远不够。真实业务中,差异必须驱动后续动作:
- diff_num > 0(盘盈):需填写盈余原因(如“供应商多发”“系统漏录入库”),并生成正向入库单;
- diff_num < 0(盘亏):需填写亏损原因(如“自然损耗”“搬运破损”“盗窃”),并生成负向出库单,同步更新product.stock_num。
系统在ckpd_do.php末尾加了这段逻辑:
if ($diff_num != 0) {
// 盈亏单据生成
$record_type = $diff_num > 0 ? 'in' : 'out';
$abs_diff = abs($diff_num);
if ($record_type === 'in') {
// 盘盈:生成入库单
$sql = "INSERT INTO ck_record (product_id, in_num, in_date, batch_no, operator_id, remark)
VALUES ({$product_id}, {$abs_diff}, NOW(), 'PD-{$pd_no}', {$_SESSION['admin_id']}, '盘点盈余')";
} else {
// 盘亏:生成出库单
$sql = "INSERT INTO out_record (product_id, out_num, out_date, sale_order_no, customer_name, remark)
VALUES ({$product_id}, {$abs_diff}, NOW(), 'PD-{$pd_no}', '盘点调整', '盘点亏损')";
}
mysqli_query($conn, $sql);
// 同步更新商品库存
$new_stock = $before_num + $diff_num;
$sql = "UPDATE product SET stock_num = {$new_stock} WHERE id = {$product_id}";
mysqli_query($conn, $sql);
}
这个闭环的意义在于:盘点结果不是停留在报表上的一组数字,而是直接转化为库存账面的修正动作,确保“账实一致”不是一句口号,而是数据库里实实在在的stock_num变更。
提示:
ckpd_do.php里所有INSERT操作都加了ON DUPLICATE KEY UPDATE防重复提交。曾有仓管员手抖点了两次“提交”,导致同一条盘点记录生成了两条盈亏单——加了唯一索引UNIQUE KEY uk_pd_product (pd_id, product_id)后,第二次插入自动转为更新,避免数据污染。
3. 实操部署全流程与关键配置详解
3.1 从零部署:ck.sql导入与conn配置的避坑指南
部署第一步永远是数据库。很多人卡在ck.sql导入失败,报错“#1067 – Invalid default value for ‘create_time’”,这是因为MySQL严格模式(STRICT_TRANS_TABLES)在起作用。
正确导入步骤:
1. 登录phpMyAdmin或命令行,创建数据库ckgl,字符集选utf8mb4(支持emoji,也为未来扩展留余地);
2. 在执行ck.sql前,先执行:
SET SQL_MODE = 'NO_ENGINE_SUBSTITUTION';
SET time_zone = "+00:00";
- 再导入
ck.sql; - 导入成功后,检查
product表结构,确认stock_num字段是INT(11) NOT NULL DEFAULT '0',而非INT(11) NULL DEFAULT NULL。
接着配置数据库连接。打开conn/db_config.php,你会看到:
$host = 'localhost';
$user = 'root';
$pwd = '';
$db = 'ckgl';
$port = 3306;
这里有两个深坑:
- 坑一:$host不能写127.0.0.1。某些Linux服务器(尤其是CentOS 7+)的MySQL默认绑定127.0.0.1,但PHP的mysqli_connect()用localhost会走socket连接,用127.0.0.1走TCP连接,性能差且可能被防火墙拦截。务必保持localhost;
- 坑二:$pwd为空时,部分PHP版本会报Access denied for user 'root'@'localhost'。解决方案:要么给root设密码(ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_password';),要么在db_config.php里把$pwd改成''(空字符串,不是NULL)。
配置完,访问http://yourdomain.com/login.php,初始账号密码是admin / 123456(明文写在ck.sql的admin_user表INSERT语句里)。首次登录后,系统会自动跳转frame.php,左侧菜单应正常显示。
注意:
frame.php里有一行<frameset rows="60,*" cols="*">,如果你用Chrome最新版打不开,显示“Refused to display ‘xxx’ in a frame because it set ‘X-Frame-Options’ to ‘deny’”,说明你的Web服务器(Apache/Nginx)返回了X-Frame-Options: DENY头。解决方法:Apache在.htaccess加Header unset X-Frame-Options;Nginx在server块加add_header X-Frame-Options "" always;。
3.2 权限分级实战:如何给销售员开通“仅看不改”权限?
假设新来一位销售员小李,老板只要求他能查商品库存、录销售出库单,但不能改商品信息、不能看入库记录。这就需要精准调整role_level和菜单权限。
步骤一:数据库层面赋权
执行SQL:
INSERT INTO admin_user (user_name, password_md5, role_level, create_time)
VALUES ('小李', MD5('123456'), 1, NOW());
role_level = 1是最低权限,对应left.php里只显示“首页”和“出库单据”两项菜单。
步骤二:代码层面加固
打开out_product/add.php,找到权限校验段(通常在开头):
if ($_SESSION['role_level'] < 1) {
die('无权访问');
}
确保它是< 1而非== 1,否则role_level = 0(禁用账号)也会被放行。
步骤三:限制数据可见性(可选增强)
销售员不应看到所有商品,只应看到他负责的品类。这时需要改product/list.php的查询SQL:
// 原始:SELECT * FROM product
// 修改后:
if ($_SESSION['role_level'] == 1) {
$sql = "SELECT * FROM product WHERE category IN ('五金', '耗材')"; // 销售员只看这两类
} else {
$sql = "SELECT * FROM product";
}
这种硬编码方式虽不优雅,但胜在简单可控。若需更灵活,可增加admin_user.category_scope字段存JSON数组,再用FIND_IN_SET()查询。
3.3 图片与资源路径:img目录的引用规范与CDN适配技巧
img/目录存放所有图标、logo、按钮背景图。系统里所有图片引用都用相对路径,如<img src="img/logo.png">。这在单机部署时没问题,但一旦上云或配CDN,就面临路径失效风险。
安全引用方案:
在conn/config.php里定义常量:
// 判断是否启用CDN
define('USE_CDN', false);
define('CDN_URL', 'https://cdn.yourdomain.com');
if (USE_CDN) {
define('IMG_PATH', CDN_URL . '/img/');
} else {
define('IMG_PATH', 'img/');
}
然后所有模板里改用:
<img src="<?php echo IMG_PATH; ?>logo.png">
这样,切换CDN只需改USE_CDN为true,无需逐个文件搜索替换。同理可扩展到css/、js/目录。
实操心得:
img/目录下不要放.psd或.ai源文件!曾有客户把Photoshop源文件传上去,黑客用http://domain.com/img/logo.psd直接下载,暴露了公司VI规范。部署前务必用find ./img -name "*.psd" -delete清理。
4. 常见问题与排查技巧实录
4.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
登录后跳转frame.php但左侧菜单空白 | left.php中$_SESSION['role_level']未正确设置或session丢失 | 1. 在verify.php末尾加var_dump($_SESSION);die();2. 检查PHP.ini中 session.save_path目录权限 | 确保session.save_path指向可写目录(如/var/lib/php/sessions),并chmod 733 |
| 出库单提交后库存没减少 | out_product/add.php未启用事务或FOR UPDATE失效 | 1. 查mysql.general_log确认SQL执行顺序2. 手动执行 SELECT * FROM product WHERE id=123 FOR UPDATE看是否报错 | 确认MySQL引擎为InnoDB;检查mysqli_query()返回值是否为false |
ck.sql导入时报错“Unknown collation: ‘utf8mb4_0900_ai_ci’” | MySQL版本低于8.0,不支持新校对规则 | mysqldump --compatible=mysql4重新导出 | 用sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' ck.sql批量替换 |
main.php区域显示“Access Denied” | main.php未定义IN_CKGL或被直接访问 | 在浏览器地址栏直接访问http://domain.com/main.php看是否报错 | 确保所有入口都通过frame.php加载,禁止直接访问main.php |
| 盘点后库存未更新 | ckpd_do.php中UPDATE product语句未执行 | 查error_log是否有mysqli_query(): (HY000/1146): Table 'ckgl.ck_pd_record' doesn't exist | 检查ck.sql是否完整导入,特别是ck_pd_record表 |
4.2 我踩过的三个最深的坑
坑一:时区错乱导致盘点日期全错
客户反馈“昨天做的盘点,报表里显示是明天”。查ck_pd_record.pd_date字段,发现全是2024-05-21 00:00:00,而服务器时间是2024-05-20 16:00:00。根源在php.ini里date.timezone = Asia/Shanghai没配,PHP用UTC时间,MySQL用系统时区(CST),两者相差8小时。
解法: 在conn/db_config.php开头加date_default_timezone_set('Asia/Shanghai');,并在MySQL里执行SET time_zone = '+8:00';。
坑二:中文商品名导致出库单打印乱码
out_product/detail.php导出PDF时,商品名显示为方框。不是字体问题,而是product.name字段在ck.sql里定义为VARCHAR(100)但没指定字符集,建表后实际是latin1。
解法: 执行ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,并确认my.cnf里[mysqld]段有character-set-server=utf8mb4。
坑三:权限升级后菜单不刷新
把小王role_level从2改成3,他仍看不到“库存盘点”菜单。不是缓存,而是left.php里$_SESSION['role_level']变量在frame.php加载时已被读取,后续修改session不触发left.php重载。
解法: 让用户退出重登;或在admin/user_edit.php里加一行header("Location: exit.php?redirect=frame.php");,强制登出后跳回frame。
4.3 二次开发必读:三个安全红线
- 绝不直接拼接用户输入到SQL
即使用了mysqli_real_escape_string,也要优先用预处理语句。例如product/search.php中搜索商品:
```php
// ❌ 危险
$sql = “SELECT * FROM product WHERE name LIKE ‘%{$_GET[‘q’]}%’“;
// ✅ 安全(PDO预处理)
$stmt = $pdo->prepare(“SELECT * FROM product WHERE name LIKE ?”);
$stmt->execute([“%{$_GET[‘q’]}%”]);
```
- 所有文件上传必须校验后缀与MIME类型
admin/upload_logo.php若允许上传PHP文件,黑客可传shell.php获得服务器权限。正确做法:
php $allowed_types = ['image/jpeg', 'image/png', 'image/gif']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $_FILES['file']['tmp_name']); if (!in_array($mime, $allowed_types)) { die('不支持的文件类型'); } $ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION); if (!in_array(strtolower($ext), ['jpg','jpeg','png','gif'])) { die('文件扩展名不合法'); }
- 日志记录必须脱敏
log_operate.content字段若记录UPDATE product SET price_out=999 WHERE id=123,就暴露了定价策略。应在写入前过滤:
php $content = preg_replace('/price_out=[\d.]+/', 'price_out=***', $content); $content = preg_replace('/password=[^&]+/', 'password=***', $content);
这套系统不是银弹,它解决不了所有仓储问题,但它把中小场景最痛的五个点——商品混乱、库存不准、出库扯皮、盘点失真、权限失控——用最朴素的代码钉死了。两年下来,我最深的体会是:好的仓储系统,不在于功能多炫,而在于每一次点击,数据库都如实响应;每一次盘点,账面都自动归零;每一次权限变更,菜单都即时刷新。
最后分享一个小技巧:如果你要加微信通知,别碰out_record表,直接在out_product/add.php事务提交后加一段curl:
// 发送企业微信通知
$data = [
'touser' => '@all',
'msgtype' => 'text',
'agentid' => 1000002,
'text' => ['content' => "【出库提醒】{$product_name}已出库{$out_num}件,单号:{$order_no}"],
'safe' => 0
];
$ch = curl_init('https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.$token);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_exec($ch);
记住,系统是工具,人才是主角。代码可以重写,但仓管员手里的扫码枪、货架上的标签纸、老板桌上的签字笔,才是真实世界的支点。
简介:一套可直接部署的轻量级仓库管理Web系统,后端用PHP编写,数据存储基于MySQL,覆盖商品信息维护、入库登记、出库开单、销售记录追踪、定期库存盘点及管理员分级权限管理。系统采用经典frame框架布局,通过left.php、top.php、main.php实现模块化页面结构,登录验证由verify.php配合session完成,退出操作由exit.php统一处理。附带ck.sql数据库脚本,导入即可初始化全部表结构;核心功能分模块存放——ckgl负责基础仓管逻辑,admin提供后台管理入口,product管理商品资料,out_product专用于出库单据处理,img目录存放图标与界面资源,conn集中配置数据库连接参数。所有PHP文件均内置基础路径防护和安全校验机制,适配中小型企业或个体仓储场景,支持快速上线与按需二次开发。


被折叠的 条评论
为什么被折叠?



