尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

广工数据库课设实战:车站售票管理系统完整业务闭环与并发扣票

广工数据库课设实战:车站售票管理系统完整业务闭环与并发扣票 简介这份资源是广东工业大学数据库系统课程设计的个人选题方案——车站售票管理系统面向正在准备数据库课设的本科生及需要参考完整项目实现的学习者。系统围绕售票与退票、车次与时刻表查询、售票情况统计以及数据备份恢复、操作员权限管理等维护功能展开覆盖了数据库课设常见的需求分析与功能落地场景。压缩包共100个文件约4.65MB以72个class与21个java源码文件为主体另含1个sql建库脚本、1个jar依赖及doc安装说明书、txt说明等源码与文档配套便于直接运行和二次修改。目前已有1154人学习下载说明该方案在同类课设中具有一定参考价值。读者可从中获得一套可运行的车站售票管理实现、数据库表结构设计思路以及安装部署说明适合作为课设选题参考、功能模块拆解与排错对照的实践材料。1. 车站售票管理系统广工数据库课设里最值得拆的一套完整业务闭环如果你正在搜广工数据库课设、广东工业大学数据库系统课程设计大概率已经被各种“学生信息管理”“图书管理”选题刷屏了。车站售票管理系统这套题之所以被反复选是因为它天然带事务、带并发、带多表关联能逼着你把数据库增删改查之外的东西也写进报告里。它本质上是一个用关系型数据库支撑“车次查询—余票扣减—订单生成—退票回滚”的完整业务闭环适合正在做数据库课设、需要交出一份能跑通且能讲清楚原理的在校生。我拆过不少课设包这套的含金量在于它逼你面对真实系统里最容易被忽略的座位库存一致性问题而不是停留在建几张表、写几个 CRUD 界面。下面按“资源是什么—怎么落地—坑在哪”的顺序把这份课设从建库到并发扣票讲透。2. 车站售票管理系统的库表设计与选型为什么不是一张表走天下2.1 从业务动作反推实体别先急着写 CREATE TABLE很多同学拿到课设第一反应是打开 Navicat 或 dbx 数据库管理工具直接开始建表。我一般会先把业务动作列出来乘客查车次、选座、下单、支付、退票、改签。每个动作背后对应哪些数据变化才是表的来源。车站售票管理系统里最核心的实体有五个车站、车次、车厢座位、乘客、订单。注意“座位”和“车次”不是一回事同一列车不同日期跑座位库存是跟着“车次日期”走的这就是后面余票扣减容易翻车的地方。常见做法是拆成station、train、schedule车次排班、seat_inventory座位库存、passenger、ticket_order六张主表。schedule是车次和日期的交叉seat_inventory挂在schedule下面。这样设计的好处是查余票只动seat_inventory查订单只动ticket_order职责清晰。如果你把座位直接塞进train表一旦要按日期区分库存就得加一堆seat_20240101这种字段后面改都改不动。2.2 建库建表脚本字段类型和约束一次到位下面这份脚本是我按课设常见要求整理的MySQL 8.0 可直接跑。注意seat_inventory上的唯一约束和version字段这两个是后面并发扣票的关键。-- 车站表 CREATE TABLE station ( station_id INT PRIMARY KEY AUTO_INCREMENT, station_name VARCHAR(64) NOT NULL, city VARCHAR(32) NOT NULL, UNIQUE KEY uk_station_name (station_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车次表 CREATE TABLE train ( train_no VARCHAR(16) PRIMARY KEY, start_station_id INT NOT NULL, end_station_id INT NOT NULL, depart_time TIME NOT NULL, arrive_time TIME NOT NULL, FOREIGN KEY (start_station_id) REFERENCES station(station_id), FOREIGN KEY (end_station_id) REFERENCES station(station_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表车次 日期 CREATE TABLE schedule ( schedule_id BIGINT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(16) NOT NULL, run_date DATE NOT NULL, base_price DECIMAL(10,2) NOT NULL, UNIQUE KEY uk_train_date (train_no, run_date), FOREIGN KEY (train_no) REFERENCES train(train_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 座位库存表 CREATE TABLE seat_inventory ( inventory_id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, seat_type VARCHAR(16) NOT NULL, -- 商务/一等/二等 total_count INT NOT NULL, sold_count INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, UNIQUE KEY uk_schedule_seat (schedule_id, seat_type), FOREIGN KEY (schedule_id) REFERENCES schedule(schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 乘客表 CREATE TABLE passenger ( passenger_id BIGINT PRIMARY KEY AUTO_INCREMENT, id_card VARCHAR(18) NOT NULL, name VARCHAR(32) NOT NULL, phone VARCHAR(16), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE ticket_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, passenger_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, seat_type VARCHAR(16) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已退票 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), FOREIGN KEY (passenger_id) REFERENCES passenger(passenger_id), FOREIGN KEY (schedule_id) REFERENCES schedule(schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明schedule用(train_no, run_date)做唯一键保证同一车次同一天只有一条排班记录。seat_inventory用(schedule_id, seat_type)做唯一键保证每种座位类型只有一行库存。version字段是乐观锁版本号扣票时靠它防止超卖。参数上DECIMAL(10,2)存金额比FLOAT稳utf8mb4避免中文站名乱码。如果你用的是达梦数据库或 GBase语法略有差异但约束思路一致。2.3 余票查询 SQL别用 COUNT 去数订单新手最容易犯的错是查余票时写SELECT total - COUNT(*) FROM ticket_order WHERE ...。订单表一大这个查询就慢而且退票状态还得过滤。正确做法是直接读seat_inventory的total_count - sold_count。SELECT s.train_no, s.run_date, si.seat_type, si.total_count - si.sold_count AS remain FROM schedule s JOIN seat_inventory si ON si.schedule_id s.schedule_id WHERE s.train_no G1234 AND s.run_date 2024-06-01;这条查询走uk_train_date和uk_schedule_seat两个唯一索引基本是常量级响应。参数说明train_no和run_date是用户输入实际项目里要做参数化查询防注入。如果你在课设报告里要体现“数据库优化”把这条和 COUNT 版本的执行计划对比贴上去比空谈索引有效得多。3. 增删改查与事务落地把扣票写成能讲清楚的故事3.1 下单扣票的完整事务UPDATE 带条件才是防超卖核心车站售票管理系统最值钱的部分就是扣票。我见过太多课设直接UPDATE seat_inventory SET sold_count sold_count 1然后答辩时被问“两个人同时买最后一张票怎么办”就卡住。正确写法是把库存判断塞进 UPDATE 的 WHERE 里靠数据库行锁保证原子性。START TRANSACTION; -- 扣减库存只有剩余大于 0 才扣得动 UPDATE seat_inventory SET sold_count sold_count 1, version version 1 WHERE schedule_id 1001 AND seat_type 二等 AND total_count - sold_count 0; -- 检查影响行数若为 0 说明没票了 -- 应用层根据 affected_rows 决定是否回滚 INSERT INTO ticket_order (order_no, passenger_id, schedule_id, seat_type, amount, status) VALUES (T20240601001, 1, 1001, 二等, 553.00, 0); COMMIT;逻辑说明UPDATE ... WHERE total_count - sold_count 0这一句在 InnoDB 里会对命中行加排他锁第二个并发事务必须等第一个提交后才能执行此时剩余已经为 0影响行数为 0应用层捕获后回滚。参数上version字段在这里其实不是必须的因为行锁已经够了但如果你在报告里想讲乐观锁可以改成WHERE version ?的写法两者选其一即可别混用。常见做法是课设演示用行锁版本报告里对比乐观锁和悲观锁的差异。3.2 退票回滚状态机和库存要一起动退票不是简单删订单而是把订单状态改成已退票同时把库存加回去。这两步必须在同一个事务里否则会出现“票退了但库存没加”的黑匣子情况。START TRANSACTION; UPDATE ticket_order SET status 2 WHERE order_no T20240601001 AND status 1; -- 只有订单确实从已支付变为已退票才回补库存 UPDATE seat_inventory si JOIN ticket_order o ON o.schedule_id si.schedule_id AND o.seat_type si.seat_type SET si.sold_count si.sold_count - 1 WHERE o.order_no T20240601001 AND o.status 2; COMMIT;逻辑说明第一个 UPDATE 带status 1条件防止重复退票。第二个 UPDATE 通过 JOIN 定位库存行。参数上status用 TINYINT 存 0/1/2 比字符串省空间也方便加索引。如果你在课设里要做“退票手续费”在订单表加refund_fee字段退票时按开车时间差计算这部分逻辑放应用层还是存储过程都行但记得在报告里说明选择理由。3.3 用 Python 把流程串起来课设演示脚本课设通常要求有界面或至少能演示。用 Python PyMySQL 写一个最小可跑脚本比硬套 Java Swing 快得多也方便你调试 SQL。import pymysql conn pymysql.connect(hostlocalhost, userroot, passwordyour_pwd, databaseticket_system, charsetutf8mb4) def buy_ticket(order_no, passenger_id, schedule_id, seat_type, amount): with conn.cursor() as cur: conn.begin() # 扣库存 cur.execute( UPDATE seat_inventory SET sold_count sold_count 1, version version 1 WHERE schedule_id %s AND seat_type %s AND total_count - sold_count 0 , (schedule_id, seat_type)) if cur.rowcount 0: conn.rollback() return False, 余票不足 # 写订单 cur.execute( INSERT INTO ticket_order (order_no, passenger_id, schedule_id, seat_type, amount, status) VALUES (%s, %s, %s, %s, %s, 0) , (order_no, passenger_id, schedule_id, seat_type, amount)) conn.commit() return True, 下单成功 if __name__ __main__: ok, msg buy_ticket(T20240601002, 1, 1001, 二等, 553.00) print(ok, msg)逻辑说明conn.begin()显式开事务cur.rowcount判断扣库存是否成功失败就rollback。参数说明%s是 PyMySQL 的占位符不要用字符串拼接否则就是 SQL 注入的活教材。这套脚本可以直接作为课设的“业务逻辑层”上面再套个 Flask 或 Tkinter 就是完整演示。如果你用 sqllite 数据库练手把pymysql换成sqlite3事务写法基本一致但 SQLite 的并发锁粒度不同高并发场景别用它做扣票。4. 避坑与排查课设答辩前必须自己先踩一遍的五个坑4.1 现象两个人同时买最后一张票结果都成功了原因扣库存的 UPDATE 没带total_count - sold_count 0条件或者先 SELECT 查余票再 UPDATE中间有时间窗口。解决把判断塞进 UPDATE 的 WHERE 里用rowcount判断结果。这是车站售票管理系统最经典的超卖坑答辩老师十有八九会问。4.2 现象退票后库存没加回去余票越卖越少原因退票只改了订单状态忘了回补sold_count或者两个操作不在同一事务里中间报错导致只执行了一半。解决退票和回补库存放同一个事务并且回补时用status 2做条件防止重复回补。测试时手动把订单状态改回去再退一次看库存会不会多加。4.3 现象中文站名存进去变成问号原因建库时用了latin1或utf8非 utf8mb4或者连接字符串没指定 charset。解决建库建表统一utf8mb4Python 连接加charsetutf8mb4Navicat 或 dbx 数据库工具里也要确认连接编码。这个坑在报告里写一句“字符集统一为 utf8mb4”就能体现你踩过。4.4 现象外键约束导致插入订单失败原因ticket_order的passenger_id或schedule_id在父表里不存在或者插入顺序反了。解决先插station、train、schedule、passenger再插ticket_order。课设演示前用SET FOREIGN_KEY_CHECKS 0临时关掉外键可以救急但报告里别这么写会被扣分。4.5 现象并发测试时出现死锁事务被回滚原因两个事务以不同顺序更新同一批库存行比如一个先扣二等再扣一等另一个反过来。解决统一更新顺序比如按seat_type字母序或inventory_id升序更新。课设里并发量不大但如果你用 JMeter 或 Python 多线程压测这个坑很容易复现。排查时看SHOW ENGINE INNODB STATUS里的死锁日志能直接看到哪两把锁在互相等。5. 进阶技巧用存储过程和执行计划把课设报告拉高一个档次课设报告想拿高分光有能跑的代码不够得让老师看到你理解数据库在做什么。我一般会加两个东西一个存储过程封装下单一个执行计划对比。存储过程的好处是把事务逻辑收进数据库层应用层只调一个CALL报告里可以写“业务逻辑下沉减少网络往返”。DELIMITER // CREATE PROCEDURE sp_buy_ticket( IN p_order_no VARCHAR(32), IN p_passenger_id BIGINT, IN p_schedule_id BIGINT, IN p_seat_type VARCHAR(16), IN p_amount DECIMAL(10,2), OUT p_result VARCHAR(32) ) BEGIN DECLARE affected INT DEFAULT 0; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result 系统异常已回滚; END; START TRANSACTION; UPDATE seat_inventory SET sold_count sold_count 1, version version 1 WHERE schedule_id p_schedule_id AND seat_type p_seat_type AND total_count - sold_count 0; SET affected ROW_COUNT(); IF affected 0 THEN ROLLBACK; SET p_result 余票不足; ELSE INSERT INTO ticket_order (order_no, passenger_id, schedule_id, seat_type, amount, status) VALUES (p_order_no, p_passenger_id, p_schedule_id, p_seat_type, p_amount, 0); COMMIT; SET p_result 下单成功; END IF; END // DELIMITER ;逻辑说明EXIT HANDLER捕获异常后回滚并返回提示ROW_COUNT()拿影响行数。参数上OUT p_result把结果传回应用层。调用方式CALL sp_buy_ticket(T003, 1, 1001, 二等, 553.00, res); SELECT res;。报告里可以对比存储过程和 Python 脚本两种实现说明各自适用场景。另一个加分项是执行计划对比。把前面余票查询的EXPLAIN结果贴出来重点看type是不是ref或constkey有没有命中唯一索引。如果看到ALL全表扫描就回去检查索引。我习惯在课设验收前把每条核心 SQL 都跑一遍EXPLAIN确认没有全表扫描。从那以后我每次交课设前都强制走一遍“建库脚本→事务测试→并发测试→执行计划检查”这四步少一步都可能在答辩现场翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表