YAOTU INSIGHTS

MySQL 8.0饭店点餐系统实战:从ER图到高并发订单

MySQL 8.0饭店点餐系统实战:从ER图到高并发订单
简介本资源是面向高校数据库课程学习者的实践型课程设计项目聚焦饭店点餐系统这一典型业务场景帮助学生掌握从需求分析、E-R建模到SQL脚本实现的完整数据库设计流程。压缩包共3个文件2个txt说明文档 1个sql建库脚本总大小仅4KB轻量实用其中sql文件含Customers、Dishes、Orders、Employees等核心表结构定义及可能的初始化语句txt文件分别提供使用说明与代码逻辑注解便于理解表间关系与字段设计意图。已有4343人学习下载适合数据库入门至中级学习者用于课设参考、实验复现或期末项目快速启动。读者可直接导入MySQL等主流DBMS运行验证配套文档还隐含事务处理、多表查询示例等延伸学习线索助力夯实建模思维与SQL实操能力。1. 为什么一个“饭店点餐系统”课程设计能暴露出90%初学者在数据库工程落地时的真实断层这不是一个单纯建几张表、写几条INSERT的练习题。当你打开那个名为数据库课程设计饭店点餐系统.zip的压缩包里面往往只有三样东西一份Word文档描述功能需求比如“顾客可查看菜单、下单、修改订单状态”一个空荡荡的SQL脚本文件和一张手绘的ER图截图——但没人告诉你这张图里“菜品”和“订单明细”之间那根带菱形的连线到底该用外键约束还是触发器来保一致性也没人提醒你“订单状态”字段如果只用VARCHAR存‘已下单’‘制作中’‘已出餐’后期加个‘已取消’就会让所有WHERE语句集体失效更没人说清楚为什么Navicat里执行一条SELECT * FROM order_detail JOIN dish ON ...明明语法没错却在真实数据量超过500条后开始卡顿到需要重启软件。这个项目真正考的是把教科书里的范式理论、SQL语法、事务概念焊接到一个有真实业务毛刺的场景里顾客可能同时点同一道菜两次厨房可能漏单服务员可能手抖多点了一份汤却忘了改价而老板明天一早就要看“昨天川菜销量TOP5”。它逼你直面数据库不是玩具——它是业务逻辑的黑匣子一旦设计失当增删改查会变成玄学备份恢复会变成后悔药连最基础的“查某天所有未完成订单”都得靠临时拼接LEFT JOIN和子查询硬扛。适合那些已经写过CREATE TABLE但还没被线上慢查询报警吓醒的人也适合那些背过ACID却第一次发现“事务隔离级别”真会影响服务员刷新页面时看到的订单状态的人。2. 从需求文档到可运行数据库用MySQL 8.0构建最小可行模型2.1 拆解需求文档里的隐藏约束比写DDL更重要很多同学拿到需求就开写CREATE TABLE customer(...)结果第三天发现“顾客可修改手机号”这条需求导致原设计的主键customer_id无法支撑实名认证变更只能推倒重来。真正的起点是把Word文档里每句话翻译成数据库语言“顾客注册时需提供姓名、手机号、密码” → 手机号需UNIQUE NOT NULL密码字段必须CHAR(64)以上为后续bcrypt哈希留空间禁止直接存明文“每道菜有分类如热菜/凉菜、价格、库存” → 分类不宜用ENUM扩展性差应单独建category表dish.category_id设外键“订单包含多个菜品每道菜可选份数” → 这是典型的多对多关系必须引入中间表order_detail且该表主键应为(order_id, dish_id)复合主键而非自增ID避免重复添加同一菜品“订单状态可变更为‘已下单’‘制作中’‘已出餐’‘已取消’” → 状态字段用TINYINT或ENUMMySQL 8.0支持ENUM排序严禁用VARCHAR存中文状态索引失效、排序错乱、国际化灾难。提示把需求逐条列成表格左列原文右列对应的数据类型、约束、索引建议。我常把这张表贴在显示器边框上写完每个CREATE TABLE前先对照三遍。2.2 建库建表用MySQL 8.0语法写出带业务语义的DDL以下脚本已在MySQL 8.0.33实测通过所有字段均按前述约束设计关键点已加注释-- 创建数据库并指定字符集避免中文乱码 CREATE DATABASE IF NOT EXISTS restaurant_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; USE restaurant_db; -- 顾客表手机号唯一密码字段预留哈希空间 CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone CHAR(11) NOT NULL UNIQUE COMMENT 11位手机号UNIQUE保证不重复注册, password CHAR(64) NOT NULL COMMENT bcrypt哈希后固定64字符, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 菜品分类表支持无限层级扩展当前仅一级 CREATE TABLE category ( id TINYINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL UNIQUE COMMENT 如热菜、酒水UNIQUE防重复, sort_order TINYINT DEFAULT 0 COMMENT 前端展示排序权重 ); -- 菜品表关联分类库存字段设CHECK约束防负数 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id TINYINT NOT NULL, price DECIMAL(10,2) NOT NULL CHECK (price 0), stock INT NOT NULL DEFAULT 0 CHECK (stock 0), description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE RESTRICT ); -- 订单主表状态用TINYINT映射便于后期加状态机 CREATE TABLE order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已下单,2制作中,3已出餐,4已取消, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customer(id) ON DELETE CASCADE ); -- 订单明细表复合主键确保同一订单不重复添加同菜品 CREATE TABLE order_detail ( order_id BIGINT NOT NULL, dish_id INT NOT NULL, quantity TINYINT NOT NULL DEFAULT 1 CHECK (quantity BETWEEN 1 AND 99), unit_price DECIMAL(10,2) NOT NULL COMMENT 快照价格避免菜品调价影响历史订单, PRIMARY KEY (order_id, dish_id), FOREIGN KEY (order_id) REFERENCES order(id) ON DELETE CASCADE, FOREIGN KEY (dish_id) REFERENCES dish(id) ON DELETE RESTRICT );关键参数说明CHAR(64)bcrypt哈希结果固定长度比VARCHAR更省空间且查询更快CHECK (stock 0)MySQL 8.0才支持CHECK约束比应用层校验更可靠ON DELETE CASCADE订单删除时自动清理明细避免孤儿记录ON DELETE RESTRICT菜品删除时阻止操作防止历史订单丢失菜品信息utf8mb4_0900_as_cs区分大小写的排序规则避免admin和Admin被误判为相同用户名。2.3 初始化测试数据用INSERT VALUES生成可验证的业务场景光建表没数据等于没跑通。以下数据覆盖高频业务路径新顾客注册、点单、状态变更、库存扣减-- 插入分类 INSERT INTO category (name, sort_order) VALUES (热菜, 1), (凉菜, 2), (酒水, 3), (主食, 4); -- 插入菜品注意stock初始值模拟真实库存 INSERT INTO dish (name, category_id, price, stock, description) VALUES (宫保鸡丁, 1, 38.00, 50, 花生、鸡肉、干辣椒), (拍黄瓜, 2, 18.00, 100, 蒜泥、香醋、芝麻), (青岛啤酒, 3, 8.00, 200, 500ml瓶装), (米饭, 4, 2.00, 500, 东北大米); -- 注册顾客密码用bcrypt哈希此处用占位符 INSERT INTO customer (name, phone, password) VALUES (张三, 13800138000, pbkdf2:sha256:260000$...), -- 实际应调用bcrypt生成 (李四, 13900139000, pbkdf2:sha256:260000$...); -- 创建订单状态1已下单 INSERT INTO order (customer_id, status, total_amount) VALUES (1, 1, 78.00); -- 张三点宫保鸡丁*2 拍黄瓜*1 38*2 18 94? 等下这里故意留坑 -- 订单明细unit_price必须与当时菜品价格一致 INSERT INTO order_detail (order_id, dish_id, quantity, unit_price) VALUES (1, 1, 2, 38.00), -- 宫保鸡丁2份单价38 (1, 2, 1, 18.00); -- 拍黄瓜1份单价18逻辑说明最后一行INSERT故意把total_amount写成78.00实际应为94.00这是为了暴露常见错误——订单总金额不能靠应用层计算后插入而应在插入明细后用触发器自动更新。否则数据不一致风险极高。我们将在第4章用触发器修复此问题。3. 让SQL不止于查询用存储过程和触发器封装业务规则3.1 用BEFORE INSERT触发器自动校验库存杜绝超卖用户下单时应用层先查库存再扣减存在并发漏洞两个请求同时查到stock1都判定可下单结果扣减两次变成-1。正确做法是在数据库层拦截DELIMITER $$ CREATE TRIGGER check_dish_stock_before_order_detail_insert BEFORE INSERT ON order_detail FOR EACH ROW BEGIN DECLARE current_stock INT DEFAULT 0; SELECT stock INTO current_stock FROM dish WHERE id NEW.dish_id FOR UPDATE; -- 加行锁阻塞其他并发UPDATE IF current_stock NEW.quantity THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 菜品库存不足无法下单; END IF; END$$ DELIMITER ;参数说明FOR UPDATE在SELECT时对目标行加锁确保后续UPDATE不会读到脏数据SIGNAL SQLSTATE 45000抛出自定义错误应用层捕获ER_SIGNAL_EXCEPTION即可提示用户触发器在INSERT前执行失败则整条INSERT回滚无需应用层处理补偿逻辑。3.2 用AFTER INSERT触发器自动更新订单总金额和菜品库存解决第2.3节留下的坑订单总金额和菜品库存必须原子化更新。DELIMITER $$ -- 更新订单总金额 CREATE TRIGGER update_order_total_after_detail_insert AFTER INSERT ON order_detail FOR EACH ROW BEGIN UPDATE order SET total_amount total_amount (NEW.quantity * NEW.unit_price) WHERE id NEW.order_id; END$$ -- 扣减菜品库存 CREATE TRIGGER update_dish_stock_after_detail_insert AFTER INSERT ON order_detail FOR EACH ROW BEGIN UPDATE dish SET stock stock - NEW.quantity WHERE id NEW.dish_id; END$$ DELIMITER ;为什么用AFTER而非BEFORE因为order_detail的unit_price是快照值必须等明细插入成功后才能累加到订单总金额。若用BEFORENEW.unit_price尚未写入表中无法读取。3.3 用存储过程封装“下单”全流程避免应用层拼接SQL把校验、插入、更新打包成一个原子操作应用层只需调用一次DELIMITER $$ CREATE PROCEDURE place_order( IN p_customer_id BIGINT, IN p_dish_list JSON -- 格式: [{dish_id:1,quantity:2},{dish_id:2,quantity:1}] ) BEGIN DECLARE v_order_id BIGINT DEFAULT 0; DECLARE v_dish_id INT DEFAULT 0; DECLARE v_quantity TINYINT DEFAULT 0; DECLARE v_unit_price DECIMAL(10,2) DEFAULT 0; DECLARE done INT DEFAULT FALSE; DECLARE cur CURSOR FOR SELECT dish_id, quantity FROM JSON_TABLE(p_dish_list, $[*] COLUMNS (dish_id INT PATH $.dish_id, quantity TINYINT PATH $.quantity)) AS jt; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; -- 开启事务 START TRANSACTION; -- 创建订单主记录 INSERT INTO order (customer_id, status, total_amount) VALUES (p_customer_id, 1, 0); SET v_order_id LAST_INSERT_ID(); -- 遍历菜品列表插入明细 OPEN cur; read_loop: LOOP FETCH cur INTO v_dish_id, v_quantity; IF done THEN LEAVE read_loop; END IF; -- 获取当前菜品价格快照 SELECT price INTO v_unit_price FROM dish WHERE id v_dish_id; -- 插入明细触发器会自动校验库存并扣减 INSERT INTO order_detail (order_id, dish_id, quantity, unit_price) VALUES (v_order_id, v_dish_id, v_quantity, v_unit_price); END LOOP; CLOSE cur; COMMIT; SELECT v_order_id AS order_id; END$$ DELIMITER ;调用示例CALL place_order(1, [{dish_id:1,quantity:2},{dish_id:2,quantity:1}]);优势应用层无需关心事务边界存储过程内自动COMMIT/ROLLBACKJSON参数支持动态菜品列表比拼接SQL更安全防注入所有业务规则集中在数据库Java/Python代码只需调用CALL逻辑更清晰。4. 避坑指南95%同学在实现饭店点餐系统时踩过的5个深坑4.1 现象订单明细插入成功但菜品库存没扣减原因触发器中UPDATEdish语句未加WHERE条件或WHERE字段名写错如WHERE dish_id NEW.dish_id写成WHERE id NEW.dish_id。解决在触发器开头加日志表记录调试信息或用SELECT ... FOR UPDATE确认行锁生效检查SHOW CREATE TRIGGER输出的SQL是否与预期一致。4.2 现象并发下单时出现“Duplicate entry 1-1 for key PRIMARY”错误原因order_detail表主键为(order_id, dish_id)但应用层未做去重校验同一订单重复提交同一菜品。解决在存储过程中插入明细前先用INSERT IGNORE或ON DUPLICATE KEY UPDATE或在应用层对菜品列表按dish_id去重。4.3 现象Navicat执行SELECT * FROM order JOIN order_detail ON ...极慢EXPLAIN显示typeALL原因order_detail.order_id字段未建索引JOIN时全表扫描。解决立即执行ALTER TABLE order_detail ADD INDEX idx_order_id (order_id);。注意外键字段必须手动建索引MySQL不会自动创建。4.4 现象修改菜品价格后历史订单明细的unit_price被错误更新原因order_detail.unit_price字段未设为NOT NULL且应用层更新菜品时误写了UPDATE dish SET price...连带更新了明细表。解决unit_price字段加NOT NULL约束菜品表UPDATE语句严格限定WHERE条件禁用UPDATE dish SET price...无条件更新。4.5 现象顾客手机号修改后登录时报“用户不存在”但数据库里明明有该手机号原因customer.phone字段用了CHAR(11)但插入时带空格如13800138000 CHAR自动右补空格导致WHERE phone13800138000匹配失败。解决统一用TRIM()函数处理输入或改用VARCHAR(11)CHECK (phone REGEXP ^1[3-9][0-9]{9}$)正则校验。5. 验证与压测用真实数据量检验设计健壮性5.1 构造千级测试数据用递归CTE生成模拟订单流手工INSERT几十条数据看不出性能问题。用MySQL 8.0的CTE批量生成1000个顾客、5000笔订单-- 生成1000个测试顾客避免主键冲突用UUID转数字 INSERT INTO customer (name, phone, password) SELECT CONCAT(顾客, seq), LPAD(seq, 11, 1), pbkdf2:sha256:260000$... FROM ( WITH RECURSIVE seq AS ( SELECT 1 as n UNION ALL SELECT n1 FROM seq WHERE n 1000 ) SELECT n as seq FROM seq ) t; -- 生成5000笔订单随机分配顾客、菜品、数量 INSERT INTO order (customer_id, status, total_amount) SELECT FLOOR(1 RAND() * 1000), FLOOR(1 RAND() * 4), ROUND(RAND() * 500, 2) FROM ( WITH RECURSIVE seq AS ( SELECT 1 as n UNION ALL SELECT n1 FROM seq WHERE n 5000 ) SELECT n FROM seq ) t; -- 关联生成订单明细每单1~5道菜 INSERT INTO order_detail (order_id, dish_id, quantity, unit_price) SELECT o.id, FLOOR(1 RAND() * 4), -- 4道菜ID FLOOR(1 RAND() * 5), -- 1~5份 (SELECT price FROM dish WHERE id FLOOR(1 RAND() * 4)) FROM order o JOIN ( WITH RECURSIVE seq AS ( SELECT 1 as n UNION ALL SELECT n1 FROM seq WHERE n 5 ) SELECT n FROM seq ) t ON RAND() 0.2; -- 控制约80%订单有明细执行后检查SELECT COUNT(*) FROM customer;→ 应≈1000SELECT COUNT(*) FROM order_detail;→ 应≈200005000单×平均4道菜SELECT COUNT(*) FROM dish WHERE stock 0;→ 必须为0触发器库存校验生效5.2 关键SQL性能诊断用EXPLAIN定位慢查询针对高频场景写测试SQL并强制走索引-- 场景1查询某顾客所有未完成订单status IN (1,2) EXPLAIN FORMATTREE SELECT o.id, o.total_amount, o.created_at, d.name, od.quantity FROM order o JOIN order_detail od ON o.id od.order_id JOIN dish d ON od.dish_id d.id WHERE o.customer_id 1 AND o.status IN (1,2); -- 场景2查询某菜品今日销量需日期范围 EXPLAIN FORMATTREE SELECT SUM(od.quantity) as today_sales FROM order o JOIN order_detail od ON o.id od.order_id WHERE od.dish_id 1 AND DATE(o.created_at) CURDATE();优化要点若EXPLAIN显示typeALL立即为order.customer_id和order.status建联合索引CREATE INDEX idx_customer_status ONorder(customer_id, status);若DATE(o.created_at) CURDATE()导致索引失效改用范围查询o.created_at CURDATE() AND o.created_at CURDATE() INTERVAL 1 DAY5.3 事务隔离级别实战READ COMMITTED如何解决“服务员看到未刷新订单”默认REPEATABLE READ下服务员A开启事务查询订单此时顾客B下单并提交A再次查询仍看不到新订单幻读。业务要求实时性应降级-- 为点餐相关连接设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 或在应用层连接池配置中指定效果验证服务员A执行START TRANSACTION; SELECT * FROM order WHERE status1;顾客B执行CALL place_order(...);并COMMIT服务员A再次SELECT * FROM order WHERE status1;→ 立即看到新订单我在带学生做这个项目时总让他们先用REPEATABLE READ跑一遍再切到READ COMMITTED对比两次查询结果的时间差。当他们亲眼看到“刚下的单在另一端秒级可见”才真正理解隔离级别不是理论名词而是业务体验的开关。后来有学生把这招用在校园二手平台作业里解决了“卖家改价后买家仍看到旧价”的投诉——数据库设计的价值就藏在这种让业务方少挨骂的细节里。希望帮到你。本文还有配套的精品资源点击获取