简介:这是一套面向高校计算机相关专业学生的Java课程设计与毕业设计参考项目,围绕学生火车票订票业务展开,涵盖学生基本信息与目的地管理、订票信息(含票价与车票目的地)维护、退票处理、信息统计查询以及操作员管理等核心模块,适合作为课程设计、毕业设计或项目开发练手素材。资源包共72个文件,约8.87MB,包含9个java源码文件、16个xml配置、15个jar依赖、7个properties配置、11张png运行截图,以及项目文档、参考论文pdf和README说明,源码已通过测试,可放心参考并在此基础上延伸扩展。开发环境为IDEA 2016.3、SQL Server 2014与JavaFX,技术栈涉及Hibernate持久化框架,目录结构清晰,便于按模块检索学习。目前已有40人学习下载,读者可借助源码、文档与论文快速理解订票系统的整体设计与实现思路,对照截图验证功能效果,并在此基础上完成二次开发或撰写自己的设计报告。
1. 学生火车票订票系统:从课程设计到毕业设计,Java 这套方案到底能不能打
每年到了毕业季和期末,总有一批计算机专业的学生在搜索框里敲下「学生火车票订票系统 Java 源码」。这个标题背后其实藏着三类人:第一类是课程设计 deadline 逼近、需要一套能跑起来、能讲清楚、能应付答辩的完整项目;第二类是毕业设计选题刚定,想找一个业务逻辑不复杂但技术栈完整的题目;第三类是想拿它当 Java Web 练手项目,把 SSM 或 Spring Boot 的增删改查真正串一遍。这三类人的共同诉求很明确——要能跑、要有文档、要有论文参考、最好还能改。
火车票订票这个场景天然适合做教学项目:它有用户、车次、订单、余票四个核心实体,有查询、下单、支付、退票四条主流程,还涉及并发扣减库存这个经典问题。相比外卖、商城这类项目,它的业务边界更清晰,数据库表不超过十张,答辩时老师问不倒你。但坑也在这里——很多人直接拿网上的半成品改,结果发现订单状态对不上、余票扣成负数、学生优惠逻辑根本没实现。这篇笔记就按「先讲清楚系统该有什么,再一步步把核心模块跑通,最后把踩过的坑摊开说」的顺序来,源码和文档的获取方式放在对应章节里,不单独列清单。
2. 先定架构再写代码:学生票场景下的技术选型与数据库设计
2.1 为什么我建议用 Spring Boot + MyBatis 而不是纯 JSP
很多课程设计模板还在用 JSP + Servlet + JDBC 的原始组合,理由是「老师只教了这个」。但如果你想让项目在答辩时显得有技术含量,同时自己写起来不那么痛苦,Spring Boot + MyBatis + Thymeleaf 是目前最稳的选择。原因有三点:第一,Spring Boot 的自动配置让你不用再手写 web.xml 和一堆 XML 映射文件,启动类一跑就能访问接口;第二,MyBatis 的 XML 映射对复杂查询(比如「查某天从 A 到 B 且有余票的车次」)比 JPA 更直观,也更容易在论文里画 ER 图对应;第三,Thymeleaf 虽然不如 Vue 流行,但它和 Spring Boot 天然集成,不需要前后端分离就能做出能看的页面,省掉跨域和 token 管理的麻烦。
如果你时间充裕,想冲优秀毕业设计,可以上 Spring Boot + Vue 前后端分离。但要注意,分离之后你需要额外处理登录态(JWT 或 Session 跨域)、接口文档(Swagger)、前端路由权限,工作量至少翻倍。我一般建议:课程设计用 Thymeleaf 单体,毕业设计如果导师不强制要求分离,也用单体,把精力花在业务逻辑的完整性上。
2.2 数据库表设计:五张核心表撑起整个系统
学生火车票订票系统的表不用多,但字段要想清楚。下面是我实际用过的建表语句,直接可以在 MySQL 8.0 里执行。注意学生优惠的逻辑我放在订单表里用ticket_type区分,而不是单独建学生表,这样查票和下单的关联更简单。
-- 用户表:区分普通用户和管理员 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'MD5加密存储', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名,取票用', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `student_no` varchar(20) DEFAULT NULL COMMENT '学号,学生票校验用', `role` tinyint DEFAULT '0' COMMENT '0-学生 1-管理员', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 车次表:一趟车一条记录 CREATE TABLE `train` ( `id` int NOT NULL AUTO_INCREMENT, `train_no` varchar(20) NOT NULL COMMENT '车次号,如G1234', `start_station` varchar(50) NOT NULL, `end_station` varchar(50) NOT NULL, `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `total_seats` int NOT NULL DEFAULT '0', `remaining_seats` int NOT NULL DEFAULT '0', `price` decimal(10,2) NOT NULL COMMENT '全价票', PRIMARY KEY (`id`), KEY `idx_route_time` (`start_station`,`end_station`,`start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:核心表,状态机在这里 CREATE TABLE `orders` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `user_id` int NOT NULL, `train_id` int NOT NULL, `ticket_type` tinyint DEFAULT '0' COMMENT '0-全价 1-学生票', `amount` decimal(10,2) NOT NULL COMMENT '实付金额', `status` tinyint DEFAULT '0' COMMENT '0-待支付 1-已支付 2-已退票 3-已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时最容易翻车的地方是remaining_seats字段。很多人把它设成int但不加非负约束,结果并发下单时扣成负数。我的做法是在 SQL 更新时加AND remaining_seats > 0,而不是只靠 Java 代码判断。另外orders表的status字段一定要用数字枚举,不要用字符串,否则后面写状态流转的 if-else 会写到崩溃。
2.3 项目文档和论文该写哪些章节
课程设计文档一般要求:需求分析、系统设计、数据库设计、详细实现、测试、总结。毕业设计论文在此基础上加绪论、相关技术介绍、参考文献。我的经验是,需求分析里一定要画用例图,系统设计里一定要画架构图和 ER 图,详细实现里至少贴三段核心代码(登录、查票、下单),测试里要有并发测试的截图。这些图用 Visio 或 draw.io 画,不要直接截图代码当图,老师一眼就能看出来。
参考论文不要直接抄知网,查重过不了。正确做法是找三到五篇同方向的硕士论文,看它们的章节结构和术语表达,然后用自己项目的实际数据重写。比如别人写「基于协同过滤的推荐」,你就写「基于固定车次查询的订票流程」,把技术名词换成你真正实现的东西。
3. 核心模块跑通:登录、查票、下单、退票四步走
3.1 登录与拦截器:别把密码明文存数据库
登录模块看起来简单,但答辩时老师最爱问「你的密码怎么存的」。如果你回答明文,基本就凉了。正确做法是 MD5 加盐或者 BCrypt。课程设计用 MD5 就够了,但要在论文里写清楚「使用 MD5 摘要算法,不可逆」。下面是一个基于 Spring Boot 拦截器的登录校验实现。
// LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录页和静态资源 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/css") || uri.contains("/js")) { return true; } HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { // 未登录跳转,注意不要用 sendRedirect 死循环 response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }这段代码的关键在preHandle的放行逻辑。很多人的拦截器把登录接口也拦了,导致永远跳登录页。参数上注意request.getContextPath(),如果你的项目部署在/ticket路径下,不加这个会跳到根路径。注册拦截器时在WebMvcConfigurer里addInterceptor并addPathPatterns("/**"),排除路径用excludePathPatterns写清楚。
3.2 车次查询:多条件组合查询的 SQL 怎么写
查票是用户用得最多的功能,输入出发站、到达站、日期,返回车次列表。这里不要用 MyBatis-Plus 的QueryWrapper硬拼,直接在 XML 里写动态 SQL 更清晰,也方便你在论文里展示。
<!-- TrainMapper.xml --> <select id="searchTrains" resultType="com.ticket.entity.Train"> SELECT * FROM train <where> <if test="startStation != null and startStation != ''"> AND start_station = #{startStation} </if> <if test="endStation != null and endStation != ''"> AND end_station = #{endStation} </if> <if test="date != null"> AND DATE(start_time) = #{date} </if> AND remaining_seats > 0 </where> ORDER BY start_time ASC </select>注意remaining_seats > 0这个条件必须加,否则用户会查到已经没票的车次,点进去下单才报错,体验很差。DATE(start_time)函数在数据量大时会导致索引失效,但教学项目数据量小,可以接受。如果想让查询更快,可以在start_time上建索引,然后把日期条件改成start_time BETWEEN #{date} 00:00:00 AND #{date} 23:59:59。
3.3 下单与余票扣减:并发场景下的正确姿势
下单是整个系统最容易出 bug 的地方。典型错误写法是先SELECT查余票,然后在 Java 里判断> 0,再UPDATE扣减。这种写法在并发下必然超卖。正确做法是用一条 SQL 完成「判断并扣减」。
// OrderService.java @Transactional(rollbackFor = Exception.class) public String createOrder(Integer userId, Integer trainId, Integer ticketType) { // 1. 原子扣减余票,返回影响行数 int affected = trainMapper.reduceSeat(trainId); if (affected == 0) { throw new RuntimeException("余票不足,下单失败"); } // 2. 计算金额,学生票打七五折 Train train = trainMapper.selectById(trainId); BigDecimal amount = train.getPrice(); if (ticketType == 1) { amount = amount.multiply(new BigDecimal("0.75")); } // 3. 生成订单 Orders order = new Orders(); order.setOrderNo(UUID.randomUUID().toString().replace("-", "")); order.setUserId(userId); order.setTrainId(trainId); order.setTicketType(ticketType); order.setAmount(amount); order.setStatus(0); orderMapper.insert(order); return order.getOrderNo(); }对应的reduceSeat方法在 Mapper 里这样写:
<update id="reduceSeat"> UPDATE train SET remaining_seats = remaining_seats - 1 WHERE id = #{trainId} AND remaining_seats > 0 </update>@Transactional保证扣减和插入订单在同一个事务里,任何一步失败都回滚。affected == 0说明余票不够,直接抛异常。学生票的折扣逻辑我放在 Java 里算,因为不同车次可能有不同折扣规则,放 SQL 里不好维护。注意BigDecimal的乘法要用multiply,不要用*,否则精度会丢。
3.4 退票与状态流转:订单状态机要画清楚
退票不是简单删记录,而是把订单状态从「已支付」改成「已退票」,同时把余票加回去。状态流转必须严格:待支付 → 已支付 → 已退票,或者待支付 → 已取消。不能从已退票再变回已支付。
@Transactional(rollbackFor = Exception.class) public void refund(String orderNo) { Orders order = orderMapper.selectByOrderNo(orderNo); if (order.getStatus() != 1) { throw new RuntimeException("只有已支付的订单才能退票"); } // 更新订单状态 order.setStatus(2); orderMapper.updateById(order); // 余票加回 trainMapper.addSeat(order.getTrainId()); }退票接口要做权限校验,只能退自己的订单。在 Controller 里从 Session 取userId,和订单的userId比对,不一致直接返回 403。这个细节答辩时老师很可能会问,提前准备好说辞。
4. 避坑与排查:学生票系统开发中最容易翻车的五个点
4.1 余票扣成负数,并发测试必现
现象:用 JMeter 开 50 个线程同时下单,最后remaining_seats变成 -3。原因:先查后改的写法在并发下多个线程读到相同的余票值,都判断为「有票」,然后各自扣减。解决:把判断和扣减合并成一条UPDATE ... WHERE remaining_seats > 0,用影响行数判断是否成功。如果项目要求更高,可以上 Redis 预扣减,但课程设计用数据库乐观锁足够。
4.2 学生票优惠没有校验学号,被老师当场问住
现象:任何用户下单时都能选学生票,享受七五折。原因:下单接口只接收ticketType参数,没有校验当前用户是否真的有学号。解决:在createOrder里加判断,如果ticketType == 1,查用户的student_no字段,为空则拒绝。同时在前端把学生票选项置灰。这个逻辑写进论文的「业务规则」一节,能体现你考虑过真实场景。
4.3 日期查询查不到当天的车次
现象:数据库里明明有今天出发的车次,但用户选今天查不出来。原因:前端传的日期是2025-06-01,数据库存的是2025-06-01 08:00:00,用=比较不相等。解决:用DATE(start_time) = #{date}或者start_time BETWEEN #{date} 00:00:00 AND #{date} 23:59:59。推荐后者,能走索引。改完之后记得清一下 MyBatis 的缓存,否则可能还是旧结果。
4.4 订单号重复导致插入失败
现象:偶尔报Duplicate entry错误,订单创建失败。原因:用时间戳或随机数生成订单号,高并发下可能重复。解决:用UUID.randomUUID().toString().replace("-", "")生成 32 位字符串,重复概率极低。或者用「日期 + 用户 ID + 随机数」的组合。订单号字段加唯一索引,让数据库兜底。
4.5 退票后余票加回,但车次已经发车了
现象:用户退了一张昨天出发的票,系统还把余票加回去了。原因:退票逻辑没有校验发车时间。解决:在refund方法里加判断,如果train.getStartTime()早于当前时间,拒绝退票。这个规则要在需求文档里写清楚:「发车前 30 分钟可退票,发车后不可退」。具体时间阈值根据业务定,教学项目写「发车后不可退」即可。
5. 从能跑到能答辩:三个让项目加分的技术细节
5.1 用 AOP 记录操作日志,答辩时演示有亮点
课程设计里很少有人加日志,但这是一个成本低、效果好的加分项。用 Spring AOP 拦截所有 Service 方法,把方法名、参数、执行时间、操作人写进数据库或日志文件。演示的时候打开日志页面,能看到「用户张三在 10:23:15 查询了北京到上海的车次,耗时 45ms」,老师会觉得你的项目有工程化思维。
@Aspect @Component public class LogAspect { @Around("execution(* com.ticket.service.*.*(..))") public Object log(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; // 这里可以写入数据库或 logback System.out.println(joinPoint.getSignature().getName() + " 耗时 " + cost + "ms"); return result; } }注意切点表达式不要拦截 Controller,否则日志太杂。只拦截 Service 层,记录核心业务操作。日志表可以简单点,只要id、method、params、cost、create_time五个字段。
5.2 用 Postman 做接口测试,把截图放进论文
论文的测试章节不要只写「功能正常」,要有具体的测试用例和结果。用 Postman 建一个 Collection,把登录、查票、下单、退票四个接口都跑一遍,每个接口保存请求和响应截图。测试用例表可以这样写:
| 用例编号 | 接口 | 输入 | 预期输出 | 实际结果 |
|---|---|---|---|---|
| TC-01 | /login | 正确用户名密码 | 跳转首页 | 通过 |
| TC-02 | /login | 错误密码 | 提示密码错误 | 通过 |
| TC-03 | /train/search | 北京-上海 2025-06-01 | 返回车次列表 | 通过 |
| TC-04 | /order/create | 余票为 0 的车次 | 提示余票不足 | 通过 |
| TC-05 | /order/refund | 已支付订单 | 状态变已退票 | 通过 |
这张表直接放进论文,比大段文字描述管用。注意实际结果要真的跑一遍再填,不要编。
5.3 源码和文档怎么整理,让接手的人能跑起来
最后说一个很多人忽略的点:你交上去的源码,老师或学弟学妹要能跑起来。我一般会在项目根目录放一个README.md,写清楚四件事:环境要求(JDK 1.8、MySQL 8.0、Maven 3.6)、数据库初始化脚本位置、启动命令、默认账号密码。数据库脚本单独放sql/init.sql,不要和代码混在一起。
# 启动步骤 mysql -u root -p < sql/init.sql mvn clean package -DskipTests java -jar target/ticket-system-1.0.jar # 访问 http://localhost:8080 # 管理员账号 admin / 123456配置文件里的数据库密码不要写死,用application-dev.yml和application-prod.yml分开。答辩演示用 dev,交上去的源码里把 prod 的密码改成占位符。这些细节看起来小,但能让你在答辩时少被问很多环境问题。
我自己做这套系统的时候,最大的教训是:不要一开始就追求功能多,先把「登录 → 查票 → 下单 → 退票」这条主链路跑通,再往上加学生优惠、日志、后台管理。主链路不稳,加再多功能都是空中楼阁。希望帮到你。
本文还有配套的精品资源,点击获取