ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP食堂预约订餐系统:从库存设计到并发控制实战

PHP食堂预约订餐系统:从库存设计到并发控制实战 简介一份面向计算机专业学生与系统开发者的食堂预约订餐系统毕业设计文档以PHP技术为主线同时涵盖ASP.NET、SQL Server等备选技术方案。文档从系统开发环境、Web服务器、B/S架构、数据管理系统等基础环节切入完整阐述了用户注册、登录、在线预约订餐、订单管理、菜单管理等功能的设计思路与实现过程并给出数据库实体与表结构设计、管理员模块和用户模块的核心实现。资源还分析了系统在效率、灵活性上的优势以及安全性、数据库维护等现存问题讨论了移动端、云计算、大数据等未来趋势帮助读者在借鉴时更全面地评估技术路线。压缩包内共有1个docx格式文件大小3.03MB章节包括摘要、需求分析、功能设计、数据库实现、主要功能实现等结构清晰便于按需查阅。已有206人学习浏览非常适合作为毕业设计选题参考或相关课程设计模板。1. 食堂预约订餐系统把排队打饭变成到点取餐的完整闭环中午十二点的食堂窗口前排起长队有人刷了卡才发现想吃的菜已经卖完后厨按经验备餐要么剩下大半盆、要么开到一半就见底。这个「基于 php 食堂预约订餐系统设计与实现」的项目本质上不是做一个花哨的点餐页面而是把备餐、下单、取餐三个环节从「猜」变成「算」用户提前选定时段和菜品食堂按预约量精确备餐到点凭单取餐。它覆盖了用户注册登录、菜品与时段管理、预约下单、库存扣减、订单状态流转这些完整的管理系统链路既适合作为毕业设计题目落地也适合食堂信息化改造时先做一个低成本起步方案同时对想完整走一遍 PHP 业务开发的初学者很有练手价值。2. 技术选型与架构设计PHP 8 MySQL 如何撑起预约订餐场景这个项目不涉及高并发抢购也不会有几万人同时在线剁手选技术栈的第一原则是「够用、好维护、容易讲清楚」。PHP 8 配合 MySQL 是这个场景里最顺手的组合PHP 的部署成本低写起来直接MySQL 的事务和行锁能力足够应付食堂预约这种量级的并发再加上 phpstorm 或 vscode 配好 PHP 插件调试体验并不比 Java 差。下面先把需求拆成链路再落表结构和代码目录。2.1 从需求倒推技术栈预约系统的核心链路不是点餐而是库存与状态机很多第一次做这类系统的人会把注意力放在「菜品展示好不好看」上结果做完发现真正难的是另一条链路用户注册登录 → 浏览今日时段 → 选时段选菜 → 提交预约 → 食堂按单备餐 → 用户到点取餐核销 → 订单结束并释放库存。注意这里的关键词是「时段」。食堂订餐和外卖点单最大的区别是食堂需要提前知道某个时段应该备多少份饭。菜品可以天天改但「11:30 到 12:00 这个时段一共卖多少份、还剩多少份」必须有一个准确数字。所以整套系统的数据模型不是围绕「菜品」转而是围绕「时段 库存」转。想清楚这一点技术实现才不会跑偏。这也解释了为什么不用 Redis 做库存也能扛住食堂预约的峰值并发不过几百人同时提交MySQL 的 InnoDB 行锁在这个量级下绰绰有余。真到了每秒上千单的抢饭场景那应该换一套抢购架构而不是用 PHP 写管理后台。先把业务模型做对再谈性能优化。2.2 数据库表设计用户、菜品、时段、订单四类核心表怎么建才不乱我一般会把表拆成六张用户表、菜品表、时段表、时段菜品关联表、订单主表、订单明细表。其中「时段菜品关联表」是最容易被忽略但最关键的一张它决定了每个菜在每个时段还剩多少份也就是库存表本身。下面给出核心表的建表语句CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1 学生用户 2 食堂管理员, created_at DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dish ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(6,2) NOT NULL COMMENT 菜品单价, image VARCHAR(255) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, created_at DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE time_slot ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, slot_date DATE NOT NULL COMMENT 哪一天, slot_time VARCHAR(20) NOT NULL COMMENT 如 11:30-12:00, total_quota INT NOT NULL DEFAULT 0 COMMENT 该时段总配额, remaining_quota INT NOT NULL DEFAULT 0 COMMENT 该时段剩余总配额 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE slot_dish ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, slot_id INT UNSIGNED NOT NULL, dish_id INT UNSIGNED NOT NULL, quota INT NOT NULL DEFAULT 0 COMMENT 该菜在该时段的配额, remaining_quota INT NOT NULL DEFAULT 0 COMMENT 该菜剩余可预订份数, UNIQUE KEY uk_slot_dish (slot_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意remaining_quota这个字段它不是冗余设计而是预约场景的刚需管理员经常需要手工调整某个时段的配额比如今天供应商多送了两斤排骨直接在管理后台给对应时段加配额就可以。如果把剩余量实时SUM出来每次调整都要改业务代码反而麻烦。另一个用心之处是DEFAULT CHARSETutf8mb4这能直接避免后面大概率会遇到的中文乱码问题。订单主表和明细表我放在第 3 章下单代码里一起讲因为订单的状态字段、时间字段和查询索引强相关。这里先记住一个设计原则订单主表只管「谁在哪个时段下的单、总额多少、什么状态」订单明细表存「点了哪几道菜、各几份」两个表用order_id关联不要把所有菜名拼成一个逗号字符串塞进主表——那样做查询和统计时迟早要拆字符串得不偿失。2.3 目录结构与分层不依赖框架的轻量 MVC 怎么摆很多毕设项目一上来就装 Laravel但如果你要答辩时能把流程讲清楚我更建议用轻量自研的 MVC 目录结构核心代码就几百行既没有框架版本兼容问题也能完整体现「设计」过程。项目结构我一般这样安排canteen/ ├── public/ │ ├── index.php # 前端控制器所有请求入口 │ ├── css/ │ └── js/ ├── app/ │ ├── controllers/ # 控制器接收参数并调用模型 │ ├── models/ # 数据库操作与业务逻辑 │ ├── views/ # 模板文件只做展示 │ └── core/ │ ├── Database.php # PDO 单例封装 │ ├── Router.php # 简单路由 │ └── Session.php # 会话封装 ├── config/ │ └── database.php # 数据库配置 └── sql/ └── init.sql # 建表语句和初始数据public目录作为唯一 Web 根目录app和config都放在外面这样浏览器永远访问不到你的业务代码这是一个很重要的安全习惯。core里的Database.php我用 PDO 单例封装所有模型类通过它拿连接避免每个方法里重复new PDO。视图层只负责foreach渲染数据控制器只做参数校验和转发模型层写 SQL 和业务规则各层职责分开之后后面加微信登录、加报表导出都不会把代码搅成一团。路由这块常见做法是写一个简单的Router.php根据?rorder/create这样的参数映射到OrderController::create()不需要引入 composer 包。注意 PHP 内置的$_GET[r]取值时要做好默认值和合法性校验否则r参数被传成../../etc/passwd这类路径会让路由解析直接报错虽然不会真的读到文件但错误信息暴露出来也不好。3. 核心功能落地从注册登录到预约下单的完整代码路径这一章直接上代码。我会按一个真实业务路径走先注册登录再配菜品和时段最后走完一次下单流程。每段代码后面都跟着为什么这么写的说明以及参数怎么改、失败时看哪里。3.1 用户登录与密码安全password_hash 和 PDO 预处理是底线用户模块是整套系统的入口。注册和登录看着简单但有两个底线密码绝不能明文存SQL 绝不能字符串拼接。PHP 从 5.5 开始内置了password_hash和password_verify这是最省事也最正确的做法不需要自己发明加盐算法。下面是一段注册接口的核心逻辑public function register($username, $password, $confirm) { // 基础校验两次密码一致、长度不低于 6 位 if ($password ! $confirm) { throw new Exception(两次输入的密码不一致); } if (mb_strlen($password) 6) { throw new Exception(密码至少 6 位); } $db Database::getInstance(); $stmt $db-prepare( INSERT INTO user (username, password_hash) VALUES (?, ?) ); try { $stmt-execute([$username, password_hash($password, PASSWORD_DEFAULT)]); } catch (PDOException $e) { // 唯一索引冲突的 SQLSTATE 是 23000用这个判断用户名重复 if ($e-getCode() 23000) { throw new Exception(用户名已被注册); } throw $e; } }这段代码里有三个值得说的点。第一password_hash($password, PASSWORD_DEFAULT)会自动生成随机盐哈希结果里已经包含了盐和算法标识校验时只需要password_verify($password, $hash)一行不用自己维护盐字段。第二prepare execute是 PDO 的预处理方式用户名里的单引号、反斜杠都会被安全处理这是防 SQL 注入的根本手段。第三判断用户名重复用的是捕获唯一索引冲突而不是先SELECT再INSERT因为先查再插在并发下必然有竞态窗口两次同时注册同一个用户名时后者会插入失败靠捕获异常才是可靠做法。登录成功后的会话处理也有讲究。登录成功后我建议用session_regenerate_id(true)重新生成 session id防止会话固定攻击同时把user_id和role写进 session不要存密码哈希。每个管理后台页面的入口处判断$_SESSION[role] ! 2就跳转回登录页这样权限控制的代码只需要写一遍放在核心控制器的构造函数里即可。3.2 菜品与时段管理管理员后台的上架、下架与配额设置管理员模块的核心操作是维护菜品和时段以及设置时段内的菜品配额。菜品本身是简单的增删改查真正的业务在「配额联动」上新建时段时默认每个上架菜品分配一个初始配额修改菜品上下架状态时对应的时段菜品记录要么停售、要么恢复可订。下面以上架菜品为例public function toggleDishStatus($dishId, $status) { $db Database::getInstance(); $db-beginTransaction(); try { // 更新菜品本身的状态 $stmt $db-prepare(UPDATE dish SET status ? WHERE id ?); $stmt-execute([$status, $dishId]); if ($status 1) { // 上架时为今天之后的每个时段生成默认配额 $stmt $db-prepare( INSERT INTO slot_dish (slot_id, dish_id, quota, remaining_quota) SELECT id, ?, 50, 50 FROM time_slot WHERE slot_date CURRENT_DATE() AND NOT EXISTS ( SELECT 1 FROM slot_dish sd WHERE sd.slot_id time_slot.id AND sd.dish_id ? ) ); $stmt-execute([$dishId, $dishId]); } $db-commit(); } catch (Exception $e) { $db-rollBack(); throw $e; } }这段代码的关键点是NOT EXISTS子查询它保证重复点上架按钮不会给同一时段插入重复的菜品配额记录这就是幂等处理的一种。默认配额 50 只是示例值实际生产环境应该把这个数字做成管理后台里的一个输入框因为不同菜品的受欢迎程度差异很大红烧肉配 100 份和清炒时蔬配 30 份才是合理的。管理后台还有一个容易被忽略的坑时段过期之后管理员应该不能在过去的时段里改配额。最简单的做法是在更新slot_dish的 SQL 里加一个条件slot_date CURRENT_DATE()让数据库层面就把过期时段的写操作挡掉不要只靠前端隐藏按钮因为直接提交表单是可以绕过页面限制的。3.3 预约下单核心代码事务、行锁和唯一订单号一个都不能少下单是整套系统里技术含量最高的地方也是并发问题集中爆发的环节。用户点完菜提交预约后端要做的事情是检查时段和菜品的剩余配额、计算总价、扣减库存、生成订单。这几步必须在一个数据库事务里完成任何一个环节失败都要回滚。下面给出核心方法public function createOrder($userId, $slotId, $items) { $db Database::getInstance(); $db-beginTransaction(); try { // 1. 锁定时段总配额行避免并发下同一时段超卖 $stmt $db-prepare( SELECT remaining_quota FROM time_slot WHERE id ? FOR UPDATE ); $stmt-execute([$slotId]); $slot $stmt-fetch(PDO::FETCH_ASSOC); if (!$slot) { throw new Exception(预约时段不存在); } // 2. 逐道菜检查并锁定库存行 $totalQty 0; $totalPrice 0; foreach ($items as $item) { $stmt $db-prepare( SELECT dish_id, price, remaining_quota FROM slot_dish WHERE slot_id ? AND dish_id ? FOR UPDATE ); $stmt-execute([$slotId, $item[dish_id]]); $sd $stmt-fetch(PDO::FETCH_ASSOC); if (!$sd || $sd[remaining_quota] $item[qty]) { throw new Exception(菜品「 . $item[dish_name] . 」剩余数量不足); } $totalQty $item[qty]; $totalPrice $sd[price] * $item[qty]; } // 3. 扣减时段总剩余和每个菜的剩余 $stmt $db-prepare( UPDATE time_slot SET remaining_quota remaining_quota - ? WHERE id ? ); $stmt-execute([$totalQty, $slotId]); foreach ($items as $item) { $stmt $db-prepare( UPDATE slot_dish SET remaining_quota remaining_quota - ? WHERE slot_id ? AND dish_id ? ); $stmt-execute([$item[qty], $slotId, $item[dish_id]]); } // 4. 生成订单主表和明细表 $orderNo $this-generateOrderNo(); $stmt $db-prepare( INSERT INTO orders (order_no, user_id, slot_id, total_qty, total_price, status) VALUES (?, ?, ?, ?, ?, 1) ); $stmt-execute([$orderNo, $userId, $slotId, $totalQty, $totalPrice]); $orderId $db-lastInsertId(); foreach ($items as $item) { $stmt $db-prepare( INSERT INTO order_item (order_id, dish_id, qty, price) VALUES (?, ?, ?, ?) ); $stmt-execute([$orderId, $item[dish_id], $item[qty], $item[price]]); } $db-commit(); return [order_no $orderNo, total_price $totalPrice]; } catch (Exception $e) { $db-rollBack(); throw $e; } }SELECT ... FOR UPDATE是 InnoDB 的行级悲观锁在事务内锁定查询到的行其他事务对这些行的写操作必须等当前事务提交或回滚后才能执行。这里先锁time_slot行再逐个锁slot_dish行能保证两个并发请求同时下单时后一个请求必须等前一个提交后才能读到最新剩余数从根上杜绝了超卖。throw new Exception一旦触发事务回滚前面锁住的行和已经做的扣减全部撤销不会留下「扣了库存但没生成订单」的半截数据。订单号生成我这里用简单的时间戳加随机数date(YmdHis) . str_pad(mt_rand(1, 999999), 6, 0, STR_PAD_LEFT)配合orders.order_no的唯一索引即使同一秒内有两个请求生成了相同随机数后插入的也会因唯一约束失败而报错不会产生重复订单。如果你想让订单号更专业可以加上用户 id 后几位但核心是数据库唯一索引兜底而不是代码里祈祷不重复。3.4 订单状态机四种状态之间的流转规则和 CAS 更新订单状态我定为四档1 已预约、2 已取餐、3 已取消、4 已过期。用户在支付环节如果系统没有接入真实支付常见做法是「提交即预约」不再额外设计支付闭包如果一定要演示支付流程可以加一档「0 待支付」用户提交后 15 分钟内不确认就自动释放库存。状态机的核心约束是「状态只允许按固定路径流转」比如已取消的订单不能直接变成已取餐。实现上用条件更新比先查后改更安全public function transition($orderId, $userId, $fromStatus, $toStatus) { $stmt $this-db-prepare( UPDATE orders SET status ? WHERE id ? AND user_id ? AND status ? ); $stmt-execute([$toStatus, $orderId, $userId, $fromStatus]); if ($stmt-rowCount() ! 1) { throw new Exception(订单状态已变化请刷新后重试); } return true; }这个写法叫 CASCompare And Swap把「检查当前状态再更新」合并成一条UPDATE语句数据库层面保证只有当前状态确实是$fromStatus时才更新成$toStatus。比先SELECT status再UPDATE可靠得多因为两者之间如果有另一个请求把状态改了先查后改的代码会覆盖掉对方的结果。在控制器调用时取餐核销只允许管理员操作transition($orderId, $userId, 1, 2)管理员传的$userId应该是当前登录管理员 id配合角色校验用户取消订单走transition($orderId, $userId, 1, 3)并且取消后要立即把扣掉的库存加回去这一步也要放在同一个事务里否则会出现「订单取消了但库存没恢复」的脏数据。4. 并发与数据一致性订餐库存不超卖、订单不重复的关键代码第三章的下单代码里已经用了悲观锁这一章集中讲并发场景的几种处理方案和取舍因为这是答辩时最容易被追问的部分也是项目上线后最现实的翻车点。很多人的系统单机测试全部正常一到中午十二点同时几十个人提交订单就出现库存超卖或者重复订单问题基本都出在这一章的内容上。4.1 超卖问题的两种解法悲观锁与乐观锁的取舍超卖的本质是「检查剩余量」和「扣减剩余量」两个动作之间存在时间窗口多个请求同时通过了检查。第三章的FOR UPDATE是悲观锁思路拿锁期间其他事务阻塞还有一个更轻量的方案是乐观锁直接在一个UPDATE语句里用条件限制剩余量必须大于本次购买量UPDATE slot_dish SET remaining_quota remaining_quota - 2 WHERE slot_id 3 AND dish_id 7 AND remaining_quota 2这条 SQL 执行后受影响行数为 1 说明扣减成功为 0 说明库存不足不需要提前SELECT也不需要事务锁。它的优点是并发高时不阻塞读、不长时间占用行锁缺点是如果扣减成功你没拿到更新前的旧值需要再查一次或者用RETURNING类语法才能知道最新剩余数业务逻辑上稍微绕一点。选哪种我一般这样判断食堂预约场景用户提交的峰值并发通常不会超过每秒 50 次悲观锁的阻塞时间只有毫秒级体验无感知而且代码里可以先「锁行、检查、扣减、生成订单」四步走逻辑直白、答辩好讲所以我推荐用悲观锁。如果你是给校外餐厅做预约预计同一时段可能有上千人抢菜那就改用乐观锁方案把UPDATE ... WHERE remaining_quota ?放在事务开头失败就让用户重新选择数量。没有银弹看你兜底能承受什么复杂度。4.2 重复下单与幂等设计前端防抖不如后端约束可靠重复下单有两种常见触发方式用户手快连点两次提交按钮或者前端请求超时后用户刷新页面再次提交。前端做个「提交后按钮置灰」的小交互能挡住大部分情况但按下回车刷新浏览器、用脚本直接 POST 接口这些场景前端管不住必须在后端做幂等。我常用的方案是下单前生成一个一次性 token存在服务端并限制只能使用一次// 提交订单页面初始化时生成 token public function initToken() { $token bin2hex(random_bytes(16)); $_SESSION[order_token] $token; echo input typehidden nametoken value . $token . ; } // 下单时校验并立即销毁 token public function verifyToken($inputToken) { if (empty($_SESSION[order_token]) || $inputToken ! $_SESSION[order_token]) { throw new Exception(页面已过期请返回重新下单); } unset($_SESSION[order_token]); // 销毁后再次提交必然失败 }这个方案的巧妙之处在于unset的时机第一次请求校验通过后 token 立即作废第二个并发请求进来时 session 里已经没这个值了所以无论用户点击多少次数据库中只会生成一个订单。要注意random_bytes(16)生成的 token 不能用$_COOKIE存必须放服务端 session否则攻击者可以直接把 token 拿出来重放。如果不想动 session还有一个数据库层的兜底在orders表给(user_id, slot_id, status1)加一个「未完成订单唯一」约束太复杂因为状态会变我一般更推荐配合 4.3 的定时清理逻辑用「查该用户该时段是否存在未取消订单 订单号唯一索引」双重保证代价是并发高峰会多一次查询但逻辑上更清晰。4.3 过期订单自动释放库存cron 脚本与防重复释放标记预约订单不可能永远挂在「已预约」状态用户可能忘了取餐也可能预约了但不想去了却没有点取消。如果这些订单不处理库存会被越占越少时间一长整个系统的配额就堵死了。常见做法是写一个 PHP CLI 脚本由系统定时任务每分钟执行一次找出超时未取餐的订单并自动流转状态、释放库存。// cron/release_expired.php require __DIR__ . /../app/bootstrap.php; $db Database::getInstance(); // 找出所有超时未取餐的订单超过取餐时段结束时间 30 分钟仍为已预约状态 $orders $db-query( SELECT o.id, o.slot_id FROM orders o JOIN time_slot ts ON o.slot_id ts.id WHERE o.status 1 AND (ts.slot_date CURRENT_DATE() OR (ts.slot_date CURRENT_DATE() AND ts.slot_time SUBTIME(CURRENT_TIME(), 00:30:00))) )-fetchAll(PDO::FETCH_ASSOC); $updateOrder $db-prepare(UPDATE orders SET status 4 WHERE id ? AND status 1); $returnSlot $db-prepare( UPDATE time_slot SET remaining_quota remaining_quota ? WHERE id ? ); foreach ($orders as $order) { // 将超时订单从状态 1 流转为状态 4已过期 if ($updateOrder-execute([$order[id]]) 1) { // 只有状态确实被改变才恢复时段总配额 $returnSlot-execute([1, $order[slot_id]]); } }这条脚本里最关键的是最后两个execute的顺序和条件先更新订单状态只有rowCount()为 1说明订单确实还处于已预约状态才恢复库存。这能防止脚本被重复执行时把同一份库存恢复两次造成库存虚增。如果你把库存细化到了每道菜slot_dish脚本里还要把每个订单明细里的菜品数量加回去逻辑一样但要额外查一遍order_item表这里为了篇幅只演示了时段总配额的恢复。定时任务用 crontab 注册每分钟执行一次*/1 * * * * /usr/bin/php /var/www/canteen/cron/release_expired.php /var/log/canteen_expire.log 21注意要把脚本路径写成绝对路径并确认/usr/bin/php的版本和你本地一致否则脚本里用了random_bytes、match等新语法时老版本 PHP 会直接报错。脚本执行日志要落盘排查问题时第一件事就是看这个日志里有没有报错。5. 避坑指南PHP 订餐系统跑起来之后的 5 个高频问题再好的设计与实现部署运行后都会遇到一些不看代码完全想不通的怪问题。这一章我按「现象 → 原因 → 解决」的格式把做这个系统最常见的 5 个坑列出来都是我反复帮人排查时撞到的案例。5.1 中文乱码页面、数据库、连接三层字符集不一致现象数据库里存的中文正常页面上显示却是问号或者一堆乱码反过来页面表单提交的中文存进数据库后变成???。原因字符集不一致。最常见的是数据库表用了latin1或utf8注意utf8在 MySQL 里是残缺的最多只能存 3 字节emoji 直接崩溃而 PHP 页面输出声明了utf8两边一交汇就乱。解决建表统一用utf8mb4PDO 连接串里显式加上charsetutf8mb4页面 HTML 的meta charsetutf-8不能省。三步都做到位乱码问题基本绝迹。我在第 2 章的建表语句里特意带上字符集就是为了这个。5.2 预约时间错乱时区配置让订单差八小时现象用户在今天 23:30 提交预约后台查到的created_at显示是明天 07:30或者管理员设置明天中午的时段用户端看到时段列表为空。原因PHP 默认时区是 UTCMySQL 的CURRENT_DATE()也随会话时区。代码里date(Y-m-d)生成的是 UTC 时间和北京时间差了 8 小时跨天边界最容易出问题。解决PHP 侧在入口文件统一设置date_default_timezone_set(Asia/Shanghai)MySQL 侧在连接后执行SET time_zone 08:00。我建议所有时间字段的业务逻辑都在应用层统一用同一时区生成数据库只负责存储不要把CURRENT_TIMESTAMP默认值当成万能药。5.3 库存被扣成负数事务没开或锁没用对现象并发测试时把某个菜品的remaining_quota扣成了 -3订单明细却只有 2 条。原因下单代码只做了SELECT检查库存然后直接UPDATE扣减两个动作之间没有事务也没有行锁。两个请求同时读到剩余数 1都认为自己能扣结果变成 -1。解决回看第 3.3 节的下单方法beginTransaction必须在检查库存之前且slot_dish的查询必须带FOR UPDATE。这是这个项目里最容易翻车的并发点没有之一。测试时用ab -n 50 -c 20并发打下单接口打完再查库存负数就一定跑不掉。5.4 图片上传成功却访问不了目录权限与入口路径的坑现象管理员上传了菜品图片后台列表能看到路径但浏览器打开图片地址返回 404或者上传时报「不能写入目录」。原因两种可能。第一public/uploads目录没有写权限PHP 进程无法写入第二上传目录不在 Web 根目录内或者 Nginx 的root配置指向了错误的路径导致保存地址和访问地址不一致。解决上传目录放在public/uploads下设置 755 权限属主改成 php-fpm 的运行用户通常是www-data图片 URL 统一用相对 Web 根目录的路径存库展示时拼接成/uploads/xxx.jpg。如果你用宝塔之类的面板还要确认站点配置里的root指向的是public而不是项目外层目录。5.5 会话莫名失效session 配置与浏览器兼容的坑现象本地测试没问题部署到服务器后用户登录状态一会儿就掉或者同一台电脑换个浏览器登录状态不互通。原因session 问题按两个方向排查。第一PHP 的session.gc_maxlifetime默认 1440 秒24 分钟用户挂页面超过这个时间再操作就会被强制重新登录第二session.cookie_path和session.cookie_domain配置不对时不同子路径或跨域请求拿不到 session cookie。如果你在前后端分离或者嵌入微信网页容器里调试跨浏览器兼容的关键就是 cookie 属性和SameSite设置要正确。解决延长session.gc_maxlifetime到 7200 并同时调大 MySQL 里 session 表如果用了数据库 session 驱动的过期时间cookie 的SameSite设为Lax而不是None除非真有跨站需求。这里用得上一个排查思路登录后直接在浏览器开发者工具里看Cookie里有没有PHPSESSID有没有Secure或Domain标记异常比漫无目的地改代码快得多。6. 上线前的最后一步压测、部署与值得投入的优化方向系统功能全部跑通之后我习惯先做两件事再谈上线压测和部署配置。压测不是为了证明系统能抗住多少并发而是为了找出瓶颈在哪里部署配置才是决定系统长期稳定性的关键。6.1 用 ab 压测看真实并发能力Apache 自带的ab工具是最直接的压测手段不需要额外装 JMeterab -n 500 -c 20 http://your-server.com/public/index.php?rorder/list-n 500表示总请求数 500-c 20表示并发数 20。跑完后重点看两个指标Failed requests是否为 0Requests per second是多少。如果失败率高要么是 PHP-FPM 进程数不够要么是代码里有死锁。我在下单接口压测时会临时把库存加到足够大然后用 50 并发去打打完立即查数据库核对库存总数和订单数是否对得上这是验证并发代码最直接的办法。6.2 Nginx PHP-FPM 部署要点生产环境推荐 Nginx PHP-FPM不要用 Apache 的mod_php性能差距明显而且进程隔离差。Nginx 站点配置里有两处最关键server { listen 80; server_name canteen.example.com; root /var/www/canteen/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~* \.(jpg|jpeg|png|gif|css|js)$ { expires 7d; access_log off; } }第一处是try_files $uri $uri/ /index.php?$query_string它把不存在的路径全部交给前端控制器处理这样路由才生效第二处是SCRIPT_FILENAME必须写成$document_root$fastcgi_script_name手滑写成$request_filename或者丢了$document_root会造成 PHP 文件无法解析页面直接下载源码或者白屏这是我见过最多的一类部署翻车。6.3 值得投入的优化方向预约订餐系统跑顺之后优先做这三件事给orders表加好索引我建表时已经加了idx_user_status和idx_slot_status上线前多压测以及引入一个简单的数据统计页让食堂管理员能直接看到每个时段的预约量曲线。不要一上来就上 Redis、上队列这个业务量级用不上反而会把系统复杂度推到无法维护的地步。我个人的习惯是每完成一个模块就先写一小段验证脚本记录当时的响应耗时攒下来能看到每次改动是让系统变快还是变慢而不是凭感觉说「应该优化好了」。这算是我做这类管理系统最值钱的一条经验希望帮到你。本文还有配套的精品资源点击获取
返回列表