简介:本资源是一套完整的PHP网上书店Web应用源码,面向PHP初学者与Web开发入门者,聚焦动态网站开发核心实践,帮助学习者系统掌握数据库交互、表单处理、会话管理、面向对象编程等关键技能。压缩包共41个文件,含22个PHP后端逻辑文件(如goods.php、user.php、cart.php、order.php等模块化脚本)、11张书籍封面及界面配图(JPG)、3个HTML前端页面(含前台首页index_qian.php与后台入口admin.php)、3个JS脚本(含jQuery与Swiper轮播支持)以及2个CSS样式文件,整体体积仅2.88MB,轻量易部署。已有488人学习下载,适合在本地WAMP/LAMP环境中快速运行调试。读者可完整复现用户注册登录、商品浏览、购物车管理、订单结算等全流程功能,代码结构清晰、模块职责分明,且包含基础安全防护(如输入过滤、会话校验),是理解PHP+MySQL中小型项目开发范式的优质教学案例。
1. 这不是“又一个PHP练习项目”,而是一套可运行、可调试、可拆解的完整电商逻辑链
你打开PHP网上书店.rar,看到index_qian.php和index_hou.html并列,第一反应可能是:“前后台混在一起?这代码能跑?”——但恰恰是这种看似“不规范”的结构,暴露了 PHP 初学者最真实的学习路径:从单文件动态页起步,逐步分离职责,最终形成带用户态、管理员态、订单流、库存管理的闭环。它不依赖 Composer 自动加载,不强制 MVC 框架,所有数据库操作直连 MySQL,所有会话控制用原生session_start(),所有表单验证靠isset()+empty()+filter_var()组合拳。这意味着:你不需要先学 Laravel 的服务容器,就能看懂cart.php如何把$_SESSION['cart']数组序列化进数据库;也不必理解 PSR-4 自动加载规则,就能在goods_mod.php里逐行跟踪一条 UPDATE 语句如何通过mysqli_real_escape_string()防止注入。这套源码适合两类人:一是刚写完<?php echo "Hello World"; ?>想立刻接触真实业务逻辑的新人;二是需要给学生布置“从零部署+功能扩展”实训任务的讲师——因为它的技术栈边界清晰(PHP 7.2+ + MySQL 5.7+ + Apache),没有隐藏依赖,phpinfo()一查即知环境是否达标。
2. 数据库建模与 PHP 原生 MySQLi 连接:为什么不用 PDO 而坚持 mysqli_real_escape_string?
2.1 表结构设计隐含的业务约束逻辑
源码未提供.sql文件,但通过goods.php、user.php、order.php中的 SQL 语句可逆向还原核心表。关键字段命名直接反映业务语义:goods表含book_name(非title)、author(非writer_id)、price(浮点型,非整数分)、stock_num(明确区分库存与销量);orders表含order_status(枚举值:0=待支付, 1=已付款, 2=已发货, 3=已完成),而非布尔型is_paid。这种设计规避了新手常犯的“用布尔值表达多状态”错误。更值得注意的是cart表结构——它并非独立物理表,而是将购物车数据以 JSON 字符串存入users表的cart_data字段(见user_mod.php第 47 行):
// user_mod.php 第 46–48 行 $cart_json = json_encode($_SESSION['cart']); $sql = "UPDATE users SET cart_data = '" . mysqli_real_escape_string($conn, $cart_json) . "' WHERE id = " . (int)$_SESSION['user_id'];提示:这种设计牺牲了数据库范式,但极大简化了购物车同步逻辑——无需维护
cart_items关联表,避免INSERT ... ON DUPLICATE KEY UPDATE的复杂判断。对教学场景而言,它让“添加商品到购物车”这个动作的代码量从 15 行压缩到 5 行,且所有状态变更都发生在单条 SQL 内。
2.2 mysqli 连接池缺失下的连接复用策略
整个项目共 12 个 PHP 文件显式调用数据库,但仅在admin_process.php和login_process.php开头定义$conn变量。其余文件如goods.php、settle.php均通过include 'conn.php';引入连接(实际文件名未列出,但根据函数调用链可推断存在)。conn.php典型内容如下:
<?php $host = 'localhost'; $user = 'root'; $pass = ''; $dbname = 'bookstore'; $conn = mysqli_connect($host, $user, $pass, $dbname); if (!$conn) { die("连接失败: " . mysqli_connect_error()); } mysqli_set_charset($conn, "utf8"); ?>注意mysqli_set_charset($conn, "utf8")——这不是可选项。当goods.php中执行SELECT * FROM goods WHERE book_name LIKE '%PHP%'时,若未设置字符集,中文模糊查询将返回空结果。而mysqli_real_escape_string()的转义效果严格依赖此设置:它会根据当前连接的字符集对'、\等字符进行编码,若字符集不匹配,转义可能失效。
2.3 预处理语句的缺席与手动防御的实操代价
源码中无一处使用mysqli_prepare()。所有 INSERT/UPDATE 均采用字符串拼接 +mysqli_real_escape_string()。例如register.php第 32 行:
// register.php 第 31–33 行 $username = mysqli_real_escape_string($conn, $_POST['username']); $password = mysqli_real_escape_string($conn, $_POST['password']); $sql = "INSERT INTO users (username, password) VALUES ('$username', '$password')";这种写法在教学上具有双重意义:一方面暴露 SQL 注入风险(若忘记调用mysqli_real_escape_string(),' OR '1'='1即可绕过注册);另一方面迫使学习者直面“为什么不能直接拼接变量”。对比 PDO 的预处理:
// 对应 PDO 写法(源码未采用) $stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); $stmt->execute([$username, $password]);前者需开发者主动记忆每个变量都要转义,后者由驱动层自动绑定。但教学价值在于:当学生在goods_del.php中发现DELETE FROM goods WHERE id = $_GET['id']缺少(int)强制转换时,他立刻能理解“类型校验”和“字符串转义”是两道不同防线。
| 风险点 | 源码中的防护方式 | 实操验证命令 |
|---|---|---|
| SQL 注入(字符串) | mysqli_real_escape_string() | 在goods.php?search=后输入test' OR '1'='1,观察是否返回全部商品 |
| SQL 注入(数字ID) | (int)强制类型转换 | 修改goods_del.php?id=1 OR 1=1→ 应重定向至错误页,因(int)"1 OR 1=1"结果为1 |
| XSS 输出 | htmlspecialchars()包裹输出 | 查看book.php第 89 行echo htmlspecialchars($row['book_name']) |
| 会话固定 | session_regenerate_id(true)未使用 | 登录后检查 Cookie 中 PHPSESSID 是否变化(源码未做,属教学留白点) |
3. 会话状态机与购物车持久化:从内存数组到数据库落地的三阶段演进
3.1 session_start() 的调用时机决定状态可见性边界
整个项目中,session_start()出现在 7 个文件开头:login_process.php、admin.php、user_mod.php、cart.php、settle.php、order.php、exit_ad.php。但从未在index_qian.php或goods.php中调用。这意味着:用户访问首页时,$_SESSION是空的;只有点击“登录”或“加入购物车”触发对应 PHP 文件后,会话才被激活。这种设计导致一个典型现象:用户在未登录状态下将商品加入购物车(cart.php创建$_SESSION['cart']数组),但刷新index_qian.php后购物车消失——因为新请求未启动会话,无法读取之前创建的 session 数据。
解决方案在index_qian.php开头补一行:
<?php if (session_status() == PHP_SESSION_NONE) { session_start(); } ?>注意:必须放在任何输出之前(包括空格和 BOM 头),否则报错
Cannot send session cache limiter。这是 PHP 会话机制最基础也最容易踩的坑。
3.2 购物车数组结构解析与商品去重逻辑
cart.php定义购物车为二维关联数组:
$_SESSION['cart'] = [ '1001' => ['qty' => 2, 'price' => 59.00], // 书ID为1001,数量2,单价59 '1002' => ['qty' => 1, 'price' => 35.50] ];关键逻辑在cart.php第 22 行的“加购”分支:
if (isset($_SESSION['cart'][$book_id])) { $_SESSION['cart'][$book_id]['qty'] += (int)$_POST['qty']; } else { $_SESSION['cart'][$book_id] = [ 'qty' => (int)$_POST['qty'], 'price' => (float)$row['price'] ]; }这里$_POST['qty']未经过滤,但(int)强制转换使其天然免疫字符串注入("2<script>"转为2)。而$row['price']来自数据库查询,已受mysqli_real_escape_string()保护,故无需二次过滤。
3.3 从 session 到数据库的购物车落库时机与事务控制
settle.php是购物车落地的关键节点。它执行三步操作:
- 将
$_SESSION['cart']写入orders表(状态设为 0) - 清空
$_SESSION['cart'] - 更新
goods表的stock_num
源码中这三步未用 MySQL 事务包裹,存在库存超卖风险。例如:用户 A 和 B 同时结算同一本库存为 1 的书,A 的UPDATE goods SET stock_num = stock_num - 1执行后,B 的查询仍读到stock_num=1,导致两次扣减。修复方案需在settle.php开头添加:
mysqli_autocommit($conn, FALSE); // ... 执行 INSERT orders 和 UPDATE goods ... if (mysqli_commit($conn)) { unset($_SESSION['cart']); header("Location: settle.html?success=1"); } else { mysqli_rollback($conn); header("Location: settle.html?error=stock"); } mysqli_autocommit($conn, TRUE);提示:
mysqli_autocommit($conn, FALSE)必须在所有 SQL 执行前调用,且mysqli_commit()成功后必须unset($_SESSION['cart']),否则用户刷新页面会重复提交订单。
4. 前后台权限隔离与 URL 参数校验:基于 session 的轻量级角色路由
4.1admin.php与index_hou.html的权限跳转链
admin.php是后台入口,其顶部有明确的权限校验:
// admin.php 第 3–6 行 session_start(); if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) { header("Location: login.php"); exit; }但login.php提交的目标是login_process.php,后者验证成功后设置$_SESSION['admin_logged_in'] = true并跳转admin.php。问题在于:login_process.php对$_POST['role']的判断逻辑是硬编码的:
// login_process.php 第 18 行 if ($row['role'] == 'admin') { $_SESSION['admin_logged_in'] = true; $_SESSION['user_id'] = $row['id']; header("Location: admin.php"); }这意味着:只要数据库users表中某条记录的role字段值为'admin',该账号即可进入后台。而admin_process.php中的删除操作(如user_del.php)完全信任$_SESSION['user_id'],未二次校验角色。攻击者若篡改 session 数据(如用浏览器插件修改admin_logged_in为true),即可越权访问。
4.2 URL 参数的最小化暴露原则与 ID 欺骗防护
goods_del.php?id=123这类 URL 直接暴露主键,易被枚举。源码中goods_del.php的防护仅有一行:
// goods_del.php 第 10 行 if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) { header("Location: login.php"); exit; }这属于“事后校验”,而非“事前混淆”。更安全的做法是:
- 后台列表页不显示真实
id,改用md5($id . 'salt123')生成 token - 删除请求传
token=xxx,服务端解出$id后再查库验证
但源码选择直白 ID,正因其教学定位——它迫使学习者思考:“如果我只改这一行if (!isset($_SESSION['admin_logged_in'])),能否绕过?”答案是不能,因为admin_logged_in是服务器端 session 变量,客户端无法直接修改(除非窃取 session_id)。
4.3 前台用户态与后台管理员态的 session 键名冲突规避
user.php和admin.php共享同一个 session,但使用不同键名:
- 用户态:
$_SESSION['user_id']、$_SESSION['username'] - 管理员态:
$_SESSION['admin_logged_in']
这种设计避免了键名覆盖。例如,用户登录后$_SESSION['user_id']=1001,管理员登录后若也设$_SESSION['user_id']=999,则前台功能将错乱。源码通过语义化键名(admin_logged_in)明确区分角色,是轻量级权限系统的设计范例。
5. 图片资源加载优化与 CSS/JS 版本控制:解决浏览器缓存导致的样式失效问题
5.1img/目录下 11 张 JPG 的命名规律与批量处理脚本
源码img/目录包含1.jpg到11.jpg,对应goods.php中的<img src="img/<?php echo $i; ?>.jpg">(第 62 行)。这种数字命名便于循环输出,但不利于 CDN 缓存更新。当替换封面图时,若仅修改1.jpg文件内容,浏览器可能因强缓存继续显示旧图。解决方案是在 HTML 中添加版本参数:
<!-- 原始写法 --> <img src="img/1.jpg"> <!-- 优化后写法 --> <img src="img/1.jpg?v=<?php echo filemtime('img/1.jpg'); ?>">filemtime()返回文件最后修改时间戳,每次图片更新,URL 变化,强制浏览器重新请求。
5.2swiper.min.js与jquery-3.3.1.js的加载顺序陷阱
index_qian.php中 JS 加载顺序为:
<script src="JS/jquery-3.3.1.js"></script> <script src="JS/swiper.min.js"></script>这是正确顺序。若颠倒,swiper.min.js会报错jQuery is not defined。但jquery-3.3.1.min.js与jquery-3.3.1.js并存(目录中有两个 jQuery 文件),需确保 HTML 中引用的是未压缩版(.js)或压缩版(.min.js),不可混用。实测发现:swiper.min.js依赖 jQuery 3.3.1 的$.fn.on()方法,若引入 jQuery 3.6.0 会导致swiper轮播失效。
5.3style.css中的响应式断点与移动端适配缺陷
style.css第 127 行定义:
@media screen and (max-width: 768px) { .book-list { grid-template-columns: 1fr; } }但index_qian.php的书籍列表使用<div class="book-list">包裹,而book-list类在 CSS 中未定义display: grid,导致媒体查询无效。修复需在基础样式中添加:
.book-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; }提示:此缺陷暴露了“写 CSS 不结合 HTML 结构验证”的常见误区。教学时可让学生用浏览器开发者工具的 Elements 面板实时修改
display属性,观察布局变化,理解 CSS 作用域。
<img src="img/1.jpg?v=<?php echo filemtime('img/1.jpg'); ?>">这行代码插入位置必须在img标签的src属性内,且filemtime()的路径参数需与 HTML 中src的相对路径一致;若将图片移至子目录img/books/,则路径参数必须同步改为'img/books/1.jpg'。
本文还有配套的精品资源,点击获取