拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PHP商城源码解析:从面向过程到安全收编实践

简介这是一份基于PHPMySQL开发的购物商城管理系统源码包代码采用原生PHP与面向过程风格编写未依赖任何框架结构清晰、逻辑直白适合正在学习PHP Web开发的初学者也适合用于毕业设计或课程设计参考。包内共65个文件包含28个PHP功能页面登录注册、商品管理、订单处理等、1个SQL数据库脚本、CSS与JS前端文件、32张PNG图片素材以及DOC格式的函数说明文档和README说明压缩包大小仅1.64MB便于快速下载与本地部署。目前已有116人学习下载代码注释清晰常用于学生项目复盘、课程设计与二次开发。通过阅读源码可以快速理解PHP与MySQL的配合方式、表单数据处理、会话管理、文件上传等经典技术点同时还能借鉴大学生编码风格中的简化实现思路帮助自己快速搭建商城类应用。1. 面向过程的PHP商城代码为什么还值得认真看一遍搜索“PHP购物商城管理系统”的人多半不是要一套生产级微服务方案而是要一份能交差、能讲清原理、能快速改动的课程设计或毕业设计源码。这类包解压之后通常是index.php、conn.php、add_cart.php这样平铺的PHP文件配一个.sql导入脚本。它们没有Composer没有MVC目录没有命名空间连接数据库的$conn变量顺着 include 在页面之间来回传。这套“大学生编码手法”并不像老程序员说的那么不堪它把商品展示、登录注册、购物车、下单这一整条链路压缩到了最小可运行状态很适合用来讲清PHPMySQL的基础写法。但代价同样明显SQL拼接多、页面职责边界模糊、错误处理依赖die()。这篇文章就按这类项目最常见的组织方式把数据库设计、商品列表、购物车、订单事务和本地部署排错拆开讲最后给一套不用引入框架也能落地的安全收编方案适合刚接手这类源码的在校生也适合需要快速改造旧商城的老手。2. 原生PHPMysql商城的文件结构与数据库设计2.1 平铺式include读懂这类代码的入口面向过程的PHP商城有个非常明显的特征几乎每个页面顶部都有一段相同的数据库连接代码。不是封装成单例不是依赖注入就是复制粘贴。文件结构一般长这样shop/ ├─ config.php // 数据库连接配置 ├─ conn.php // 引入config并建立连接很多项目把两者合并 ├─ index.php // 商品列表首页 ├─ detail.php // 商品详情 ├─ login.php // 登录页 ├─ logout.php // 退出登录 ├─ register.php // 注册 ├─ cart.php // 购物车页面 ├─ add_cart.php // 加购处理 ├─ order.php // 订单确认与提交 ├─ admin/ │ ├─ admin_login.php // 后台登录 │ ├─ goods_add.php // 商品添加 │ └─ goods_list.php // 商品管理 └─ install.sql // 数据库导入脚本看懂这套代码的关键在于include的调用方向。每个业务页面顶部先include config.php拿到$conn然后直接写SQL。函数文件通常叫function.php里面的函数体靠global $conn拿到连接句柄。这种写法的优点是链路短从浏览器请求到数据返回只需要经过一个文件调试时打var_dump()就能定位。缺点是$conn随处可见一旦页面被直接访问而没有 include就会报“未定义变量”一类错误。?php // config.php 最简版本 $host 127.0.0.1; $port 3306; $user root; $pass 123456; $dbname shop; // 第5个参数是端口很多老代码写的是3306注意不要写成字符串端口和密码颠倒 $conn mysqli_connect($host, $user, $pass, $dbname, $port); if (mysqli_connect_errno()) { die(数据库连接失败: . mysqli_connect_error()); } mysqli_set_charset($conn, utf8mb4);这段代码里有两处新手老改错。第一mysqli_connect()的参数顺序是主机、用户名、密码、库名、端口有人把密码和库名写反导致“Access denied for user”怎么查都查不出原因。第二符号抑制了连接错误输出连不上时页面白屏只看到die()里的中文提示这时应该先去掉看完整报错或者用mysqli_connect_errno()拿错误码去mysql命令行里核对账号权限。字符集设置用utf8mb4而不是utf8否则商品描述里的特殊符号转义写入时会出问题。2.2 商城最少需要几张表从install.sql反推业务拿到安装包第一步不是急着配环境而是打开install.sql看表结构。一个能跑通“浏览商品→加购→下单”流程的商城最少需要六张表。表名用途关键字段注意事项user前台用户id, username, password, email密码列至少 varchar(255)为后续换 password_hash 留空间category商品分类id, name, parent_id面向过程项目一般只做一级分类parent_id 留着扩展goods商品表id, cid, title, price, stock, image, statusstatus1 才在前台展示下架商品不要物理删除cart购物车id, uid, gid, num, addtime多数用 session 替代但数据表方案更接近真实商城orders订单主表id, orderno, uid, total, status, addtime订单号用date(YmdHis) . rand(1000,9999)生成order_detail订单明细id, order_id, gid, price, num下单时必须把商品价格冗余保存商品改价不影响历史订单MySQL 建表时goods.price不要用float用decimal(10,2)。浮点类型在累加计算时会出现 29.999999 之类的精度误差金额计算不是闹着玩的。库存字段stock用int unsigned下单时在事务里做扣减而不是先查出来再在PHP里减完写回去并发场景下会超卖。2.3 用mysql命令验证导入结果和字符集导入install.sql之前先在命令行确认 MySQL 服务是活的、账号能登录。常见做法是打开终端执行mysql -uroot -p -e SHOW VARIABLES LIKE character_set%; # 输出中 character_set_server 和 character_set_database 建议都是 utf8mb4这条命令的作用是直接通过 MySQL 客户端检查服务器字符集配置绕开 PHP 层能快速定位“页面显示正常但写入数据库变问号”的问题。如果character_set_server是latin1后边 PHP 代码里写再多mysqli_set_charset也会在入库时被转成乱码。修改方式是在 MySQL 配置文件my.cnf的[mysqld]段加character-set-serverutf8mb4然后重启 MySQL 服务。导入表结构用重定向即可mysql -uroot -p shop install.sql mysql -uroot -p -e USE shop; SHOW TABLES;注意shop这个库要提前建好CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4;否则会报Unknown database。第二句SHOW TABLES用来确认导入结果看到六张表都在再进下一步。这一步检查到位了后续商品页和购物车页的联调会顺很多。3. 用面向过程mysqli实现商品、购物车与订单3.1 商品列表页查询循环输出商品列表页是整站访问量最大的页面面向过程写法通常是先拼一条 SQL然后用while循环输出。下面这段代码几乎是这类项目的标准模板?php // index.php 商品列表 include config.php; // 分类筛选参数必须强制转整型防止SQL注入 $cid isset($_GET[cid]) ? intval($_GET[cid]) : 0; $where $cid 0 ? WHERE cid $cid : ; // 分页参数page从1开始每页显示12个商品 $page isset($_GET[page]) ? max(1, intval($_GET[page])) : 1; $pageSize 12; $offset ($page - 1) * $pageSize; $sql SELECT id, title, price, image, stock FROM goods WHERE status 1 $where ORDER BY id DESC LIMIT $offset, $pageSize; $result mysqli_query($conn, $sql); ? table trth商品/thth价格/thth库存/thth操作/th/tr ?php while ($row mysqli_fetch_assoc($result)): ? tr td?php echo $row[title]; ?/td td?php echo $row[price]; ?/td td?php echo $row[stock]; ?/td tda hrefdetail.php?id?php echo $row[id]; ?查看/a/td /tr ?php endwhile; ? /table这段代码有几个细节值得拆开讲。第一intval($_GET[cid])直接把参数变成整数注入字符全被干掉这是面向过程项目里成本最低的防注入手段。第二分页 SQL 用了LIMIT $offset, $pageSize$offset必须经过计算而不是直接拼$_GET[page]否则page-1会让 MySQL 直接报错。第三mysqli_fetch_assoc()拿的是关联数组字段名直接对应数据库列名比mysqli_fetch_row()的数字下标可读性好得多。输出商品名时要记得加htmlspecialchars()商品标题里如果包含引号或尖括号会直接破坏页面结构。3.2 购物车session方案还是数据表方案课程设计级别的商城购物车实现方式分两派。一派用$_SESSION[cart]存数组刷新页面不丢关闭浏览器就清空另一派建cart表用户登录后购物车永久保留。面向过程的代码两种都有从“更像真实商城”的角度看数据表方案更能体现MySQL的增删改查能力。?php // add_cart.php 加购处理 include config.php; session_start(); $uid isset($_SESSION[uid]) ? intval($_SESSION[uid]) : 0; if ($uid 0) { die(请先登录); } $gid intval($_GET[gid]); $num isset($_GET[num]) ? max(1, intval($_GET[num])) : 1; // 先查商品是否存在且上架 $check mysqli_query($conn, SELECT id FROM goods WHERE id $gid AND status 1); if (mysqli_num_rows($check) 0) { die(商品不存在); } // 已存在则数量1不存在则插入 $sql INSERT INTO cart (uid, gid, num, addtime) VALUES ($uid, $gid, $num, NOW()) ON DUPLICATE KEY UPDATE num num $num; mysqli_query($conn, $sql); header(Location: cart.php); exit;这里用了一个MySQL语法点ON DUPLICATE KEY UPDATE。前提是cart表有uid和gid的联合唯一索引否则这个语句不会触发数量累加而是每次加购都插入一行新记录。索引在install.sql里要提前定义好ALTER TABLE cart ADD UNIQUE KEY uk_uid_gid (uid, gid);面向过程函数里处理这类逻辑时最常见的坑是忘了global $conn。如果在function.php里写function addCart($uid, $gid, $num)函数内部直接写mysqli_query($conn, ...)会报“未定义变量 $conn”。必须在函数第一行声明global $conn;才能访问到config.php里创建的连接。这是和面向对象最大的思维差异OOP 把连接存成属性面向过程靠全局变量传递。3.3 下单事务commit与rollback的使用边界下单是整个商城唯一必须用事务的地方。用户点击“提交订单”时后台要做两件事往orders表插一条主记录往order_detail表插多条明细同时扣减goods表库存。任何一个步骤失败都不能留下半张订单。?php // order_submit.php 下单处理 include config.php; session_start(); $uid intval($_SESSION[uid]); // 关闭自动提交开启事务 mysqli_begin_transaction($conn, MYSQLI_TRANS_START_READ_WRITE); try { // 生成订单号并插入主表 $orderno date(YmdHis) . rand(1000, 9999); $sql INSERT INTO orders (orderno, uid, total, status, addtime) VALUES ($orderno, $uid, 0, 0, NOW()); if (!mysqli_query($conn, $sql)) { throw new Exception(订单创建失败); } $orderId mysqli_insert_id($conn); // 从购物车读取商品并写入明细同时扣库存 $cartSql SELECT gid, num FROM cart WHERE uid $uid; $cartResult mysqli_query($conn, $cartSql); $total 0; while ($item mysqli_fetch_assoc($cartResult)) { $gid intval($item[gid]); $num intval($item[num]); // 扣库存条件里带上当前库存值防止超卖 $updateSql UPDATE goods SET stock stock - $num WHERE id $gid AND stock $num; if (mysqli_affected_rows($conn) 0) { throw new Exception(商品 ID $gid 库存不足); } // 查询商品价格并写入明细这里做了冗余 $priceResult mysqli_query($conn, SELECT price FROM goods WHERE id $gid); $price mysqli_fetch_assoc($priceResult)[price]; $total $price * $num; $detailSql INSERT INTO order_detail (order_id, gid, price, num) VALUES ($orderId, $gid, $price, $num); if (!mysqli_query($conn, $detailSql)) { throw new Exception(明细写入失败); } } // 更新订单总金额 mysqli_query($conn, UPDATE orders SET total $total WHERE id $orderId); // 清空购物车 mysqli_query($conn, DELETE FROM cart WHERE uid $uid); // 全部通过提交事务 mysqli_commit($conn); header(Location: order_success.php?orderno . $orderno); } catch (Exception $e) { mysqli_rollback($conn); die(下单失败: . $e-getMessage()); }这段代码里有三个面向过程项目必须养成的习惯。mysqli_affected_rows($conn)返回的是上一条语句影响的行数UPDATE goods SET stock stock - $num WHERE id $gid AND stock $num这个 SQL 巧妙地利用 WHERE 条件做并发控制库存不够时影响行数为 0程序立刻抛异常回滚。mysqli_insert_id($conn)拿的是刚插入的orders.id必须在插入后马上调用中间不能穿插其他查询。最后事务结束后要手动mysqli_commit()只写mysqli_begin_transaction()不写提交脚本结束时会自动回滚订单会神秘消失。订单状态字段status建议用整数而不是字符串0待付款、1已付款、2已发货、3已完成、4已取消。面向过程代码里用switch做状态映射不要在数据库里存“待付款”这种中文值否则后续统计报表写SQL会非常痛苦。3.4 面向过程页面常见的两个脏数据出口商品列表、详情页、搜索页是注入重灾区购物车和订单页则是 XSS 重灾区。典型的搜索页代码长这样// search.php 危险写法 $kw $_GET[kw]; $sql SELECT * FROM goods WHERE title LIKE %$kw%; $result mysqli_query($conn, $sql);用户在地址栏输入search.php?kw%27%20OR%2011%20--%20拼出来的 SQL 就变成WHERE title LIKE %% OR 11 -- %整张商品表被拖出来。搜索这种场景不适合用intval()因为关键词本来就是字符串。正确做法是转义或预处理面向过程里最省事的转义函数是$kw mysqli_real_escape_string($conn, $_GET[kw]);mysqli_real_escape_string()会对、、\等字符做转义让它们失去SQL语法含义。注意它依赖数据库连接的字符集所以必须先mysqli_set_charset()再调用。商品标题输出的 XSS 预防则是另一件事等到了第五章统一处理。4. PHPMysql商城本地部署版本、报错与字符集4.1 部署前先检查PHP版本和已移除的函数老源码包里偶尔还能见到mysql_connect()这种不带i的老函数。PHP 7.0 已经彻底移除了mysql_*扩展直接运行会报致命错误Call to undefined function mysql_connect()。部署前先跑两个命令php -v php -m | grep mysqli第一行查看PHP版本第二行确认mysqli扩展已经启用。如果php -m的输出里没有mysqliWindows下需要在php.ini里去掉extensionmysqli前面的分号Linux下需要安装php-mysql扩展包。还有一种常见情况是命令行PHP和集成面板的PHP版本不一致命令行是 5.6面板里是 8.1代码在浏览器里正常在终端跑脚本却报错。这种问题优先检查环境变量里php指向哪个路径。PHP 8.0 以后还有几个影响老商城运行的变化each()函数被移除原来用来遍历数组的while (list($k, $v) each($arr))要改成foreach ($arr as $k $v)count()在参数不是数组或Countable对象时会抛 TypeError$HTTP_RAW_POST_DATA早就没了获取原始请求体要用file_get_contents(php://input)。把报错信息复制到搜索框里基本都能定位到具体是哪一行语法需要改。4.2 三个常见报错的定位逻辑学弟学妹把压缩包发给我的时候最常见的三个报错其实是可预测的。报错信息原因解决路径Connection refusedMySQL服务没启动或端口不是3306检查服务状态mysql -uroot -p能否登录Access denied for user rootlocalhost密码错误或账密写反核对 config.php 参数顺序去mysql命令行验证Uncaught Error: Call to undefined function函数名拼错或扩展未启用php -m检查扩展确认函数名是 mysqli 前缀Headers already sent输出内容在 header() 之前产生检查文件开头有没有空格或UTF-8 BOM最后这个 BOM 问题在大学项目里出现概率极高。用记事本编辑过PHP文件再保存文件头部会多三个不可见字节header(Location: xxx)就会报 “headers already sent”。解决方法是用 VS Code 打开文件右下角编码选择“UTF-8”重新保存或在输出前执行ob_start()。4.3 上传图片和商品描述的中文乱码处理商城后台上传商品图片时文件名经常是中文存到数据库再显示出来就变成一堆百分号。原因有两个层面一是HTTP传输过程中浏览器对中文文件名做了urlencode编码二是数据库表字符集不是utf8mb4。处理上传文件名的标准做法是丢弃原名用时间戳重新生成从根上避开编码问题。?php // admin/goods_add.php 图片上传处理 $ext pathinfo($_FILES[image][name], PATHINFO_EXTENSION); // 白名单校验只允许图片格式 $allow [jpg, jpeg, png, gif, webp]; if (!in_array(strtolower($ext), $allow)) { die(不支持的图片格式); } // 文件名用时间戳随机数避免中文乱码和路径穿越 $newName date(YmdHis) . _ . rand(1000, 9999) . . . $ext; $target ./uploads/ . $newName; move_uploaded_file($_FILES[image][tmp_name], $target);pathinfo(..., PATHINFO_EXTENSION)提取扩展名strtolower()统一成小写再和$allow白名单比较JPG和jpg都能通过。文件名里的冒号、斜杠这类字符在 Windows 和 Linux 文件系统里表现不同用date(YmdHis)加随机数生成新名是最保险的。商品描述字段如果出现中文乱码先确认 HTML 页面头部有meta charsetutf-8再确认 PHP 文件本身是 UTF-8 保存最后看数据库连接mysqli_set_charset($conn, utf8mb4)是否在查询前执行。三层都对齐了乱码问题基本绝迹。5. 无框架商城最小安全收编预处理、哈希与自检5.1 把登录和详情页的拼接SQL换成预处理面向过程代码整体改造工作量大但有两个页面必须优先处理登录页和商品详情页。登录页被注入直接等于账号被接管详情页的id参数虽然用了intval()能防注入但换成预处理语句后代码风格更统一后续维护也少踩坑。以登录查询为例?php // login.php 使用mysqli预处理替代拼接 $username $_POST[username]; $password $_POST[password]; $stmt mysqli_prepare($conn, SELECT id, username, password FROM user WHERE username ?); mysqli_stmt_bind_param($stmt, s, $username); mysqli_stmt_execute($stmt); $result mysqli_stmt_get_result($stmt); $user mysqli_fetch_assoc($result); mysqli_stmt_close($stmt);mysqli_prepare()先把SQL语句发送到MySQL服务端做预编译?是参数占位符后续mysqli_stmt_bind_param()的s表示绑定一个字符串类型参数。这种方式把数据和SQL结构彻底分开任何引号、注释符都只是字符串内容不再具有SQL语法含义。注意mysqli_stmt_get_result()需要mysqlnd驱动集成环境面板默认都有如果运行时报“Call to undefined function”说明PHP装的是老版本libmysqlclient改用mysqli_stmt_bind_result()配合mysqli_stmt_fetch()也能实现同样的查询。5.2 把md5密码升级成password_hash老商城几乎没有例外地用md5($password)存密码这种散列已经被彩虹表覆盖到接近裸奔。改造方案是用PHP自带函数一行代码换存储一行代码换校验// 注册时存储 $hash password_hash($password, PASSWORD_DEFAULT); // 登录时校验 if (password_verify($password, $user[password])) { // 密码正确写入session }PASSWORD_DEFAULT目前会生成bcrypt格式的哈希串长度60字符所以前面建表时要求password字段留varchar(255)。老数据里的md5哈希无法反向还原但可以在登录校验时做兼容如果$user[password]长度是32且与md5($password)相等就自动升级为password_hash($password, PASSWORD_DEFAULT)并写回数据库用户无感知完成了过渡。这个技巧在接手旧项目时非常实用。5.3 交付前跑一遍语法检查和文件扫描最后收尾时别只点开页面点两下就完事。用命令做两轮全量检查比肉眼强得多# 遍历所有php文件检查语法错误 find . -name *.php -exec php -l {} \; # 扫描常见的危险函数和后门特征 grep -rn eval( --include*.php . grep -rn base64_decode( --include*.php .php -l只做语法解析不执行代码能一次性揪出所有缺分号、少括号的问题。grep扫到eval或base64_decode时如果代码里没有合理的业务场景比如模板引擎或加密解密组件就要打开文件逐行看上下文这类函数是PHP webshell 最常见的藏身点。检查完这两项再把第五章改过的登录和详情页重新跑一遍确认购物车加购和下单事务没有受到预处理改造的影响。一套原本只求能跑的课程设计商城到了这一步才算真正达到可交付、可演示、可继续扩展的状态。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门