简介面向PHP与MySQL课程设计的小型购物系统完整项目包内含课程论文、Web源码和数据库脚本覆盖管理员与普通顾客两种登录模式适合高校学生完成电商类课设或作为PHP入门实战参考。压缩包共74个文件大小约13.97MB其中30个PHP页面承担商品展示、购物车、订单、后台管理等核心功能CSS与JavaScript负责前端交互与样式JPG/PNG提供图片素材SQL文件用于快速导入数据库Word文档即配套课程论文整体目录将用户端与管理端分开便于对照学习。目前已有4046人浏览学习。通过该资源可理解登录身份不同导致页面动态变化的实现方法掌握权限区分、商品增删、购物车结算、订单处理等关键模块的编码思路还能直接复用论文与源码进行演示或二次改造对课程设计和毕业设计均有较强参考价值。1. 课程设计选型实录这套 PHPMySQL 购物系统解决什么问题期末课题是做一个基于 PHPMySQL 的小型购物系统网站时最怕的不是写不出代码而是图省事直接套模板结果答辩时连“购物车存在哪、订单状态怎么跳”都答不上来。这套资源就是把完整的购物网站闭环——商品展示、用户注册登录、购物车、下单结算、后台订单管理——全部拆开用最直白的 PHP 代码和 MySQL 表结构串起来每个模块都能独立讲清楚。适合两类人一是正在做课程设计、需要快速跑通全流程的同学二是想用这套结构当基座后续改成商城毕业设计的从业者。资源里带建库 SQL、前后台 PHP 源码和演示用的初始化数据照着导入数据库就能开始改。2. 搭环境与建库XAMPP 配置、六张业务表与数据初始化2.1 环境选型为什么课程设计默认看 XAMPP本地跑 PHPMySQL我一般直接用 XAMPP 或者 phpStudy原因很简单Apache、PHP、MySQL 三个组件一次装齐phpMyAdmin 也能顺手打开省掉手动配环境变量的时间。需要注意版本选择下载的这套源码按 PHP 7.4 以上语法写的用 PHP 8.x 也能跑但别选 PHP 5 时代的安装包那会儿的mysql_*函数已经删干净了拿到代码第一步就会报Call to undefined function。装完后先做三件验证确保基础环境没问题。第一命令行执行php -v能看到版本号第二浏览器打开 Apache 默认首页第三phpMyAdmin 能正常打开并登录 MySQL。这三步过了再处理源码包。MySQL 8.0 的默认认证插件是caching_sha2_password老代码如果连不上大概率是认证方式兼容问题后面避坑章会细说。2.2 建库脚本六张表的最小 Schema这套系统的数据库设计是我比较认可的方式六张表各管一件事不堆冗余字段但每张表都留了扩展点。打开 phpMyAdmin新建数据库shop字符集选utf8mb4_general_ci然后执行下面的 SQLCREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(255) NOT NULL COMMENT 密码哈希, email varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, cat_name varchar(50) NOT NULL, sort int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id int(11) NOT NULL AUTO_INCREMENT, cat_id int(11) NOT NULL COMMENT 分类ID, goods_name varchar(200) NOT NULL, price decimal(10,2) NOT NULL COMMENT 单价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, image varchar(255) DEFAULT NULL COMMENT 图片路径, description text, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量冗余字段, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_cat (cat_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cart ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, goods_id int(11) NOT NULL, num int(11) NOT NULL DEFAULT 1, add_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id,goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int(11) NOT NULL, total_price decimal(10,2) NOT NULL, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, add_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders_detail ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, goods_id int(11) NOT NULL, goods_name varchar(200) NOT NULL COMMENT 下单时商品名快照, price decimal(10,2) NOT NULL COMMENT 下单时单价快照, num int(11) NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个细节值得注意。price用decimal(10,2)而不用float因为浮点算钱会有精度误差这在电商系统里是常识答辩时主动提出来是加分项orders_detail里冗余了goods_name和price因为商品信息可能被管理员修改或删除但订单历史必须保持下单那一刻的快照stock和sales分开存前者是可用库存后者是累计销量更新逻辑在第 4 章会展开。建表时建议把外键都省略用KEY建普通索引就够了。课程设计阶段用外键约束反而容易在删除商品时被订单明细挡住徒增联调成本这是我带项目时踩过一遍的坑。2.3 初始化数据与图片路径约定表建好之后需要填一点初始数据否则前台空空如也。常见的做法是用一段INSERT脚本把分类、演示商品、测试账号都准备好INSERT INTO category (cat_name, sort) VALUES (手机数码, 1), (电脑办公, 2), (图书文娱, 3); INSERT INTO goods (cat_id, goods_name, price, stock, image, description, sales) VALUES (1, 测试手机A, 1999.00, 100, uploads/phone_a.jpg, 演示用商品, 23), (1, 测试手机B, 2999.00, 50, uploads/phone_b.jpg, 演示用商品, 18), (2, 机械键盘, 399.00, 200, uploads/keyboard.jpg, 演示用商品, 66), (3, PHP入门书籍, 59.00, 300, uploads/book_php.jpg, 演示用商品, 120); INSERT INTO users (username, password, email) VALUES (test, $2y$10$VFTn9XfW3kG5uCg0eBkUouQzY8v3Iu7JjzM8hTnYwO6L1S9R0Xa2K, testexample.com);这里的password不是明文也不是 md5而是一个password_hash()生成的 bcrypt 哈希后面登录校验必须用password_verify()配对。图片路径统一写成uploads/xxx.jpg这种相对路径不要写死成C:/xampp/htdocs/...——换台电脑演示就全裂开。项目根目录下建一个uploads/文件夹放图片这是这套资源里默认的目录约定代码里所有图片引用都基于这个约定来改目录名时记得全局搜索uploads。初始化数据导完后顺手在 phpMyAdmin 里跑一句SELECT COUNT(*) FROM goods看到 4 条记录就说明导入成功。接下来写 PHP 代码时把这个db.php连接配置放在项目根目录所有页面require_once它比每个文件重复写连接要干净得多。3. 前后台核心逻辑登录鉴权、商品查询与 Session 购物车3.1 登录与 Session连接数据库、密码校验与错误处理先在项目根目录建一个统一连接文件db.php后面每个页面都从它拿连接。注意我用的是mysqli面向对象写法PHP 8 下完全兼容?php // db.php 数据库连接配置 $host 127.0.0.1; $user root; $pass root; // 换成你自己 MySQL 的密码 $dbname shop; $port 3306; $conn new mysqli($host, $user, $pass, $dbname, $port); if ($conn-connect_error) { die(数据库连接失败 . $conn-connect_error); } $conn-set_charset(utf8mb4);host这里我建议写127.0.0.1而不是localhost。尤其在 macOS 上PHP 解析localhost时可能优先走 socket 文件/tmp/mysql.sock而 MySQL 实际把 socket 放在/opt/lampp/var/mysql/mysql.sock两边对不上就会报连接失败写成 IP 强制走 TCP 就能绕开这个坑。端口默认 3306如果 XAMPP 里 MySQL 被改过端口这里要同步改。登录页的校验逻辑是这套系统的核心之一。把它独立成一个login.php表单 POST 提交后走下面这段?php // login.php 登录处理 session_start(); require_once db.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $sql SELECT id, username, password FROM users WHERE username ? AND status 1 LIMIT 1; $stmt $conn-prepare($sql); $stmt-bind_param(s, $username); $stmt-execute(); $result $stmt-get_result(); $user $result-fetch_assoc(); if ($user password_verify($password, $user[password])) { session_regenerate_id(true); // 防止会话固定攻击 $_SESSION[user_id] $user[id]; $_SESSION[username] $user[username]; header(Location: index.php); exit; } $error 用户名或密码错误请重试;这里我直接从预处理语句开始写而不是先用字符串拼接再教你怎么修。?是占位符bind_param(s, $username)是参数绑定这条路径在答辩时被问“防 SQL 注入”可以直接拿它当答案。password_verify()是password_hash()的配对函数自动处理盐值和哈希比对不需要自己拆字符串。session_regenerate_id(true)一行代码防会话固定攻击属于老师问“你怎么考虑安全的”时你能答出来的亮点。3.2 商品列表与分页WHERE 拼接、ORDER BY 与 LIMIT商品列表页是前台的门面一般带分类筛选和分页。分类筛选本质是动态拼WHERE条件分页则是LIMIT偏移计算。先看核心查询逻辑?php // list.php 商品列表 分类筛选 分页 require_once db.php; $page max(1, (int)($_GET[page] ?? 1)); $pageSize 9; $offset ($page - 1) * $pageSize; $where WHERE 11 ; $params []; $types ; if (!empty($_GET[cat_id])) { $where . AND cat_id ? ; $params[] (int)$_GET[cat_id]; $types . i; } $countSql SELECT COUNT(*) AS total FROM goods . $where; $stmt $conn-prepare($countSql); if ($types) $stmt-bind_param($types, ...$params); $stmt-execute(); $total $stmt-get_result()-fetch_assoc()[total]; $sql SELECT id, goods_name, price, image, sales FROM goods . $where . ORDER BY id DESC LIMIT {$offset}, {$pageSize}; $stmt $conn-prepare($sql); if ($types) $stmt-bind_param($types, ...$params); $stmt-execute(); $goodsList $stmt-get_result()-fetch_all(MYSQLI_ASSOC); $totalPages max(1, ceil($total / $pageSize));WHERE 11是拼接条件的惯用技巧核心作用是让后续每个AND子句都不需要判断“是不是第一段”代码少三个分支后边加减筛选条件只动一行。分页参数按(page - 1) * pageSize计算偏移这是LIMIT的标准用法LIMIT 0, 9是第 1 页LIMIT 9, 9是第 2 页。注意ORDER BY id DESC商品按最新入库排前面这也是老师常考的一个细节排序字段选错会让分页数据出现重复。前端渲染部分就是foreach循环拼卡片图片地址用htmlspecialchars($goods[image])输出防止商品名里带引号把 HTML 结构打穿。价格显示记得用number_format($goods[price], 2)做格式化这不是可选项直接输出1999.00和输出1,999.00的观感差距很大。3.3 Session 购物车数据结构、增删改与持久化边界购物车有两种做法存数据库表或者存 Session。这套系统默认走 Session理由是购物车是短生命周期数据用户关浏览器就作废为它建表反而制造垃圾数据。Session 里的结构用关联数组goods_id做键数量和单价做值增删改都围绕这个数组展开?php // add_cart.php 加入购物车 session_start(); require_once db.php; if (!isset($_SESSION[user_id])) { header(Location: login.php); exit; } $goodsId (int)($_GET[goods_id] ?? 0); $num max(1, (int)($_GET[num] ?? 1)); // 校验商品存在且库存充足 $stmt $conn-prepare(SELECT id, goods_name, price, stock FROM goods WHERE id ?); $stmt-bind_param(i, $goodsId); $stmt-execute(); $goods $stmt-get_result()-fetch_assoc(); if (!$goods || $goods[stock] $num) { die(商品不存在或库存不足); } if (!isset($_SESSION[cart])) { $_SESSION[cart] []; } if (isset($_SESSION[cart][$goodsId])) { $_SESSION[cart][$goodsId][num] $num; } else { $_SESSION[cart][$goodsId] [ name $goods[goods_name], price $goods[price], num $num ]; } $_SESSION[cart][$goodsId][num] min( $_SESSION[cart][$goodsId][num], $goods[stock] ); header(Location: cart.php);每次加购都查一次数据库不是为了性能而是为了校验用户拿一个不存在的goods_id硬拼 URL 时后端不能把脏数据写进 Session。min()限制购买数量不超过库存这是第一道关口。Session 数组里我冗余存了name和price好处是购物车页渲染时不用回查商品表坏处是用户下单前如果管理员改了价格购物车显示的还是旧价格。这个矛盾在结算环节处理生成订单时重新读一遍商品表以数据库里的最新价格为准。这一章的三个模块是完整闭环的登录态控制能不能加购商品查询是购物车的数据源Session 购物车攒够了下单条件。下一步就是结算。4. 结算与订单流转库存扣减、事务与订单状态设计4.1 结算下单从 Session 到订单表的数据流下单是整个系统里最容易写崩的环节因为它同时涉及三件事写订单主表、写订单明细表、清空购物车这三件事中间任何一步失败都会产生脏数据。所以在动手写 SQL 之前先理一下数据流从 Session 购物车取出所有条目重新查询数据库拿到每个商品的最新价格和库存计算订单总价插入orders主表拿到自增order_id循环插入orders_detail明细扣减库存清空 Session 购物车。核心实现如下注意第 4 步到第 6 步被包在一个事务里?php // checkout.php 结算下单 session_start(); require_once db.php; if (empty($_SESSION[cart]) || empty($_SESSION[user_id])) { header(Location: index.php); exit; } $orderNo date(YmdHis) . rand(1000, 9999); $total 0; $conn-begin_transaction(); try { // 1. 重新计算价格 $items []; foreach ($_SESSION[cart] as $goodsId $item) { $stmt $conn-prepare(SELECT goods_name, price, stock FROM goods WHERE id ? FOR UPDATE); $stmt-bind_param(i, $goodsId); $stmt-execute(); $goods $stmt-get_result()-fetch_assoc(); if (!$goods || $goods[stock] $item[num]) { throw new Exception(商品 {$item[name]} 库存不足); } $items[] [ goods_id $goodsId, name $goods[goods_name], price $goods[price], num $item[num] ]; $total $goods[price] * $item[num]; } // 2. 写订单主表 $stmt $conn-prepare(INSERT INTO orders (order_no, user_id, total_price, status) VALUES (?, ?, ?, 0)); $stmt-bind_param(sid, $orderNo, $_SESSION[user_id], $total); $stmt-execute(); $orderId $stmt-insert_id; // 3. 写订单明细 $stmt $conn-prepare(INSERT INTO orders_detail (order_id, goods_id, goods_name, price, num) VALUES (?, ?, ?, ?, ?)); foreach ($items as $item) { $stmt-bind_param(iisdi, $orderId, $item[goods_id], $item[name], $item[price], $item[num]); $stmt-execute(); } $conn-commit(); // 4. 清空购物车 unset($_SESSION[cart]); header(Location: order_success.php?order_no . $orderNo); } catch (Exception $e) { $conn-rollback(); die(下单失败 . $e-getMessage()); }订单号date(YmdHis) . rand(1000, 9999)是课程设计里最常见的做法——时间戳保证同一秒内有 9999 种变化简单够用。真正上线会用更严谨的雪花算法但答辩时说清楚“时间戳加随机数避免重复”已经及格。查询商品时加的FOR UPDATE是行级锁作用是把当前事务要操作的商品行锁住防止两个订单同时在减同一个商品的库存这一行的价值在下一节会体现得更明显。事务这里必须理解一点commit()之前任何一步抛异常rollback()都会把前面所有 INSERT 全部撤销。所以千万别把“清空 Session 购物车”放进事务里——Session 是文件存储不受数据库事务控制放进事务里反而在回滚时出现“订单没生成但购物车空了”的灵异现象。4.2 库存扣减一条 UPDATE 解决并发超卖下单流程写完了但库存扣减我要单独拎出来讲因为这是课程设计答辩里老师们最爱追问的点。最蠢的写法是下面这种先SELECT stock在 PHP 里判断库存够不够再UPDATE stock stock - 1。这在单用户演示时没问题但一旦涉及“两个用户同时买最后一件商品”的场景两个请求都读到库存为 1都判断够都执行扣减库存就变成 -1 了。正确的扣减方式是把“判断库存够不够”和“扣减”合并成一条 SQL?php // 库存扣减带有条件判断的原子 UPDATE $stmt $conn-prepare( UPDATE goods SET stock stock - ?, sales sales ? WHERE id ? AND stock ? ); $stmt-bind_param(iiii, $num, $num, $goodsId, $num); $stmt-execute(); if ($stmt-affected_rows 0) { throw new Exception(库存不足扣减失败); }这条 SQL 的精髓在WHERE stock ?。MySQL 执行 UPDATE 时是逐行加锁再判断条件stock ?不成立就不更新affected_rows返回 0于是我们就能识别出“库存不够”这个业务场景。整个过程是原子操作不需要SELECT先查一遍也没有两个请求同时读到旧库存的窗口期。affected_rows判断的是“真正发生变化的行数”和“匹配到几行”是两回事。第一次扣减 5 件affected_rows是 1第二次再扣 3 件affected_rows还是 1只有当stock小于要扣的数量时才会返回 0。这个细节如果答辩被问到答出来基本就是满分反馈。4.3 订单状态字段枚举值与模拟支付orders.status是订单流转的轴心我用tinyint存数字而不是直接存字符串原因是省空间且方便扩展。状态定义如下状态值含义触发动作0待支付下单成功默认值1已支付用户点击“模拟支付”2已发货后台管理员点击发货3已完成用户确认收货4已取消用户取消或超时未支付课程设计里没有真实支付网关一般用“模拟支付”按钮代替。点击后执行一条更新语句把状态从 0 改成 1并写入pay_time?php // pay.php 模拟支付 session_start(); require_once db.php; $orderId (int)($_GET[order_id] ?? 0); $stmt $conn-prepare( UPDATE orders SET status 1, pay_time NOW() WHERE id ? AND user_id ? AND status 0 ); $stmt-bind_param(ii, $orderId, $_SESSION[user_id]); $stmt-execute(); if ($stmt-affected_rows 0) { die(订单不存在、已支付或无权操作); } header(Location: order_list.php);注意WHERE里带了user_id意思是每个用户只能支付自己的订单这是越权防护的底线。同时带上status 0保证“已支付订单不能被重复支付”和库存扣减一样把业务校验写进 SQL 条件里比先查询再判断更可靠。订单状态机在后台管理里还要配合“发货”操作把状态从 1 改成 2。这步操作通常要管理员权限代码里加一个角色判断比如$_SESSION[is_admin] 1才放行。前台用户对订单只有“取消”“确认收货”两个动作对不上状态值就直接返回友好提示不要抛怪异的 SQL 错误。5. 避坑清单中文乱码、socket 连接失败与 5 个高发翻车点5.1 中文乱码建库、连接、页面三层统一现象商品名、用户名在页面上显示成???或一堆乱码。原因三层字符集不一致——建库时用了latin1PHP 连接后没设置字符集HTML 页面头部又写了gb2312。任何一层不统一中文就必乱。解决建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci连接后立刻执行$conn-set_charset(utf8mb4)页面meta charsetutf-8三处全部统一。如果是从旧库导出的数据在 phpMyAdmin 里把表的排序规则批量改成utf8mb4_general_ci再重新导入。顺带说明utf8mb4和utf8在 MySQL 里的区别是前者支持 emoji 和生僻字宁可无脑选utf8mb4。5.2 数据库连接失败error 2002 (HY000) 与 socket 之谜现象刷新页面直接报Warning: mysqli::connect(): (HY000/2002): Cant connect to local MySQL server through socket /tmp/mysql.sock但 phpMyAdmin 明明能打开。原因PHP 尝试通过/tmp/mysql.sock连接 MySQL但 XAMPP 的 MySQL 实际把 socket 文件放在/opt/lampp/var/mysql/mysql.sock。两边位置对不上TCP 连接又没被使用就卡在 socket 这一层。解决db.php里把host从localhost改成127.0.0.1PHP 会强制走 TCP 而不是 socket。顺带检查 MySQL 服务是否真的启动了——XAMPP 控制台里 MySQL 显示绿色才是在运行。换电脑或重装环境后先跑一遍php -m | grep mysqli确认扩展存在再谈连接问题。5.3 SQL 注入登录框里最常见的扣分项现象在用户名输入框填 OR 11结果不需要密码就登录进去了。原因登录查询用的是字符串拼接比如$sql SELECT * FROM users WHERE username $username。输入的内容被当成 SQL 代码执行OR 11恒为真条件被绕过。解决所有涉及用户输入的 SQL 一律改预处理语句prepare()bind_param()不拼接字符串。那怕是$_GET[id]这种看起来只是数字的参数也要用(int)强制转换或者绑定参数。答辩老师最爱在登录框和商品详情页 URL 里试这两处提前把两处都改成预处理基本就封死了。5.4 mysqli 扩展与 PHP 版本错位现象页面空白错误日志里写Call to undefined function mysqli_connect()或者打开后台直接 500。原因PHP 8 的安装包里有些版本默认没开mysqli扩展或者代码里还在用 PHP 5 时代的mysql_connect()注意没有i这个函数在 PHP 7 就被移除了。解决打开 php.ini搜索mysqli确认extensionmysqli前面没有分号注释。如果用的是下载源码包自带的配置检查phpinfo()页面里是否显示mysqli这一项。老代码里的mysql_*函数没有迁移捷径只能逐个改成mysqli或PDO这也是我坚持推荐这段源码的原因——它从一开始就是mysqli写的。5.5 图片与上传目录权限和路径参数现象前台商品图片裂开后台上传图片提示“无法写入”。原因两类问题各占一半。第一类是图片路径写死成C:/xampp/htdocs/...换目录就全挂正确的做法是统一存相对路径uploads/filename.jpg第二类是uploads/目录权限不够Apache 没权限写入文件。解决项目根目录建uploads/文件夹权限设为 755 或 775然后确认php.ini里upload_max_filesize和post_max_size至少是 2M 以上。后台代码里用move_uploaded_file()时目标路径拼成dirname(__FILE__) . /uploads/ . $fileName用dirname(__FILE__)定位项目绝对路径这样不管项目放在哪个盘都能正确写入。6. 上机前体检一次php -l、报错定位与演示动线6.1 先过语法与错误开关上机演示前的半小时我会强制走一遍体检流程。第一步命令行用php -l检查每个 PHP 文件的语法find . -name *.php -exec php -l {} \;任何文件输出PHP Parse error立刻改掉再查。第二步临时把错误显示打开在db.php顶部加入两行配置error_reporting(E_ALL); ini_set(display_errors, 1);这样页面报错会直接显示在浏览器里定位问题不用翻日志。演示前记得把display_errors改回0否则报错信息里有数据库密码的线索明晃晃挂在屏幕上答辩观感很差。6.2 演示动线一个让答辩不卡壳的顺序我一般建议按这条线演示注册新账号 → 浏览商品列表 → 点击分类筛选 → 加入购物车 → 查看购物车 → 结算下单 → 模拟支付 → 查看订单列表 → 登录后台 → 发货。整个流程是一条线走下来的中间不需要跳来跳去也不会出现“先演示这个等下再演示那个”的断档。这条动线里有三个容易卡壳的节点提前演练第一注册时如果提示用户名已存在是因为初始化数据里已经有test用户换一个用户名就行第二下单后购物车自动清空不要慌这是预期行为第三后台发货入口在admin/order_list.php不是前台页面。6.3 老师会追问的三个点把答案提前背熟老师大概率会问三件事。第一件“你这个密码是怎么存的”答password_hash()生成 bcrypt 哈希数据库里看不到明文登录用password_verify()校验。第二件“如果两个人同时买最后一件商品怎么办”答库存扣减那条UPDATE ... WHERE stock ?的原子操作affected_rows为 0 就是库存不足。第三件“购物车为什么存 Session 不存数据库”答购物车是临时数据Session 跟着会话走关浏览器就销毁不产生垃圾数据如果要支持“用户换设备购物车还在”才需要考虑落库。这三个答案在代码里都有对应实现不是背概念。哪怕只答对其中两个课程设计的演示分基本就稳了。从那次被老师追问“库存超卖怎么办”答不上来之后我每次拿到一套新源码都强制把“并发安全”和“密码存储”这两个点先过一遍再谈功能演示。希望帮到你。本文还有配套的精品资源点击获取