ARTICLE DETAIL

资讯详情

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

广工数据库课设车站售票管理系统:表结构设计与并发扣票实战

广工数据库课设车站售票管理系统:表结构设计与并发扣票实战 简介这份资源是广东工业大学数据库系统课程设计的个人选题方案——车站售票管理系统面向正在准备数据库课设的本科生及需要Java数据库综合练习的开发者。系统围绕售票与退票核心业务展开涵盖车次查询、时刻表查询、售票情况统计等常用功能并实现数据备份与恢复、操作员管理、权限设置等维护模块可作为课设参考或二次开发基础。压缩包共100个文件约4.65MB其中72个class与21个java构成完整可运行的Java工程另含1个sql建库脚本、1个jar依赖及doc安装说明书、txt说明等源码与文档配套齐全。目前已有1154人学习下载说明该方案在同类课设中具有较高参考价值。读者可据此快速理解售票系统的表结构设计、功能划分与实现思路对照安装说明完成环境部署并在此基础上调整需求或扩展统计查询模块节省从零搭建的时间成本。1. 车站售票管理系统广工数据库课设到底在考什么广工数据库课设选「车站售票管理系统」这个题表面上是让你做一个卖票的小软件实际上老师想看的是你能不能把数据库这一套东西真正用起来——建库建表、写约束、做增删改查、处理并发、写存储过程最后再套一个能跑起来的前端。很多同学一上来就急着写界面结果表结构设计得一塌糊涂后面改到崩溃。我带过几届做这个题的人血泪经验就一句话先把 ER 图和表结构定死再动手写代码。这个系统典型角色有三类乘客查票买票退票、售票员开窗卖票、管理员维护车次和站点。核心业务是「一趟车次在某个日期某个区间还剩几张票」难点全在余票的并发扣减和订单状态流转上。适合正在做课设、想拿高分又不想返工的同学也适合想借这个题把 MySQL 增删改查和事务真正练一遍的人。下面我按「设计 → 建库 → 核心逻辑 → 避坑 → 进阶」的顺序把能直接抄作业的东西讲清楚。2. 表结构怎么设计才不会被老师打回2.1 先画 ER 图再定五张核心表这个题最容易翻车的地方就是表设计。很多人把「车次」和「余票」混在一张表里结果一改车次就要动一堆数据。正确的拆法是按实体拆车站、车次、车次经停站、订单、乘客。车次和车站是多对多关系中间用「经停站表」拆开同时把区间票价和到达时间挂在这张中间表上。订单表关联乘客和具体车次余票不单独存一张表而是通过「车次总座位数 - 已售订单数」实时算或者用一张余票表加乐观锁。我一般推荐后者因为课设答辩时老师爱问并发有张余票表好讲。下面是我常用的建表顺序先建被引用的表再建引用别人的表避免外键报错-- 车站表所有站点的基础信息 CREATE TABLE station ( station_id INT PRIMARY KEY AUTO_INCREMENT, station_name VARCHAR(50) NOT NULL UNIQUE, city VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车次表一趟车的整体信息 CREATE TABLE train ( train_id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(20) NOT NULL UNIQUE, -- 如 G1234 start_station_id INT NOT NULL, end_station_id INT NOT NULL, depart_time TIME NOT NULL, total_seats INT NOT NULL DEFAULT 500, FOREIGN KEY (start_station_id) REFERENCES station(station_id), FOREIGN KEY (end_station_id) REFERENCES station(station_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明station表用station_name做唯一约束防止同一个站被录两次。train表里total_seats是这趟车的总定员后面算余票要用。参数上train_no设成唯一是因为现实里车次号不会重复老师也爱拿这个考你唯一约束。ENGINEInnoDB必须写MyISAM 不支持事务后面讲并发扣票直接没法做。2.2 经停站表和订单表是重头戏经停站表决定了区间怎么算。一趟车从 A 到 D中间经停 B、C那 A→B、A→C、B→D 都是合法区间。这张表要存站序靠站序判断谁在前谁在后-- 车次经停站表记录每趟车经过哪些站、第几站、从起点算的累计里程 CREATE TABLE train_stop ( stop_id INT PRIMARY KEY AUTO_INCREMENT, train_id INT NOT NULL, station_id INT NOT NULL, stop_order INT NOT NULL, -- 站序从 1 开始 arrive_time TIME, depart_time TIME, mileage INT DEFAULT 0, -- 累计里程用来算票价 UNIQUE KEY uk_train_order (train_id, stop_order), FOREIGN KEY (train_id) REFERENCES train(train_id), FOREIGN KEY (station_id) REFERENCES station(station_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表一张票一条记录 CREATE TABLE ticket_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, train_id INT NOT NULL, passenger_id INT NOT NULL, from_stop INT NOT NULL, -- 上车站序 to_stop INT NOT NULL, -- 下车站序 seat_no VARCHAR(10), price DECIMAL(8,2) NOT NULL, status TINYINT DEFAULT 1, -- 1已支付 2已退票 3已改签 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (train_id) REFERENCES train(train_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明train_stop上的uk_train_order唯一键保证同一趟车不会有两个「第 3 站」这是数据一致性的关键。ticket_order里用from_stop和to_stop存站序而不是站 ID是为了后面判断区间重叠时直接比大小省一次关联查询。status用 TINYINT 而不是字符串省空间也方便加索引。参数上price用DECIMAL(8,2)别用 FLOAT金额算着算着就出现 0.0000001 的误差答辩被问到很尴尬。2.3 余票表加唯一约束防超卖余票如果每次现算高并发下会超卖。我一般单独建一张按「车次 日期 区间」粒度的余票表并加唯一约束CREATE TABLE seat_inventory ( inv_id BIGINT PRIMARY KEY AUTO_INCREMENT, train_id INT NOT NULL, travel_date DATE NOT NULL, from_stop INT NOT NULL, to_stop INT NOT NULL, remain INT NOT NULL, version INT DEFAULT 0, -- 乐观锁版本号 UNIQUE KEY uk_inv (train_id, travel_date, from_stop, to_stop) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明uk_inv保证同一趟车同一天同一区间只有一条余票记录扣票时用UPDATE ... WHERE remain 1或版本号乐观锁。version字段是给乐观锁用的后面第 4 章会讲怎么用。这张表的数据可以在生成车次时批量初始化也可以懒加载——第一次卖某区间票时再插入。3. 增删改查和事务把卖票逻辑写对3.1 查询余票和区间判断的 SQL 怎么写乘客查票的本质是给定出发站、到达站、日期找出所有经停这两站且站序满足 from to 的车次再关联余票。核心 SQL 如下-- 查询 2025-06-01 从广州到武汉的所有车次及余票 SELECT t.train_no, s1.station_name AS from_station, s2.station_name AS to_station, ts1.depart_time, ts2.arrive_time, (ts2.mileage - ts1.mileage) * 0.5 AS price, inv.remain FROM train_stop ts1 JOIN train_stop ts2 ON ts1.train_id ts2.train_id AND ts1.stop_order ts2.stop_order JOIN train t ON t.train_id ts1.train_id JOIN station s1 ON s1.station_id ts1.station_id JOIN station s2 ON s2.station_id ts2.station_id LEFT JOIN seat_inventory inv ON inv.train_id t.train_id AND inv.travel_date 2025-06-01 AND inv.from_stop ts1.stop_order AND inv.to_stop ts2.stop_order WHERE s1.station_name 广州 AND s2.station_name 武汉;逻辑说明ts1和ts2是同一张经停站表的两次自连接ts1.stop_order ts2.stop_order保证出发站在前。票价这里用里程差乘 0.5 简单模拟真实项目会有分段计价表。LEFT JOIN余票表是因为可能还没人买过这个区间余票记录不存在用 LEFT JOIN 让它显示 NULL前端再显示「有票」。参数上travel_date是查询条件实际项目里要加索引(train_id, travel_date)不然数据一多全表扫。3.2 下单扣票必须放在一个事务里卖票最怕的就是「扣了票没生成订单」或者「生成了订单没扣票」。这两步必须在一个事务里要么都成功要么都回滚START TRANSACTION; -- 1. 扣减余票remain 1 才允许扣防止扣成负数 UPDATE seat_inventory SET remain remain - 1, version version 1 WHERE train_id 1001 AND travel_date 2025-06-01 AND from_stop 1 AND to_stop 3 AND remain 1; -- 2. 检查上一步是否真的扣到了受影响行数 -- 如果 affected_rows 0说明没票了回滚 -- 3. 插入订单 INSERT INTO ticket_order (train_id, passenger_id, from_stop, to_stop, price, status) VALUES (1001, 2001, 1, 3, 120.00, 1); COMMIT;逻辑说明UPDATE ... WHERE remain 1是防超卖的第一道防线数据库行锁保证同一行不会被两个事务同时扣。第二步在应用层判断affected_rows如果是 0 就ROLLBACK并提示「余票不足」。参数上version自增是为了配合乐观锁如果不用乐观锁可以去掉。注意START TRANSACTION和COMMIT之间不要做网络请求或耗时操作否则锁持有时间太长并发一高就死锁。3.3 退票和改签的状态流转退票不是删订单是把status改成 2同时把余票加回去。改签更复杂要先退旧票再占新票两个操作也要在一个事务里START TRANSACTION; -- 退票状态改为已退票余票加回 UPDATE ticket_order SET status 2 WHERE order_id 5001 AND status 1; UPDATE seat_inventory SET remain remain 1 WHERE train_id 1001 AND travel_date 2025-06-01 AND from_stop 1 AND to_stop 3; COMMIT;逻辑说明WHERE status 1保证只有已支付的票能退重复退票第二次affected_rows为 0应用层据此提示「该票已退」。余票加回时不用判断remain 1因为加不会超。参数上退票和改签建议加一个「退票记录表」留痕答辩时老师问「退票历史怎么查」你能答上来。4. 并发扣票和常见坑课设答辩最爱问的地方4.1 超卖是怎么发生的怎么复现超卖的经典场景两个乘客同时买最后一张票。如果代码写成「先 SELECT 查余票再 UPDATE 扣减」两个事务都查到 remain1都以为有票结果扣成 -1。复现方法很简单开两个 MySQL 客户端手动模拟-- 会话 A START TRANSACTION; SELECT remain FROM seat_inventory WHERE inv_id 1; -- 查到 1 -- 先不提交 -- 会话 B START TRANSACTION; SELECT remain FROM seat_inventory WHERE inv_id 1; -- 也查到 1 UPDATE seat_inventory SET remain remain - 1 WHERE inv_id 1; COMMIT; -- 回到会话 A UPDATE seat_inventory SET remain remain - 1 WHERE inv_id 1; COMMIT; -- 结果 remain -1超卖原因就是「查」和「改」之间没有锁住。解决办法有两个一是把扣减写成UPDATE ... WHERE remain 1原子操作二是用SELECT ... FOR UPDATE悲观锁。课设里推荐第一种简单且够用。4.2 避坑清单五个我踩过的坑坑一外键顺序建反导致建表失败。现象是ERROR 1215: Cannot add foreign key constraint。原因是先建了ticket_order再建train引用了一张还不存在的表。解决是按依赖顺序建表或者先建所有表再统一ALTER TABLE ADD FOREIGN KEY。坑二用 FLOAT 存票价出现精度误差。现象是两张票加起来 119.99999。原因是 FLOAT 是二进制浮点存不了精确小数。解决是金额一律用DECIMAL(8,2)Java 侧用BigDecimal接收。坑三事务里做了耗时操作导致锁等待超时。现象是并发测试时报Lock wait timeout exceeded。原因是在START TRANSACTION和COMMIT之间调了外部接口或打印了大量日志。解决是把非数据库操作挪到事务外事务里只留 SQL。坑四余票表没加唯一约束同一区间出现多条记录。现象是查余票时remain对不上。原因是初始化脚本跑了两次。解决是加uk_inv唯一键重复插入直接报错逼你写INSERT ... ON DUPLICATE KEY UPDATE。坑五日期字段用 VARCHAR 存。现象是WHERE travel_date 2025-6-1查不到2025-06-01的数据。原因是字符串比较不做日期归一化。解决是老老实实用DATE类型前端传参统一格式。4.3 用 EXPLAIN 看你的查询有没有走索引课设数据量小的时候查什么都快但老师会问「数据量大了怎么办」。这时候你要能掏出EXPLAINEXPLAIN SELECT * FROM ticket_order WHERE train_id 1001 AND status 1;如果type是ALL说明全表扫要加索引(train_id, status)。如果key显示你用上了索引rows估算值很小就说明没问题。参数上possible_keys列出候选索引key是实际用的Extra里出现Using filesort或Using temporary就要警惕通常意味着排序或分组没走索引。5. 存储过程和触发器让课设多拿几分5.1 写一个自动算票价的存储过程老师喜欢看存储过程因为能体现你会写 PL/SQL。下面这个按里程算票价里程差每公里 0.5 元最低 5 元DELIMITER // CREATE PROCEDURE calc_price( IN p_train_id INT, IN p_from_stop INT, IN p_to_stop INT, OUT p_price DECIMAL(8,2) ) BEGIN DECLARE v_mileage INT; SELECT (ts2.mileage - ts1.mileage) INTO v_mileage FROM train_stop ts1 JOIN train_stop ts2 ON ts1.train_id ts2.train_id WHERE ts1.train_id p_train_id AND ts1.stop_order p_from_stop AND ts2.stop_order p_to_stop; SET p_price GREATEST(v_mileage * 0.5, 5.00); END // DELIMITER ;逻辑说明DELIMITER //是为了让 MySQL 把整个存储过程当成一条语句不然遇到分号就截断了。INTO v_mileage把查询结果赋给变量GREATEST保证最低票价 5 元。调用方式是CALL calc_price(1001, 1, 3, price); SELECT price;。参数上OUT类型的参数用来返回结果Java 侧用CallableStatement注册Types.DECIMAL接收。5.2 触发器记录余票变更日志答辩时如果老师问「怎么追踪余票变化」触发器是加分项CREATE TABLE inventory_log ( log_id BIGINT PRIMARY KEY AUTO_INCREMENT, inv_id BIGINT, old_remain INT, new_remain INT, change_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; DELIMITER // CREATE TRIGGER trg_inventory_update AFTER UPDATE ON seat_inventory FOR EACH ROW BEGIN IF OLD.remain NEW.remain THEN INSERT INTO inventory_log (inv_id, old_remain, new_remain) VALUES (OLD.inv_id, OLD.remain, NEW.remain); END IF; END // DELIMITER ;逻辑说明AFTER UPDATE表示更新之后触发OLD和NEW分别代表改前和改后的行。IF判断只有余票真变了才记日志避免无关更新刷屏。参数上触发器会增加写开销课设数据量小无所谓生产环境要谨慎一般用应用层记日志替代。5.3 用视图简化前端查询前端不想写复杂 JOIN可以建视图CREATE VIEW v_train_schedule AS SELECT t.train_no, s1.station_name AS from_station, s2.station_name AS to_station, ts1.depart_time, ts2.arrive_time, ts1.stop_order AS from_order, ts2.stop_order AS to_order FROM train_stop ts1 JOIN train_stop ts2 ON ts1.train_id ts2.train_id AND ts1.stop_order ts2.stop_order JOIN train t ON t.train_id ts1.train_id JOIN station s1 ON s1.station_id ts1.station_id JOIN station s2 ON s2.station_id ts2.station_id;逻辑说明视图把自连接逻辑封装起来前端直接SELECT * FROM v_train_schedule WHERE from_station广州。参数上视图不存数据每次查都执行底层 SQL性能取决于底层索引。课设里用视图能让代码更干净答辩也好讲。6. 从课设到能跑的系统连接池和压测小技巧课设最后要交一个能演示的系统很多人卡在「本地跑得好好的一演示就卡」。问题多半出在数据库连接上。JDBC 每次DriverManager.getConnection都新建物理连接几十个并发就顶不住。加一个连接池是性价比最高的优化HikariCP 配置如下HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/ticket_db?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); // 最大连接数课设 10 够用 config.setMinimumIdle(2); // 最小空闲连接 config.setConnectionTimeout(3000); // 获取连接超时 3 秒 config.setIdleTimeout(60000); // 空闲连接 60 秒回收 HikariDataSource ds new HikariDataSource(config);逻辑说明maximumPoolSize不是越大越好MySQL 默认最大连接 151设太大反而拖慢。connectionTimeout设 3 秒拿不到连接快速失败别让请求堆着。参数上serverTimezone必须写不然 MySQL 8 会报时区错误这是新手最常见的翻车点。压测不用上 JMeter写个简单的多线程脚本就能验证扣票逻辑import threading, pymysql def buy_ticket(): conn pymysql.connect(hostlocalhost, userroot, passwordpwd, databaseticket_db) cur conn.cursor() cur.execute(UPDATE seat_inventory SET remain remain - 1 WHERE inv_id 1 AND remain 1) if cur.rowcount 1: cur.execute(INSERT INTO ticket_order (train_id, passenger_id, from_stop, to_stop, price) VALUES (1001, 1, 1, 3, 120)) conn.commit() conn.close() threads [threading.Thread(targetbuy_ticket) for _ in range(50)] for t in threads: t.start() for t in threads: t.join()逻辑说明开 50 个线程同时抢同一张余票跑完查remain和订单数如果remain没变负、订单数等于初始余票数说明扣票逻辑正确。参数上remain 1是防超卖的关键去掉它再跑一次就能看到负数这个对比演示在答辩时特别有说服力。我自己的习惯是每次改完扣票逻辑先跑一遍这个 50 线程脚本确认没超卖再往下做界面。课设这东西界面丑一点老师能忍数据错了直接挂。把表结构、事务、并发这三块啃下来广工这个题拿高分不难。希望帮到你。本文还有配套的精品资源点击获取
返回列表