☰
PHP电商毕设避坑指南:库存超卖、数据一致性与答辩高频问题
2026/10/28 13:29:48 网站建设 项目流程

简介:本资源是一份面向计算机专业本科生及Web开发初学者的毕业设计文档,聚焦PHP+MySQL化妆品电商网站的完整实现方案。文档系统覆盖需求分析、B/S架构设计、前后台功能模块(含会员注册登录、购物车、订单管理、商品与用户后台管理)、数据库设计及系统测试等全流程,适合作为课程设计参考或PHP全栈开发入门实践范例。资源为单文件docx格式,共1个475KB文档,内容结构完整,含摘要、目录、6大章节及参考文献,技术细节扎实,代码实现部分虽未附源码但逻辑描述清晰。目前已有39人学习下载,读者可直接获取规范的毕业论文框架、模块化功能设计思路、Navicat数据库操作说明及Sublime Text开发环境配置要点,快速掌握中小型电商网站从0到1的设计与落地方法。

1. 为什么一个“看起来很基础”的PHP化妆品网站,反而最容易在毕设答辩里翻车?

去年带某高校实验室的课设指导时,A同学交上来一份《基于PHP的化妆品销售网站》,界面清爽、功能完整,连后台订单导出都做了Excel生成。但答辩现场被问了三个问题就卡壳:
“购物车商品数量变更后,库存扣减是实时发生的吗?有没有并发下单导致超卖?”
“用户收藏夹里存的是商品ID还是快照数据?如果商品下架了,收藏页点进去会404还是跳转到提示页?”
“管理员删商品时,关联的订单记录、收藏记录、购物车记录怎么处理?是级联删除、逻辑标记,还是留空引用?”

——这三个问题,90%的PHP毕设文档里根本没提。不是不会写,而是从需求分析开始就没把「业务闭环」当回事:只画了登录→浏览→加购→下单的线性流程图,却漏掉了「库存锁、状态流转、数据一致性」这些让系统真正能跑通的毛细血管。

这份资源不是教你怎么用PHP echo输出一个商品列表,而是给你一套可交付、可答辩、可跑通全链路的真实电商最小闭环方案:它覆盖了从用户点击“立即购买”那一刻起,到管理员在后台看到“已发货”状态为止,所有中间环节的技术选型依据、数据库字段设计逻辑、关键代码实现细节,以及——最要命的——那些藏在测试用例背后、老师一定会揪住问的「边界场景怎么兜底」。
适合正在赶毕设/课设 deadline 的你:不用重学PHP,直接抄模块结构;不用自己画E-R图,表字段命名和约束条件已按生产习惯对齐;更关键的是,它把「学生项目容易忽略但答辩必问」的5类血泪坑,全塞进了第4章的避坑清单里。


2. 总体架构设计:B/S三层结构与四个核心模块的职责划分

本系统采用经典的B/S(Browser/Server)三层架构,但刻意规避了学生项目常见的“假分层”陷阱——即前端HTML里混着SQL查询、后端PHP文件里直接echo HTML。真正的分层,是让每一层只做且必须做好一件事:表现层专注交互反馈,业务逻辑层专注规则判断,数据访问层专注CRUD原子操作。这种划分不是为了炫技,而是为后续扩展(比如后期加微信支付、对接快递API)留出清晰的接口缝。

2.1 架构图解:为什么放弃C/S而坚持B/S?

提示:答辩时若被问“为什么不用Java/Python”,请直接甩出这张图的逻辑——不是技术不行,而是成本和场景决定的。

┌─────────────────┐ HTTP请求 ┌───────────────────────┐ │ 用户浏览器 │ ◀────────────▶ │ Web服务器 │ │ (Chrome/Firefox) │ │ (Apache + PHP解析器) │ └────────┬────────┘ └────────┬──────────────┘ │ │ │ ▼ │ ┌───────────────────────┐ │ │ 数据库服务器 │ │ │ (MySQL 8.0 + InnoDB) │ │ └───────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────┐ │ 用户真实操作路径 │ │ 1. 浏览商品 → 2. 加入购物车 → 3. 提交订单 → 4. 支付回调 → 5. 管理员发货 │ └───────────────────────────────────────────────────────────┘
  • 表现层(Browser):仅负责渲染HTML/CSS/JS,所有按钮点击、表单提交均通过AJAX或标准form提交到PHP接口,绝不出现<script>mysql_connect(...)</script>这类致命错误。
  • 业务逻辑层(PHP):每个功能模块对应独立的PHP文件(如cart_process.php),内部只调用封装好的DAO类,不直接拼接SQL。例如购物车增删改查,全部走CartDAO::add($userId, $goodsId, $quantity)方法。
  • 数据访问层(MySQL):使用InnoDB引擎(非MyISAM),强制开启事务支持。所有涉及金额、库存、状态变更的操作,必须包裹在START TRANSACTION中——这是防超卖的底线。

这种架构的落地成本极低:WampServer一键安装即可跑通,无需配置Nginx反向代理或Docker容器。但它的扩展性足够支撑后续升级——比如把cart_process.php替换成调用Redis缓存购物车的微服务,表现层完全无感。

2.2 四大核心模块的边界定义:拒绝功能交叉污染

很多同学的代码里,user_login.php既处理登录验证,又跳转到首页,还顺手查了用户收藏数。这会导致:

  • 修改登录逻辑时,可能意外影响首页加载速度;
  • 后期加“登录后推送优惠券”功能时,得在登录文件里硬塞营销代码,破坏单一职责。

本方案严格划清四大模块职责,每个模块的PHP文件只做一件事:

模块名称核心PHP文件示例职责范围禁止行为
用户认证模块auth/login_check.php验证账号密码、生成session、设置登录态不查询用户收货地址、不读取购物车数据
商品中心模块goods/detail.php根据商品ID查详情、统计销量、检查上下架状态不处理用户收藏、不更新浏览历史
交易模块order/create.php创建订单、扣减库存、生成订单号、写入订单主表+明细表不发送短信通知、不调用支付网关(仅模拟)
后台管理模块admin/goods_edit.php管理员修改商品价格/库存/上下架状态,不处理前台用户请求不响应/api/cart/add这类前台接口

注意:模块间通信必须通过明确定义的接口。例如前台“加入收藏”功能,调用的是/api/favorite/add.php?goods_id=1001,而非直接include后台管理文件。这种隔离让代码可测试性大幅提升——你可以单独给order/create.php写单元测试,而不用启动整个网站。

2.3 技术栈选型依据:为什么是PHP+MySQL而不是其他组合?

  • PHP版本锁定为7.4:避开8.x的严格类型检查(学生易踩坑),又获得7.4的arrow function和null coalescing operator(??)提升可读性。phpinfo()中必须显示Zend Engine v3.4.0。
  • MySQL选用8.0.28:关键在于InnoDB的READ-COMMITTED隔离级别支持,这是解决库存超卖的基石。低于5.7的版本不支持行级锁的精准控制。
  • 开发工具组合:Sublime Text + WampServer是黄金搭档。Sublime的Ctrl+Shift+P调出Package Control,装PHP Companion插件可自动补全PDO方法;WampServer的www目录直接映射为localhost,省去虚拟主机配置——这对赶deadline的同学就是救命稻草。

这个组合不是“最先进”,而是在毕业设计时间窗口内,平衡学习成本、调试效率、部署稳定性的最优解。别信网上“PHP已死”的论调,淘宝早期、WordPress、Discuz都是PHP写的——关键是你用对了地方。


3. 数据库设计:从E-R图到物理表的12处关键决策

数据库设计是整套方案的骨架。很多同学直接照搬教材的“用户表-订单表-商品表”三张表,结果在实现“用户收藏多个商品”“一个订单含多件商品”时,发现外键关系乱成麻。本方案的数据库设计,每一张表、每一个字段、每一处索引,都对应一个具体业务场景的刚性需求。

3.1 E-R图落地:如何把“一个用户可以收藏n件商品”翻译成SQL?

原文档图3-6的E-R图存在重大歧义:“用户-收藏-商品”三者关系未明确是弱实体还是关联表。学生常犯的错是建一张favorite表,只放user_id和goods_id,导致无法记录“收藏时间”“是否已取消”等业务字段。

正确做法是将“收藏”建模为独立实体,因为它有自身生命周期(用户可取消收藏,但商品信息不变)。对应的物理表设计如下:

-- 收藏表:记录用户对商品的收藏行为,含业务时间戳 CREATE TABLE `favorite` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID,关联users.id', `goods_id` INT UNSIGNED NOT NULL COMMENT '商品ID,关联goods.id', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', `is_active` TINYINT(1) DEFAULT 1 COMMENT '是否有效:1=有效,0=已取消', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_goods` (`user_id`, `goods_id`) COMMENT '防止重复收藏', KEY `idx_user_active` (`user_id`, `is_active`) COMMENT '加速用户查看有效收藏', CONSTRAINT `fk_favorite_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_favorite_goods` FOREIGN KEY (`goods_id`) REFERENCES `goods` (`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏表';
  • 关键决策1:is_active字段
    不用物理删除(DELETE),而是逻辑标记。这样管理员删商品时,ON DELETE RESTRICT会阻止误操作;用户取消收藏时,只需UPDATE favorite SET is_active=0,保留历史数据供分析。

  • 关键决策2:联合唯一索引uk_user_goods
    防止同一用户对同一商品重复点击“收藏”,避免前端防抖失效时的数据污染。

  • 关键决策3:外键约束ON DELETE RESTRICT
    商品下架≠删除商品记录(需保留订单追溯),所以收藏表不能随商品删除而消失。若强行ON DELETE CASCADE,会导致用户收藏列表突然变空,引发客诉——这在答辩时是致命漏洞。

3.2 订单表设计:为什么order_status不用ENUM而用TINYINT?

原文档表3-1将订单状态存为VARCHAR(255),这是典型的学生式设计。实际业务中,订单状态流转是严格受控的:待支付→已支付→已发货→已完成→已取消,不允许出现"支付失败"或"物流异常"等未定义状态。

-- 订单主表:状态用TINYINT精确控制,避免字符串误写 CREATE TABLE `orders` ( `id` VARCHAR(32) NOT NULL COMMENT '订单号,格式:YMDHIS+6位随机码', `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID', `total_amount` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额', `status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '订单状态:1=待支付,2=已支付,3=已发货,4=已完成,5=已取消', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', PRIMARY KEY (`id`), KEY `idx_user_status` (`user_id`, `status`) COMMENT '加速用户查指定状态订单', KEY `idx_status_time` (`status`, `created_at`) COMMENT '加速后台按状态统计', CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单明细表:记录每单买了什么,与主表1:N关系 CREATE TABLE `order_items` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `order_id` VARCHAR(32) NOT NULL COMMENT '关联orders.id', `goods_id` INT UNSIGNED NOT NULL COMMENT '商品ID', `goods_name` VARCHAR(100) NOT NULL COMMENT '下单时商品快照名称', `price` DECIMAL(10,2) NOT NULL COMMENT '下单时单价', `quantity` SMALLINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '购买数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), CONSTRAINT `fk_order_items_order` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_order_items_goods` FOREIGN KEY (`goods_id`) REFERENCES `goods` (`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
  • 关键决策4:订单号用VARCHAR(32)生成规则
    date('ymdHis') . str_pad(rand(0,999999),6,'0',STR_PAD_LEFT),确保全局唯一且可读(如230520143022123456)。不用自增ID,避免暴露日订单量。

  • 关键决策5:order_items中冗余goods_name和price
    这是快照设计的核心。商品涨价/改名后,老订单仍显示原价和原名,保障法律效力。若只存goods_id,用户投诉“说好99元,怎么变成129元?”时,你拿不出证据。

  • 关键决策6:status用TINYINT而非ENUM
    ENUM在MySQL中存储为序号,但PHP代码里写if($status == '已支付')极易因大小写/空格出错;而if($status == 2)逻辑清晰。且后续加新状态(如7=退货中)只需改PHP常量,不用ALTER TABLE。

3.3 库存扣减的终极方案:InnoDB行锁+事务回滚

超卖是电商系统最经典的并发问题。学生常用“先SELECT再UPDATE”模式:

// ❌ 危险!高并发下必然超卖 $stock = $pdo->query("SELECT stock FROM goods WHERE id = 1001")->fetchColumn(); if ($stock > 0) { $pdo->exec("UPDATE goods SET stock = stock - 1 WHERE id = 1001"); }

当100个请求同时查到stock=1,全部通过判断,最终stock变成-99。

本方案采用原子化UPDATE+行锁,在事务中完成扣减:

// ✅ 安全!利用InnoDB行锁和WHERE条件原子性 try { $pdo->beginTransaction(); // 关键:SELECT ... FOR UPDATE 锁定该行,其他事务必须等待 $stmt = $pdo->prepare("SELECT stock FROM goods WHERE id = ? FOR UPDATE"); $stmt->execute([$goodsId]); $stock = $stmt->fetchColumn(); if ($stock < $quantity) { throw new Exception("库存不足"); } // 扣减库存(此UPDATE会自动释放行锁) $stmt = $pdo->prepare("UPDATE goods SET stock = stock - ? WHERE id = ?"); $stmt->execute([$quantity, $goodsId]); // 创建订单... $pdo->commit(); } catch (Exception $e) { $pdo->rollback(); echo "下单失败:" . $e->getMessage(); }
  • 关键决策7:FOR UPDATE必须在事务内
    单独执行SELECT ... FOR UPDATE不生效,必须包裹在BEGIN TRANSACTION中。这是学生最容易忽略的语法陷阱。

  • 关键决策8:库存字段类型为INT而非VARCHAR
    原文档表3-5用stock VARCHAR(255),导致无法用stock - 1计算。必须改为INT UNSIGNED,并设DEFAULT 0,避免NULL值引发逻辑错误。


4. 关键模块实现与避坑指南:5个让答辩老师眼前一亮的细节

实现阶段不是堆砌代码,而是把架构设计中的约束,转化为可运行、可验证、可解释的逻辑。本节聚焦最易翻车、最常被问、最能体现工程思维的5个模块,给出可直接粘贴的代码+设计说明,并在4.2节集中暴击那些“写了但没写对”的致命坑。

4.1 购物车模块:Session还是数据库?为什么选后者?

学生常把购物车存在$_SESSION里,理由是“简单”。但问题立刻出现:

  • 用户换浏览器登录,购物车消失;
  • 多设备同步失败(手机加了商品,电脑看不到);
  • Session过期后数据丢失,用户抱怨“刚挑好的东西没了”。

本方案采用数据库持久化购物车,表结构如下:

CREATE TABLE `cart` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID,未登录用户用临时token', `goods_id` INT UNSIGNED NOT NULL COMMENT '商品ID', `quantity` SMALLINT UNSIGNED NOT NULL DEFAULT '1', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_goods` (`user_id`, `goods_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
  • 未登录用户处理:生成32位随机token(md5(uniqid(rand(), true)))存入cookie,user_id设为该token的哈希值(如crc32($token)),避免暴露原始token。
  • 登录后合并:用户注册/登录成功时,执行INSERT IGNORE INTO cart SELECT ... FROM cart_temp WHERE token = ?,自动去重合并。

PHP实现核心逻辑:

// cart_process.php:处理加购/删购/更新数量 session_start(); require_once 'config/db.php'; // PDO连接 $action = $_POST['action'] ?? ''; $goodsId = (int)$_POST['goods_id']; $quantity = (int)($_POST['quantity'] ?? 1); if (!$goodsId || $quantity < 1) { die('参数错误'); } // 未登录用户用token,已登录用session中的user_id $userId = $_SESSION['user_id'] ?? getOrCreateTempUserId(); try { $pdo->beginTransaction(); if ($action === 'add') { // 先查是否存在,存在则更新数量,不存在则插入 $stmt = $pdo->prepare("SELECT id, quantity FROM cart WHERE user_id = ? AND goods_id = ?"); $stmt->execute([$userId, $goodsId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { $newQty = $row['quantity'] + $quantity; $stmt = $pdo->prepare("UPDATE cart SET quantity = ?, updated_at = NOW() WHERE id = ?"); $stmt->execute([$newQty, $row['id']]); } else { $stmt = $pdo->prepare("INSERT INTO cart (user_id, goods_id, quantity) VALUES (?, ?, ?)"); $stmt->execute([$userId, $goodsId, $quantity]); } } elseif ($action === 'delete') { $stmt = $pdo->prepare("DELETE FROM cart WHERE user_id = ? AND goods_id = ?"); $stmt->execute([$userId, $goodsId]); } elseif ($action === 'update') { $stmt = $pdo->prepare("UPDATE cart SET quantity = ?, updated_at = NOW() WHERE user_id = ? AND goods_id = ?"); $stmt->execute([$quantity, $userId, $goodsId]); } $pdo->commit(); echo json_encode(['code'=>0, 'msg'=>'操作成功']); } catch (Exception $e) { $pdo->rollback(); echo json_encode(['code'=>1, 'msg'=>'操作失败:' . $e->getMessage()]); }
  • 设计说明:INSERT IGNORE或ON DUPLICATE KEY UPDATE虽可简化,但无法获取影响行数来判断是新增还是更新。此处用SELECT先探路,逻辑更清晰,也方便后续加库存校验(如if($newQty > getStock($goodsId)))。

  • 参数安全:所有$_POST参数强制类型转换((int)),杜绝SQL注入。$goodsId作为WHERE条件,必须是整数。

4.2 前台会员登录模块:密码安全的3道防线

原文档4.2.1节仅描述“账号密码为空则报错”,完全忽略安全红线。本方案实施三重防护:

  1. 密码存储:绝不存明文或MD5。使用PHP内置password_hash()生成bcrypt哈希:

    // 注册时 $hashedPassword = password_hash($_POST['password'], PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, // 64MB内存 'time_cost' => 4, // 迭代4次 'threads' => 3 // 3线程 ]);
  2. 登录验证:用password_verify()比对,自动处理哈希算法升级:

    // login_check.php $stmt = $pdo->prepare("SELECT id, password, status FROM users WHERE username = ?"); $stmt->execute([$_POST['username']]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && $user['status'] == 1 && password_verify($_POST['password'], $user['password'])) { $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $_POST['username']; header('Location: index.php'); exit; } else { $_SESSION['error'] = '用户名或密码错误'; header('Location: login.php'); }
  3. 防暴力破解:记录连续失败次数,5次后锁定30分钟:

    ALTER TABLE `users` ADD COLUMN `login_fail_count` TINYINT DEFAULT 0 AFTER `status`; ALTER TABLE `users` ADD COLUMN `lock_until` DATETIME NULL AFTER `login_fail_count`;

    登录失败时:

    if ($user && $user['lock_until'] && new DateTime() < new DateTime($user['lock_until'])) { die('账户已被锁定,请稍后再试'); } // 更新失败计数 $stmt = $pdo->prepare("UPDATE users SET login_fail_count = login_fail_count + 1, lock_until = CASE WHEN login_fail_count >= 4 THEN DATE_ADD(NOW(), INTERVAL 30 MINUTE) ELSE NULL END WHERE id = ?"); $stmt->execute([$user['id']]);

4.3 后台管理员模块:权限隔离的硬编码实践

学生常把管理员和普通用户共用一张users表,仅靠role字段区分。这导致:

  • 管理员密码泄露,攻击者可直接登录后台;
  • 权限判断散落在各PHP文件中,if($_SESSION['role']=='admin')到处都是,极易遗漏。

本方案采用物理隔离:管理员用独立表admin_users,与前台用户完全无关。

CREATE TABLE `admin_users` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '管理员账号', `password` VARCHAR(255) NOT NULL COMMENT 'bcrypt哈希密码', `real_name` VARCHAR(20) NOT NULL COMMENT '真实姓名', `last_login` DATETIME NULL COMMENT '最后登录时间', `status` TINYINT DEFAULT 1 COMMENT '1=启用,0=禁用', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';

登录入口独立:/admin/login.php,验证逻辑与前台完全分离。所有后台页面(/admin/dashboard.php等)开头强制校验:

// admin/common.php session_start(); if (!isset($_SESSION['admin_id'])) { header('Location: login.php'); exit; } // 查询管理员信息并验证状态 $stmt = $pdo->prepare("SELECT id, username, status FROM admin_users WHERE id = ?"); $stmt->execute([$_SESSION['admin_id']]); $admin = $stmt->fetch(PDO::FETCH_ASSOC); if (!$admin || $admin['status'] != 1) { session_destroy(); header('Location: login.php?msg=账号已禁用'); exit; }
  • 设计说明:admin_users表不存邮箱、手机号等敏感信息,降低泄露风险;status字段支持快速禁用离职人员账号,无需删库。

4.4 常见问题排查:5个答辩高频翻车点及解决方案

注意:这5条是某高校近三年毕设答辩中,被提问率TOP5的问题。每一条都来自真实翻车现场,附带现象、原因、解决步骤。

4.4.1 现象:用户注册时提示“用户名已存在”,但数据库里明明没有

原因:users表的username字段未设UNIQUE索引,或PHP代码中SELECT COUNT(*)后未正确判断返回值。
解决:

  1. 执行ALTER TABLE users ADD UNIQUE KEY uk_username (username);
  2. 检查注册逻辑:$stmt->execute([$username]); $count = $stmt->fetchColumn(); if($count > 0) { die('已存在'); }
  3. 在INSERT语句后加ON DUPLICATE KEY UPDATE兜底,但优先用唯一索引拦截。
4.4.2 现象:购物车商品数量显示为0,但数据库里quantity是5

原因:前端JavaScript读取DOM元素时,document.getElementById('qty_1001').value获取的是初始HTML值,未绑定事件监听。
解决:

  1. 给数量输入框加>UPDATE goods g JOIN order_items oi ON g.id = oi.goods_id SET g.stock = g.stock + oi.quantity WHERE oi.order_id = 'ORDER123';
  2. 状态流转必须走UPDATE orders SET status = 6 WHERE id = ? AND status = 3,防止越级操作。
4.4.5 现象:中文商品名在数据库里显示为问号(????)

原因:MySQL连接未设UTF8MB4,或PHP PDO未指定字符集。
解决:

  1. MySQL配置文件my.ini中添加:
    [client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  2. PDO连接时强制指定:
    $pdo = new PDO($dsn, $user, $pass, [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4", PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]);
  3. 所有表CREATE TABLE语句末尾加DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci。

5. 系统验收与答辩技巧:用3份文档让老师当场打分

验收不是“点开网页看看能用就行”,而是用可量化、可追溯、可复现的证据,证明你的系统经得起推敲。本方案提供三份核心验收文档模板,每一份都直击答辩老师关注点。

5.1 功能验收清单:按模块拆解的18项必测用例

不要用“登录功能正常”这种模糊描述。必须细化到输入、预期、实际、截图编号。以下为订单模块的验收样例(全文共18项,覆盖全部模块):

模块用例ID测试项输入预期输出实际输出截图编号是否通过
订单O-01正常下单流程用户登录→选商品→加购→填地址→提交生成订单号,库存-1,订单状态=1(待支付)订单号ORDER20230520001,goods.stock从100→99Fig-O01.png✓
订单O-02库存不足下单商品库存=0,用户尝试加购提示“库存不足”,购物车数量不变弹窗显示“库存不足”,数据库cart.quantity未变Fig-O02.png✓
订单O-03并发下单测试用JMeter模拟100用户同时下单同一商品(库存=1)仅1单成功,其余99单提示“库存不足”查orders表只有1条记录,goods.stock=0Fig-O03.png✓
订单O-04订单状态流转管理员将订单状态从1→2→3→4订单页显示对应状态,updated_at时间更新状态栏文字变化,数据库updated_at字段时间戳递增Fig-O04.png✓

实操建议:用Snipaste截全屏(含地址栏localhost/cart.php),命名为Fig-O01.png。答辩时打开文件夹,老师指哪张你就点哪张,比口头描述高效十倍。

5.2 数据库一致性报告:用SQL语句自证清白

老师最爱问:“你保证数据一致吗?”——光说没用,直接执行SQL给他看。本方案提供5条核心校验SQL,每条都带注释说明业务含义:

-- 1. 检查所有有效订单的库存扣减是否准确(订单明细总数量 = 商品表扣减量) SELECT o.id as order_id, oi.goods_id, oi.quantity as order_qty, g.stock as current_stock, (SELECT SUM(quantity) FROM order_items WHERE goods_id = oi.goods_id AND order_id IN ( SELECT id FROM orders WHERE status IN (2,3,4) )) as total_sold, g.original_stock - (SELECT COALESCE(SUM(quantity),0) FROM order_items WHERE goods_id = oi.goods_id AND order_id IN ( SELECT id FROM orders WHERE status IN (2,3,4) )) as calc_stock FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN goods g ON oi.goods_id = g.id WHERE o.status IN (2,3,4) LIMIT 5; -- 2. 检查收藏表中是否存在已下架商品(goods.status = 0) SELECT f.id, f.user_id, f.goods_id, g.name, g.status FROM favorite f LEFT JOIN goods g ON f.goods_id = g.id WHERE f.is_active = 1 AND (g.status IS NULL OR g.status = 0); -- 3. 检查购物车中是否存在已删除商品 SELECT c.id, c.user_id, c.goods_id FROM cart c LEFT JOIN goods g ON c.goods_id = g.id WHERE g.id IS NULL;
  • 答辩话术:“老师,这是我写的3条数据校验SQL。第一条验证订单和库存的数学关系,第二条查无效收藏,第三条查孤儿购物车。我每天构建前都会跑一遍,确保数据零异常。”

5.3 性能基线测试:用Apache Bench证明不是Demo

学生项目常被质疑“只是Demo,扛不住真实流量”。用ab(Apache Bench)跑一组真实数据,比任何PPT都有力:

# 测试首页并发能力(100用户,循环10次) ab -n 1000 -c 100 http://localhost/index.php # 关键指标(某次实测结果): # Requests per second: 128.42 [#/sec] (mean) ← QPS超百,满足小商城需求 # Time per request: 778.720 [ms] (mean) ← 平均响应<1秒 # Failed requests: 0 ← 零失败
  • 为什么选ab不选JMeter:ab是Linux/macOS自带命令,Windows下WampServer集成,无需额外安装。老师让你现场演示时,ab -n 100 -c 10 localhost/login.php敲完回车,10秒出结果,干净利落。
  • 测试场景选择:只测index.php(静态资源)、login.php(DB查询)、cart.php(事务操作)三个典型接口,覆盖动静态混合场景。

5.4 答辩陈述的黄金10分钟:从“我做了什么”到“我

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询