ARTICLE DETAIL

资讯详情

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

数据库课程设计机票预订系统:从ER建模到事务避坑指南

数据库课程设计机票预订系统:从ER建模到事务避坑指南 简介机票预订系统数据库课程设计文档面向高校数据库课程设计及大型数据库Oracle实践环节完整覆盖从需求分析、E-R建模到物理实现的全过程。文档围绕航空客运业务梳理航班基本信息、机票信息、客户信息三类核心数据构建包含航空公司、飞机、航班、机舱、机票、乘客、业务员等实体的E-R模型并据此设计关系模式与物理表。实现层面详细给出表空间创建语句、参数化视图、存储过程、函数、触发器以及角色、用户、权限和备份恢复方案SQL示例贯穿始终方便直接对照练习。文档以PL/SQL为开发语言在Oracle数据库管理系统中实现通过表空间分配区分数据量较大的乘客表、机票表与销售表体现了实际项目的容量规划思路。资源为单个doc文件容量约1.2MB目录按序言、需求分析、分析和设计、课程设计总结等章节组织结构完整。已有84人学习下载适合需要完成数据库课程设计、复习Oracle数据库设计流程或撰写设计报告的学生参考。1. 数据库课程设计机票预订系统一份 doc 背后真正要交的东西「数据库课程设计机票预订系统」是一道经典到不能再经典的课设题但正因为熟大部分交上去的 .doc 反而翻车两张表、一个下拉框、几段增删改查最后被老师一句话问住——「余票怎么保证不超卖」这个题目真正要练的是三层ER 建模怎么拆分乘客、舱位、订单SQL 怎么在事务里扣减余票并回补文档怎么写才能让评分的人一眼看到设计思路。这篇笔记按能直接跑通、能上台答辩的标准把建库、SQL、存储过程、避坑一次讲完。适合正在赶课设的学生也适合打算拿这个题目熬过答辩的人。2. 先建模再写 SQL机票预订系统的需求拆分与 ER 设计2.1 功能模块怎么拆别把机票做成图书管理图书管理系统只需要 book 和 borrow_record 两张表就能撑住机票预订不行。原因在于机票业务有状态流转用户查航班、选舱位、下单、支付、出票、退票每一步都会修改不止一张表。我一般先把功能拆成五个模块再决定表结构。第一个是航班管理对应后台上架航班核心是航班号、起降城市、起降时间、机型第二个是余票查询用户按城市和日期看有哪些航班、什么舱位、剩多少票第三个是购票下单这一步同时涉及余票扣减、订单生成、乘客信息落地第四个是退票改签退票要回补余票不能只改订单状态第五个是统计报表按日期或航线算售票数和收入。拆完模块再回头画 ER 图你就知道为什么机票系统少说要五张表。这里有个常见误区上来就画 ER 图画到订单和乘客的关系时卡住。正确顺序是先列业务动词——查询、下单、退票、统计每一个动词对应一次数据变化表结构跟着变化走。例如「退票」这个动作如果你只设计了订单表而没有舱位余票表回补余票就只能把余票字段塞在航班表里后面做统计时就会发现口径很别扭。2.2 ER 建模的四个取舍乘客、余票、价格、订单先说乘客和账号的关系。用户注册一个账号但一个订单可以给三个人买票所以 user 和 passenger 不能混在一张表。常见做法是 user_account 存登录账号ticket_order 存订单ticket_passenger 存每个乘客的姓名和证件号一个订单对多条乘客记录。如果偷懒把乘客名字直接塞进订单表两个人同行就要插两行订单统计收入时会把同一笔订单算两次。第二个取舍是余票放哪里。一个航班有经济舱、商务舱、头等舱三个舱位余票不同、价格也不同。如果把余票和价格做成 flight 表的字段加一个舱位就要改表结构。正确做法是拆一张 flight_seat 表主键是航班 ID 加舱位等级total_seats 和 remain_seats 跟着舱位走。这样余票扣减变成对一行记录的 UPDATE天然避免「两个舱位抢同一列」的尴尬。第三个取舍是价格要不要单独建表。课程设计里价格只随舱位和航班走不涉及淡旺季动态调价所以 price 字段直接放在 flight_seat 里就够了。单独建 price 表意味着多一次 JOIN而收益几乎为零这是典型的过度设计。第四个取舍是起降城市直接做成 flight 表的两个字段不要拆 route 表。拆 route 表能支持「同一航线多趟航班」的规范化但会让查询变成三表 JOIN课设里把 dep_city 和 arr_city 直接冗余在 flight 上写余票查询时省掉一层关联数据一致性靠录入时把控即可。2.3 数据字典与完整性约束字段类型、默认值、检查约束定完实体关系就可以落数据字典。下面这张表是我在做这个题目时用的核心结构五个表覆盖上面五个功能模块字段名和类型都是 MySQL 8 可直接执行的。表名关键字段约束与说明user_accountid, username, password, phone, created_atusername 唯一密码字段建议 VARCHAR(64) 存哈希不存明文flightid, flight_no, dep_city, arr_city, dep_time, arr_time, aircraft_typeflight_no 唯一dep_city 与 arr_city 建联合索引flight_seatid, flight_id, seat_level, total_seats, remain_seats, price(flight_id, seat_level) 唯一remain_seats 非负ticket_orderid, order_no, user_id, flight_id, seat_level, passenger_count, total_price, status, create_timeorder_no 唯一status 用 TINYINT 注释枚举ticket_passengerid, order_id, passenger_name, id_card, seat_noorder_id 外键指向订单id_card 存 18 位证件号状态字段我建议用 TINYINT 加 COMMENT不要用 ENUM。比如 status 0 待支付、1 已出票、2 已取消、3 已退票。ENUM 在 MySQL 里修改枚举值要 ALTER TABLE而 TINYINT 加注释既直观又方便加状态答辩时被问「为什么不用 ENUM」也有得答。金额字段统一用 DECIMAL(10,2)不要用 FLOAT因为浮点数的精度问题在金额场景下是硬伤。数据字典做完ER 图其实就出来了user_account 对 ticket_order 是一对多flight 对 flight_seat 是一对多ticket_order 对 ticket_passenger 是一对多flight_seat 和 ticket_order 通过 flight_id 加 seat_level 关联。ER 图里不需要画 ticket_passenger 对 flight 的关系乘客的航班信息从订单表取避免关系线交叉。3. 从 DDL 到业务 SQL建库建表与增删改查的落地写法3.1 DDL建库建表脚本与字段说明ER 模型定完就直接写 DDL。以下脚本可以在 MySQL 8 里原样执行注意建表顺序先建被引用的表user_account、flight再建引用它们的表flight_seat、ticket_order最后建 ticket_passenger否则外键会因为找不到目标表而报错。CREATE DATABASE airline_ticket DEFAULT CHARACTER SET utf8mb4; USE airline_ticket; CREATE TABLE user_account ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE flight ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, flight_no VARCHAR(10) NOT NULL UNIQUE, dep_city VARCHAR(30) NOT NULL, arr_city VARCHAR(30) NOT NULL, dep_time DATETIME NOT NULL, arr_time DATETIME NOT NULL, aircraft_type VARCHAR(20), INDEX idx_dep_arr (dep_city, arr_city) ); CREATE TABLE flight_seat ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, flight_id INT UNSIGNED NOT NULL, seat_level VARCHAR(10) NOT NULL COMMENT 经济舱/商务舱/头等舱, total_seats INT NOT NULL, remain_seats INT NOT NULL, price DECIMAL(10,2) NOT NULL, UNIQUE KEY uk_flight_seat (flight_id, seat_level), CONSTRAINT fk_seat_flight FOREIGN KEY (flight_id) REFERENCES flight(id) ); CREATE TABLE ticket_order ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(30) NOT NULL UNIQUE, user_id INT UNSIGNED NOT NULL, flight_id INT UNSIGNED NOT NULL, seat_level VARCHAR(10) NOT NULL, passenger_count INT NOT NULL DEFAULT 1, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已出票 2已取消 3已退票, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user_account(id), CONSTRAINT fk_order_flight FOREIGN KEY (flight_id) REFERENCES flight(id) ); CREATE TABLE ticket_passenger ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, passenger_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL, seat_no VARCHAR(5) COMMENT 出票后生成的座位号, CONSTRAINT fk_passenger_order FOREIGN KEY (order_id) REFERENCES ticket_order(id) );几个字段的显式命名值得说外键约束用了 fk_seat_flight 这种带语义的名字后面如果要删外键或者排查约束问题不用去翻 MySQL 自动生成的随机名。seat_level 在 ticket_order 里存的是字符串而不是 flight_seat 的 ID这是故意的——下单时用户选的是「经济舱」这个档位不是某个具体座位行用字符串能避免下单后再去 JOIN 舱位表才能知道买的是什么舱。3.2 余票与航班查询联表查询的正确打开方式用户最常见的操作是「查 6 月 1 日北京到上海还有没有票」这条 SQL 要 JOIN flight 和 flight_seat 两张表同时过滤城市、日期和余票大于零SELECT f.flight_no, f.dep_city, f.arr_city, DATE_FORMAT(f.dep_time, %Y-%m-%d %H:%i) AS dep_time, fs.seat_level, fs.price, fs.remain_seats FROM flight f JOIN flight_seat fs ON fs.flight_id f.id WHERE f.dep_city 北京 AND f.arr_city 上海 AND f.dep_time 2025-06-01 00:00:00 AND f.dep_time 2025-06-02 00:00:00 AND fs.remain_seats 0 ORDER BY f.dep_time, fs.price;日期条件用左闭右开区间 起始日 00:00:00 AND 次日 00:00:00不要用BETWEEN或者 2025-06-01否则跨天航班比如 6 月 2 日 00:05 起飞会被漏掉这一点在避坑章节还会专门展开。城市过滤用第 3.1 节建的 idx_dep_arr 索引配合 EXPLAIN 可以看到查询走的是索引而不是全表扫。3.3 购票事务先扣余票还是先生成订单购票是整个系统里最考验数据库基本功的地方涉及三张表扣减 flight_seat 余票、插入 ticket_order 订单、插入 ticket_passenger 乘客。三件事必须同一个事务任何一个失败都要全部回滚。核心是先扣余票扣成功了再写订单START TRANSACTION; -- 条件更新只有余票大于 0 时才能扣减成功 UPDATE flight_seat SET remain_seats remain_seats - 1 WHERE flight_id 1 AND seat_level 经济舱 AND remain_seats 0; -- 检查上一条 UPDATE 影响的行数影响行数为 0 说明余票不足需要回滚 INSERT INTO ticket_order (order_no, user_id, flight_id, seat_level, passenger_count, total_price, status) VALUES (202506010001, 1, 1, 经济舱, 1, 800.00, 1); INSERT INTO ticket_passenger (order_id, passenger_name, id_card) VALUES (LAST_INSERT_ID(), 张三, 110101199001011234); COMMIT;这段逻辑里最关键的是 UPDATE 语句带了remain_seats 0这个条件。它把「检查余票」和「扣减余票」合并成一条原子操作数据库并发锁会锁住这一行两个请求同时买最后一张票时只有一个 UPDATE 能影响一行另一个影响 0 行直接判定失败。如果你先 SELECT 余票再 UPDATE两个事务会读到同一个余票数字双双扣减成功这就是超卖。顺序总结先扣余票再写订单最后写乘客订单号通过 order_no 唯一索引保证不重复。3.4 退票、取消与统计报表状态变更和数据口径退票不能只把订单状态改成 3必须同步回补余票否则卖出去的票数和数据库里的余票对不上这就是「票从哪里来」的数据口径问题。退票的正确姿势是把状态更新和余票回补放进同一个事务START TRANSACTION; UPDATE ticket_order SET status 3 WHERE order_no 202506010001 AND status 1; -- 影响行数为 0 说明订单不存在或已退过直接回滚 UPDATE flight_seat fs JOIN ticket_order o ON o.flight_id fs.flight_id AND o.seat_level fs.seat_level SET fs.remain_seats fs.remain_seats 1 WHERE o.order_no 202506010001; COMMIT;退票语句里用 JOIN 子查询把订单对应的航班和舱位带出来比先查订单再更新余票少一次往返也避免事务中间夹着业务代码导致的不一致。统计报表的常见需求是按天算收入和出票量这时 status 条件必须写死否则取消和退票的单子会被算进收入里SELECT DATE(create_time) AS stat_date, COUNT(*) AS ticket_count, SUM(total_price) AS revenue FROM ticket_order WHERE status 1 GROUP BY DATE(create_time) ORDER BY stat_date DESC;GROUP BY 后面用 DATE(create_time) 而不是直接 GROUP BY create_time是为了把同一天的订单合并成一行。再加一个维度就按航线统计把 GROUP BY 换成 dep_city、arr_city前边的 WHERE 不变这就是报表系统最基础的分组聚合写法。4. 视图、存储过程、触发器把重复 SQL 封装成答辩加分项4.1 视图统一余票查询口径写到这里你会发现余票查询的 JOIN 条件在多处重复出现多了就容易改一处漏一处。视图的作用是把这段 JOIN 固定成一个虚拟表业务层只对视图做查询口径一致且不易写错CREATE VIEW v_flight_remaining AS SELECT f.flight_no, f.dep_city, f.arr_city, f.dep_time, fs.seat_level, fs.price, fs.remain_seats FROM flight f JOIN flight_seat fs ON fs.flight_id f.id WHERE fs.remain_seats 0;视图建好之后前端的余票列表查询就变成一行SELECT flight_no, dep_city, arr_city, dep_time, seat_level, price, remain_seats FROM v_flight_remaining WHERE dep_city 北京 AND arr_city 上海;视图在课程设计里的加分点有两个一是报告里可以写「通过视图屏蔽底层表结构变化」二是答辩时能解释清楚视图不占物理存储、每次查询都会重新执行底层 SQL。注意别在视图上做嵌套视图或者复杂的聚合视图课设阶段一个 JOIN 加 WHERE 就是最合适的复杂度。4.2 存储过程一个 CALL 完成购票事务第三章的购票事务有三条 SQL在代码里手写很容易漏掉 COMMIT 或者忘记判断影响行数。封装成存储过程之后业务层只传乘客和航班参数事务边界从数据库层保证。下面这个存储过程包含余票扣减、订单插入、乘客插入三个步骤DELIMITER $$ CREATE PROCEDURE buy_ticket( IN p_user_id INT, IN p_flight_id INT, IN p_seat_level VARCHAR(10), IN p_passenger_name VARCHAR(50), IN p_id_card VARCHAR(18), OUT p_order_no VARCHAR(30) ) BEGIN DECLARE v_price DECIMAL(10,2); DECLARE v_affected INT DEFAULT 0; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; UPDATE flight_seat SET remain_seats remain_seats - 1 WHERE flight_id p_flight_id AND seat_level p_seat_level AND remain_seats 0; SELECT ROW_COUNT() INTO v_affected; IF v_affected 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 余票不足; END IF; SELECT price INTO v_price FROM flight_seat WHERE flight_id p_flight_id AND seat_level p_seat_level; SET p_order_no CONCAT(DATE_FORMAT(NOW(), %Y%m%d%H%i%s), FLOOR(RAND() * 1000)); INSERT INTO ticket_order (order_no, user_id, flight_id, seat_level, passenger_count, total_price, status) VALUES (p_order_no, p_user_id, p_flight_id, p_seat_level, 1, v_price, 1); INSERT INTO ticket_passenger (order_id, passenger_name, id_card) SELECT id, p_passenger_name, p_id_card FROM ticket_order WHERE order_no p_order_no; COMMIT; END$$ DELIMITER ;调用方式CALL buy_ticket(1, 1, 经济舱, 李四, 110101199502024321, order_no); SELECT order_no;这个存储过程里有几个参数设计值得注意。ROW_COUNT() 必须在 UPDATE 之后立刻调用中间间隔别的语句会拿到错误的值所以我把更新和取值写成了连续的两行。订单号用时间戳加三位随机数拼接并发场景下靠唯一索引兜底重复了会抛异常回滚不会出现两张相同订单。SIGNAL SQLSTATE 45000 是主动抛错让外层应用程序能捕获「余票不足」这个业务异常而不是吞掉错误继续跑。4.3 触发器哪些场景该用哪些场景是坑很多课程设计为了展示触发器把「余票回补」写进触发器订单状态变成已退票时自动加回余票。这个思路看起来漂亮实际是个坑因为如果你在存储过程里已经手动回补了余票触发器会再补一次余票翻倍。触发器最大的问题是像个黑匣子——你看到余票变了但不知道是哪条路径改的排错全靠翻表结构。我通常建议触发器只做毫无歧义的辅助动作比如订单插入后写一条日志。下面这个场景就非常安全因为它不涉及业务数据只做留痕CREATE TABLE order_log ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, action VARCHAR(20) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER trg_order_log AFTER INSERT ON ticket_order FOR EACH ROW BEGIN INSERT INTO order_log (order_id, action, create_time) VALUES (NEW.id, 下单, NOW()); END;触发器演示了这个机制能自动捕获 INSERT 事件又不会和事务里的其他逻辑互相干扰。答辩时如果老师问你为什么不用触发器回补余票你回答「显式调用比隐式触发更容易排查余票变化属于核心业务不能靠触发器隐藏掉」这个答案比「我不会写触发器」高明得多。5. 五个翻车现场课程设计最常见的避坑与排查记录5.1 现象订单号做主键后插入越来越慢偶尔还报主键冲突原因很多人把 order_no 这种业务单号直接设成主键。业务单号带日期、时间、随机数插入时的值在 B 树里基本是乱序的每次插入都可能触发页分裂数据量大之后性能明显劣化。更糟的是同一秒生成的单号可能撞车唯一索引直接报错。解决主键用自增 idorder_no 单独建 UNIQUE 索引保证业务上的唯一。查询订单时用 order_no 走索引性能同样没问题但插入顺序变成纯递增B 树不用反复重排。5.2 现象查「明天北京到上海」的航班返回 0 条但数据库里明明有数据原因日期条件写错了。最常见的两种写法一是dep_time 2025-06-01MySQL 会把字符串转成2025-06-01 00:00:00白天所有航班全部被排除二是用BETWEEN 2025-06-01 00:00:00 AND 2025-06-01 23:59:59跨天航班次日 00:05 起飞被排除。解决一律用左闭右开区间dep_time 2025-06-01 00:00:00 AND dep_time 2025-06-02 00:00:00这样次日凌晨的航班归到它的实际起飞日期CURDATE() 做动态日期时也套用这个写法。排查这类问题时先单独 SELECT 该航班的 dep_time 原始值再用肉眼对一遍边界。5.3 现象两个窗口同时买最后一张票两边都提示购买成功原因代码里先SELECT remain_seats判断大于 0再执行 UPDATE。两个事务同时读到余票 1都判断可以买各自扣减数据库里余票变成 -1但两个订单都生成了。这是典型的并发超卖根因是检查和写入之间有空窗期。解决把检查并进 UPDATE 的条件里UPDATE flight_seat SET remain_seats remain_seats - 1 WHERE remain_seats 0然后判断影响行数为 0 就抛异常回滚。这一条是数据库并发锁最直接的体现也是答辩时老师最爱追问的点。5.4 现象文档里的 ER 图跟交付的 SQL 脚本对不上老师对照着看直接扣分原因后期发现缺字段比如订单要加一个 passenger_count直接改了表和代码但没有回头更新 Word 里的 ER 图和数据字典。文档和实现分家是课设文档最冤的丢分点。解决强制自己按「先改图再改表最后改 SQL」的顺序动工任何字段变动都先在 ER 图和数据字典里落一笔。交付前做一次交叉核对把 Word 里的表清单和数据库里的 SHOW TABLES 结果逐行比对。这里建议 .doc 按下面这个结构组织每章和数据库里的对象一一对应文档章节对应内容篇幅建议需求分析五个功能模块的描述与用例3 页内概念模型ER 图实体和联系的文字说明2 页内逻辑模型数据字典表名/字段/类型/约束5 个表逐表列出物理实现DDL 脚本、核心 SQL、存储过程直接贴代码加注释功能验证每个功能的操作截图与运行结果每个功能 1 到 2 张截图5.5 现象建表时建了物理外键后面插入数据报「外键约束失败」删外键又怕老师说破坏参照完整性原因物理外键保证了数据一致性但也带来两个麻烦——插入时要先插父表再插子表删除时要先删子表再删父表数据初始化脚本必须严格按顺序跑另外导入数据时稍微乱序就会中断。解决课程设计里我建议保留物理外键因为评分标准里「参照完整性」是白纸黑字的得分点。如果遇到初始化顺序问题先执行 SET FOREIGN_KEY_CHECKS 0; 再导入最后改回来。如果你实在想删外键保留逻辑外键不加约束的普通字段答辩时说明「真实系统为了性能和迁移方便外键约束放在应用层校验」同时强调你理解外键的原理不会丢分。6. 答辩前的最后验证运行记录、索引与数据库面试题课程设计交的不是代码是你能讲清楚这套设计。我习惯在答辩前一晚把整个系统删库重建按真实用户路径跑一遍完整流程边跑边记录。下面是一张验证清单每一行对应一次操作、一个预期结果场景操作预期注册插入重复 username唯一索引拒绝报错余票查询查北京到上海的航班只返回余票大于 0 的舱位购票CALL buy_ticket 买最后一张票余票减到 0订单和乘客生成超卖再次 CALL buy_ticket 买同舱位抛出余票不足无新订单退票更新订单状态为 3余票回补 1统计金额减少这五步跑通系统的基本盘就稳了。然后做一层最简单的数据库优化验证给查询频繁的 where 和 join 字段确认有索引并用 EXPLAIN 检查执行计划EXPLAIN SELECT * FROM v_flight_remaining WHERE dep_city 北京;如果 type 列是 ALL说明在扫全表检查 dep_city 是否命中联合索引 idx_dep_arr改成 ref 之后查询时间通常有明显下降。索引不是越多越好课设里把「查询频率最高的两个字段建联合索引」写到文档里比列一堆用不上的索引更让老师认可。答辩环节老师问的其实就是几道数据库面试题事务隔离级别分别解决什么问题持悲观锁和乐观锁的场景差异索引什么时候会失效。你手里有购票和退票两个真实业务把「余票扣减属于悲观锁」「统计报表用 READ COMMITTED 够了」这两句话用业务场景讲出来比背概念要有说服力得多。我自己的教训是永远不要带着没跑过一遍的脚本上答辩你以为最稳的建表顺序往往就是翻车的地方。希望帮到你。本文还有配套的精品资源点击获取
返回列表