ARTICLE DETAIL

资讯详情

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

基于PHP的食堂预约订餐系统:数据库设计与并发控制实战

基于PHP的食堂预约订餐系统:数据库设计与并发控制实战 简介这是一份基于PHP的食堂预约订餐系统毕业设计文档面向计算机相关专业学生与毕设开发者用于解决食堂高峰期排队拥挤、管理效率低等实际问题。文档系统梳理了开发环境、Web服务器、B/S架构、数据管理系统以及PHP技术等关键环节完整说明了用户注册登录、预约订餐、订单管理、菜单管理等核心功能的设计与实现并对技术选型、系统优缺点、应用前景及发展趋势进行了分析。包体为1个docx文件大小3.03MB内含摘要、目录、需求分析、数据库设计、模块实现等标准论文章节结构清晰且便于参考和二次修改。目前已有205人学习适合需要完成同类型毕业设计或快速搭建在线订餐系统的人群。1. PHP食堂预约订餐系统毕业设计代码好找能跑通才是真本事很多选PHP方向做毕业设计的人一搜就是“食堂预约订餐系统”下载下来却发现要么是ThinkPHP老版本跑不起来要么数据库脚本和代码对不上要么预约逻辑只是一个“假表单”——提交完订单就完事根本没有时段配额和冲突检查。这个基于PHP的食堂预约订餐系统属于典型的PHP毕业设计项目功能链路是完整的用户注册登录、查看菜谱、按时段预约、提交订单、后台管理员确认外加用户管理、菜品管理和订单管理。它适合三类人拿它当毕设底子改改界面和字段的本科生、想练原生PHP写完整业务流的初学者、以及需要一套能快速演示的后台管理系统的开发者。我拆这套系统的结论先放在这里它看着是增删改查但最容易翻车的地方全在“预约”两个字上——时间冲突、重复下单、数量超卖、状态机错乱。这篇文章就把整个系统的数据库设计、核心流程、部署步骤和踩坑记录拆开直接对着代码讲。2. 系统架构与数据库设计预约订餐的核心是时段表与订单状态机2.1 技术选型这套系统用原生PHP而不是框架是好事也是坑这套系统用的是原生PHP MySQL前端是Bootstrap或类似的后台模板。为什么毕业设计普遍选这个组合因为不依赖Composer、不依赖复杂的路由配置PHPStudy或XAMPP起一个Apache就能跑演示的时候不容易出环境事故。相比ThinkPHP或Laravel原生PHP的好处是每个文件职责清晰users.php管用户、order.php管订单答辩时能直接指着一个文件讲逻辑。但原生PHP的坑同样明显没有ORMSQL和HTML混在一个文件里的情况很常见没有框架自带的CSRF防护和路由过滤更重要的是PHP版本兼容问题——很多下载来的代码还写着mysql_connect()这种PHP 5时代的函数在PHP 7.4以上直接Fatal Error。所以拿到资源第一步不是改功能而是把mysql_*系列全部替换成mysqli或PDO这也是我下面所有示例代码统一用mysqli的原因。2.2 数据表设计四张核心表解决预约全流程拆这个系统的数据库核心就四张表用户表、菜品表、时段表、订单表。很多简化版直接不设时段表把就餐时间写死在订单里那样做管理员没法配置每日的预约名额换一天就得改代码。正确的设计是把“时段”抽成独立表订单通过period_id关联时段。-- 时段表控制每天几个就餐时段、每个时段多少人 CREATE TABLE period ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 时段名称如午餐A段, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, quota int(11) NOT NULL DEFAULT 50 COMMENT 该时段可预约总人数, reserved int(11) NOT NULL DEFAULT 0 COMMENT 已预约人数, date date NOT NULL COMMENT 有效的就餐日期, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表状态字段是核心 CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, period_id int(11) NOT NULL, dish_ids text NOT NULL COMMENT 菜品ID列表JSON格式存, total_amount decimal(10,2) NOT NULL DEFAULT 0.00, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, remark varchar(255) DEFAULT NULL COMMENT 用户备注如忌口, create_time datetime NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), KEY period_id (period_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;period表里quota和reserved两个字段是关键quota是上限reserved是已占用的名额。用户点击预约时先查reserved quota条件满足才允许下单。dish_ids直接用JSON存比如[{id:3,num:1},{id:7,num:2}]这样就少了一张订单明细表毕设场景下完全够用查询时用json_decode处理即可。这里有个设计取舍要说明为什么不把菜品和订单拆成多对多关联表因为拆了之后订单列表页每次都要三表联查对于食堂订餐这种“菜品种类固定、每单数量不多”的场景JSON存储的查询开销更小代码更短。缺点是如果想统计“小龙虾卖了多少份”得遍历订单的JSON不能写SQL聚合。毕设和中小型食堂系统前者节省的开发时间更划算。2.3 订单状态机没有状态流转的预约系统就是假系统“预约订餐”不是提交完就结束。食堂工作人员要提前确认订单——是接受还是驳回用户可能取消订单就餐完成后要标记完成这些都需要状态字段去承载。下面这张表是这套系统里状态流转的完整定义状态数值含义允许的下一步触发方待确认0用户已提交食堂未处理已确认 / 已取消管理员 / 食堂窗口已确认1食堂接纳预约已完成 / 已取消就餐时核销已完成2已就餐流程结束无管理员手动核销已取消3订单作废无用户或管理员状态机写进业务代码时要禁止“跳变”——比如用户不能把已完成的订单改成已取消管理员不能把已取消的订单改回已确认。我在代码里会写一个简单的状态检查函数更新订单前先判断当前状态是否允许目标状态下面第3章会给出具体示例。3. 核心功能实现预约、点餐、后台确认的完整代码流程3.1 登录与会话控制用session管理角色避免每页重复写权限判断这套系统的登录逻辑不复杂但有一个常见的低级错误登录成功后只存user_id不存role导致每个页面都要查一遍数据库才能判断是普通用户还是管理员。我一般会在登录成功时把用户信息完整写入session后续页面直接用$_SESSION[role]做权限判断。?php session_start(); require_once config.php; // 里面定义 DB_HOST/DB_USER/DB_PASS/DB_NAME建立mysqli连接 if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username]); $password trim($_POST[password]); // 用预处理语句防SQL注入 $stmt $conn-prepare(SELECT id, username, password, role FROM user WHERE username ?); $stmt-bind_param(s, $username); $stmt-execute(); $result $stmt-get_result(); $user $result-fetch_assoc(); if ($user password_verify($password, $user[password])) { // 登录失败次数清零 unset($_SESSION[login_attempts]); $_SESSION[user_id] $user[id]; $_SESSION[username] $user[username]; $_SESSION[role] $user[role]; // admin 或 user header(Location: user_index.php); exit; } else { // 简单的防暴力破解记录失败次数超过5次锁定15分钟 $_SESSION[login_attempts] ($_SESSION[login_attempts] ?? 0) 1; if ($_SESSION[login_attempts] 5) { $_SESSION[lock_until] time() 900; } $error 用户名或密码错误; } } ?密码不能是明文要用password_hash()生成验证用password_verify()这是PHP 5.5之后内置的比md5加盐安全得多。login_attempts锁定机制是可选的但答辩时提到这个老师会觉得你考虑了真实场景的安全问题。$_SESSION[login_attempts] ($_SESSION[login_attempts] ?? 0) 1用了PHP 7的新语法如果是PHP 5环境要改成先判断再赋值。3.2 预约流程先查配额再下单两步放在一个事务里预约是这套系统最核心的流程。用户选择一个就餐时段系统要同时做三件事检查时段是否有效、检查剩余名额、扣减名额并生成订单。这三件事如果分开做就会出现超卖——两个用户同时看到剩1个名额同时下单结果名额变成-1。解决方法是把“扣减名额插入订单”放到数据库事务里并且用SELECT ... FOR UPDATE锁住时段记录。?php session_start(); require_once config.php; if (!isset($_SESSION[user_id])) die(未登录); $user_id $_SESSION[user_id]; $period_id intval($_POST[period_id]); // 接收菜品JSON形如 [{id:3,num:1}] $dishes json_decode($_POST[dishes], true); if (!$dishes || count($dishes) 0) { die(请至少选择一种菜品); } $conn-begin_transaction(); try { // 锁定该时段记录防止并发超卖 $sql SELECT id, quota, reserved, date, start_time, end_time FROM period WHERE id ? AND date CURDATE() FOR UPDATE; $stmt $conn-prepare($sql); $stmt-bind_param(i, $period_id); $stmt-execute(); $period $stmt-get_result()-fetch_assoc(); if (!$period) throw new Exception(时段不存在或已过期); if ($period[reserved] $period[quota]) throw new Exception(该时段已约满); // 计算金额 $total 0; foreach ($dishes as $d) { $dish_id intval($d[id]); $num intval($d[num]); $dish_sql SELECT price FROM dish WHERE id ?; $ds $conn-prepare($dish_sql); $ds-bind_param(i, $dish_id); $ds-execute(); $dish $ds-get_result()-fetch_assoc(); if (!$dish) throw new Exception(菜品不存在); $total $dish[price] * $num; } // 扣减名额 生成订单同一事务 $update UPDATE period SET reserved reserved 1 WHERE id ?; $up $conn-prepare($update); $up-bind_param(i, $period_id); $up-execute(); $order_sql INSERT INTO orders (user_id, period_id, dish_ids, total_amount, status, create_time) VALUES (?, ?, ?, ?, 0, NOW()); $os $conn-prepare($order_sql); $os-bind_param(iisss, $user_id, $period_id, json_encode($dishes), $total); $os-execute(); $conn-commit(); echo 预约成功订单编号 . $conn-insert_id; } catch (Exception $e) { $conn-rollback(); // 注意这里返回给用户的错误信息不要带SQL细节防信息泄露 echo 预约失败 . $e-getMessage(); } ?这段代码里有几个要点。第一FOR UPDATE是MySQL InnoDB的行级锁两个并发请求同时进来后者会阻塞在SELECT上等前者事务提交后才继续这时它读到的reserved已经是扣减后的值自然能判断是否约满。第二begin_transaction()一定要和try...catch配合任何一步异常都要rollback否则数据就脏了。第三菜品价格不信任前端传的金额而是从数据库里查出来重新计算防止有人改POST数据把价格改成0.01元。3.3 后台管理管理员确认订单时的状态校验和名额回退管理员端的功能集中在“订单管理”页面——列出所有预约、确认、拒绝、完成。这里最容易出bug的是“拒绝订单后时段的reserved名额要不要回退”。答案是要而且必须和状态更新放在一个事务里。如果不回退这个时段的名额会越来越少最后所有用户都约不上。?php session_start(); require_once config.php; if ($_SESSION[role] ! admin) die(无权访问); $order_id intval($_POST[order_id]); $action $_POST[action]; // confirm / cancel / complete $conn-begin_transaction(); try { // 查订单当前状态和关联时段 $sql SELECT o.id, o.status, o.period_id, p.reserved FROM orders o JOIN period p ON o.period_id p.id WHERE o.id ? FOR UPDATE; $stmt $conn-prepare($sql); $stmt-bind_param(i, $order_id); $stmt-execute(); $order $stmt-get_result()-fetch_assoc(); if (!$order) throw new Exception(订单不存在); $new_status 0; if ($action confirm) { if ($order[status] ! 0) throw new Exception(只有待确认订单才能确认); $new_status 1; } elseif ($action cancel) { // 如果原本是待确认或已确认取消防窗外退名额 if ($order[status] 1) throw new Exception(该订单不能取消); $new_status 3; $upd UPDATE period SET reserved GREATEST(reserved - 1, 0) WHERE id ?; $up $conn-prepare($upd); $up-bind_param(i, $order[period_id]); $up-execute(); } elseif ($action complete) { if ($order[status] ! 1) throw new Exception(只有已确认订单才能核销); $new_status 2; } $upd_order UPDATE orders SET status ? WHERE id ?; $uo $conn-prepare($upd_order); $uo-bind_param(ii, $new_status, $order_id); $uo-execute(); $conn-commit(); echo 操作成功; } catch (Exception $e) { $conn-rollback(); echo 操作失败 . $e-getMessage(); } ?GREATEST(reserved - 1, 0)这个写法是防止reserved被减成负数——正常情况下不会发生但万一有脏数据或者并发边缘情况这个函数能兜底。整套后台逻辑的核心就是“状态判断在前数据变更在后”每次更新前都查一次现有状态防止用户通过直接构造HTTP请求绕过前端按钮来乱改订单状态。4. 避坑与常见问题这套系统最容易翻车的六个地方4.1 时间处理预约日期和服务器时区不一致现象用户在前端选的日期是“明天”下单却选不成提示“时段不存在或已过期”或者管理员看到的订单日期和用户下单的日期差了一天。原因PHP默认时区是UTC或服务器的设定时区CURDATE()和date(Y-m-d)返回的不是北京时间和前端时间对不上。解决在config.php顶部统一设置时区date_default_timezone_set(Asia/Shanghai)数据库连接后执行SET time_zone 08:00。两层都设防止PHP层和MySQL层各用各的时区。4.2 并发预约同样的名额被抢超现象内网测了100个并发预约明明设置了50个名额实际生成订单超过50单。原因直接用“先SELECT查reserved再UPDATE”两步走中间没有加锁也没有事务两个请求同时读到reserved49同时更新成50名额就被超了。解决必须用SELECT ... FOR UPDATE把订单生成和名额扣减合并成一个事务这块代码在第3章已经给了完整实现。测试时可以用Apache的ab命令模拟并发ab -n 100 -c 20 http://localhost/order.php看最终订单数是否超过quota。4.3 拒绝或取消订单后名额不回退现象管理员取消了一笔预约但该时段仍然显示约满用户无法再约。原因后台更新订单状态时没有同步把period.reserved减回去。解决第3.3节的代码已经处理了。需要注意回退时机用户在“已确认”状态取消要回退管理员拒绝“待确认”订单也要回退“已完成”订单不允许取消防止食堂已经备了菜再退导致浪费。4.4 验证码输对了却提示错误现象用户在登录时输入的验证码明明正确提交后却提示“验证码错误”刷新也不管用。原因这是原生PHP项目极其常见的问题。验证码生成的代码把验证码写入session但登录页面和验证码图片接口不在同一个session作用域——比如用了多个域名访问localhost和127.0.0.1混用cookie携带不一致或者验证码脚本里调用了session_start()之后前面有HTML输出导致session写入失败。解决把验证码接口和登录处理URL统一成同一个域名验证码生成脚本开头只放session_start()不允许任何输出包括BOM头。如果验证码是表单里一起提交的还要注意imagepng()输出图片前不能有echo任何字符。要定位这个问题直接在验证码脚本第一行写error_reporting(E_ALL)看看有没有“headers already sent”的警告。4.5 PHP版本升级后mysql_*函数全部报错现象代码里到处是mysql_query()PHP 7环境跑起来白屏或报Call to undefined function mysql_connect()。原因mysql_*扩展在PHP 7.0被移除只能用mysqli_*或PDO。很多下载下来的老代码包还停留在10年前的写法。解决全局搜索替换mysql_query为mysqli_querymysql_fetch_array为mysqli_fetch_array但要注意mysqli_query的第一个参数必须是连接对象所以代码里原来的mysql_query($sql)要改成mysqli_query($conn, $sql)。如果代码量大最省事的方案是先装 PHP 5.6 跑通再说但毕业设计答辩现在普遍要求PHP 7/8建议直接用我前面写的PDO或mysqli示例重写核心数据访问层。4.6 用户提交的JSON菜品数据解析失败现象前端用JSON.stringify提交菜品数组后端json_decode($_POST[dishes], true)得到null。原因可能的坑有三个——前端没设置Content-Type: application/jsonPHP收到的是表单格式而不是JSONJSON里有中文转义问题PHP拿到的是字符串但json字符串有BOM头。解决前端提交时统一用$.ajax加contentType: application/x-www-form-urlencoded把JSON对象手动转成dishes JSON.stringify(...)传过来。后端在json_decode之前先$raw $_POST[dishes]; if (substr($raw, 0, 3) \xEF\xBB\xBF) $raw substr($raw, 3);把BOM去掉再执行json_decode($raw, true)并检查返回值json_last_error()是不是JSON_ERROR_NONE。5. 部署与测试把代码包从压缩文件变成可演示的系统5.1 环境准备PHPStudy是老朋友但版本要对这套代码最稳妥的运行环境是Apache PHP 7.4 MySQL 5.7用PHPStudy就能一键配出来。需要注意PHP版本不是越高越好——如果代码里还有老式each()、create_function()这些函数PHP 8会直接报错。我建议直接下载PHPStudy在某个版本切到PHP 7.4这个版本兼容性最好既有??空合并运算符又保留了绝大多数PHP 5的旧函数。5.2 导入数据库与修改配置代码包里一般会带database.sql或db.sql用phpMyAdmin导入即可。如果导入报错检查SQL文件头部的CREATE DATABASE语句是不是和你的库名一致。然后找到config.php或conn.php把数据库账号密码填进去?php // config.php 示例 date_default_timezone_set(Asia/Shanghai); define(DB_HOST, 127.0.0.1); define(DB_USER, root); define(DB_PASS, root); define(DB_NAME, canteen_order); $conn new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); $conn-set_charset(utf8mb4); if ($conn-connect_error) { die(数据库连接失败 . $conn-connect_error); } ?这里有个容易被坑的点很多老代码里用的是mysql_connect连的库即使你改成了mysqli数据库DB_NAME也可能不对——去数据库里看一眼实际的库名不要盲信SQL文件里的名称。set_charset(utf8mb4)必须设置否则菜品名称里的中文到MySQL里全是乱码而且emoji字符会被截断。5.3 本地功能测试清单跑通后别急着交按下面这张表逐项测试测试项操作步骤预期结果用户注册去注册页填写用户名、密码、确认密码入库成功密码字段是哈希不是明文用户登录输入正确/错误密码各一次正确密码进首页错误密码有提示预约时段展示查看“可预约时段”列表只显示未来日期过期时段不出现提交预约选时段、选2个菜品、提交弹“预约成功”时段剩余名额减1重复预约同一时段再提交一次被拒绝提示“该时段已约满”或“请勿重复下单”管理员登录用admin账号进后台能看到全部订单列表管理员确认点“确认”一笔待确认订单订单状态变为已确认名额不变化管理员取消点“取消”一笔待确认订单订单状态变为已取消对应时段名额回退1未登录访问后台直接访问admin_order.php跳转登录页不允许进入测试的时候有个小习惯我一直保留每测一项就打开MySQL的general_log看系统实际执行的SQL是什么样的。如果发现预约功能提交后执行了两条UPDATE说明代码在事务外重复扣减了名额这比界面报错更隐蔽但更容易暴露逻辑问题。6. 进阶改造把毕设系统从“能跑”变成“扛打”如果你不想止步于“演示通过”这三个小改造提升明显而且代码量不大。第一把时段的并发控制从数据库锁升级成Redis。数据库锁在50个并发下表现还算稳定但到了几百人同时抢一个午餐时段FOR UPDATE会让大量请求排队阻塞响应时间飙升。改造方案是在扣减名额前先redis的DECR操作时段剩余名额的keyDECR是原子的返回负数说明已抢完。Redis的剩余名额作为前置快速过滤数据库事务仍然保留做最终一致性兜底。这样大部分请求在Redis层就被拦截数据库压力大幅降低。$redis new Redis(); $redis-connect(127.0.0.1, 6379); $key period_remaining_ . $period_id; $remaining $redis-decr($key); if ($remaining 0) { $redis-incr($key); // 补偿回滚 die(该时段已约满); }这个改造在答辩时特别加分因为涉及了“缓存与数据库一致性”的话题。但要注意Redis里的初始值必须在每天凌晨或管理员设置配额时同步重置否则第二天这个key还是昨天的剩余值。我一般用定时任务每天的凌晨把period.quota - period.reserved推送到Redis。第二给预约成功和取消加邮件或钉钉通知。原生PHP发邮件要用mail()函数本地测试容易失败最省事的做法是调用SMTP库phpmailer。如果食堂内部用企业微信或钉钉接Webhook机器人更实用——订单一生成就向群机器人POST一条JSON消息食堂工作人员即时看到。代码不复杂核心是用curlPOST一个JSON到Webhook地址。$data json_encode([ msgtype text, text [content 新预约用户{$_SESSION[username]} {$period[start_time]} 订单号{$order_id}] ]); $ch curl_init(); curl_setopt($ch, CURLOPT_URL, https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyYOUR_KEY); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: application/json]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch);第三数据统计和导出台账。食堂管理员最想看的不是界面多漂亮而是“这个月每天的就餐人数曲线”和“哪几个菜最受欢迎”。给orders表加一段简单的报表SQL按日分组统计已确认订单的数量再用fputcsv输出成Excel能打开的csv文件。因为dish_ids是JSON统计菜品销量需要遍历订单数据量大时可以写一个每天夜间执行的聚合脚本把菜品的日销量汇总到dish_daily_report表前台直接查这个表就行。我从拆这个项目到现在每次给别人讲PHP的预约类系统都会强制自己先看一眼时间字段和事务嵌套——这两个地方解决的问题比页面多写一百行代码都值。把这套逻辑吃透了往后接到会议室预约、实验室预约、课程选课这类“名额型”需求直接把时段表和订单状态机搬过去改改就能交付。希望帮到你。本文还有配套的精品资源点击获取
返回列表