简介:这份资源是面向Java Web初学者与课程设计、毕业设计学习者的车票预订信息系统完整项目源码,围绕Java、Servlet、JSP与MySQL构建Web预订平台,帮助读者理解分层开发、请求处理与数据库交互的完整流程。压缩包共1337个文件,约55.82MB,以js、png、gif、jpg、css等前端静态资源为主,包含57个jsp页面、47个html与33个htm页面,另有31个jar依赖、9个java源文件及sql脚本、properties配置等,覆盖界面展示、业务逻辑与数据存储各环节。项目已有1105人学习,配套视频讲解可辅助完成环境配置、项目导入、数据库连接与运行测试。读者可据此掌握Servlet请求响应、JSP表现层分离及JDBC增删改查等核心技能,并参考现有目录结构快速搭建与调试自己的Web应用,适合作为课程实践与毕设起步的参考案例。
1. 从一张排队买票的照片说起:这套车票预订系统到底解决什么问题
节假日去车站窗口排队,最怕的不是人多,而是排到你的时候票没了。这个场景背后其实是一个典型的信息不对称问题:余票数据在后台,购票人却只能靠窗口一条条问。基于 WEB 的车票预订信息系统要做的,就是把这条链路搬到浏览器里,让查票、选座、下单、退改签在一个页面里闭环。这套方案适合两类人:一类是要交毕设或课程设计的学生,需要一套结构完整、能跑通、能讲清楚的 JAVA + JSP 项目;另一类是从业者,想拿一套现成的车票预订管理系统做二次改造。它不追求高并发秒杀,而是把「车次查询—余票校验—订单生成—退票回滚」这条主业务链做扎实,这才是这类系统真正的交付价值。
2. 需求拆解与技术选型:为什么是 JAVA + JSP 这套组合
2.1 先把角色和业务边界划清楚
做车票预订系统,最容易翻车的地方不是写代码,而是一上来就堆功能。我一般会先把角色分成三类:乘客、售票员、系统管理员。乘客关心的是查得到、订得上、退得掉;售票员关心的是代售和订单核验;管理员关心的是车次维护和用户管理。把这三类人的动作列成一张表,需求边界就出来了。
| 角色 | 核心动作 | 对应模块 |
|---|---|---|
| 乘客 | 注册登录、查车次、选座下单、查看订单、退票 | 用户模块、查询模块、订单模块 |
| 售票员 | 代客下单、订单核验、余票调整 | 订单模块、车次模块 |
| 管理员 | 车次增删改、用户管理、数据统计 | 后台管理模块 |
这张表的价值在于,它直接决定了后面数据库表怎么拆、JSP 页面怎么分。很多同学做课设时把「管理员」和「乘客」混在一个登录入口里,结果权限判断写得到处都是,后期改一个功能要动五个文件,血泪经验。
2.2 技术栈选型:够用、能讲、好验收
这套系统选 JAVA + JSP,不是因为它新,而是因为它对毕设场景最友好。后端用 Servlet + JavaBean 承载业务逻辑,JSP 负责视图渲染,数据库用 MySQL,连接走 JDBC。这套组合的好处是每一层都看得见摸得着,答辩时老师问「请求怎么走到数据库的」,你能从浏览器地址栏一路讲到 ResultSet。
具体分层是这样的:JSP 页面只做展示和表单提交,Servlet 做请求分发和参数校验,JavaBean 封装车次、订单、用户这些实体,DAO 层用 JDBC 拼 SQL 访问 MySQL。没有引入 Spring 那套容器,是因为课设阶段把 Servlet 生命周期讲透,比会配几个注解更有说服力。如果项目要求里明确写了要用框架,那就在 DAO 层之上加一层简易的工厂模式,别硬上全家桶。
提示:技术选型要在开题报告里写清楚「为什么不用更重的框架」,理由落在「教学场景可控、依赖少、部署简单」上,比空谈先进更站得住。
2.3 数据库设计:五张核心表撑起主业务链
车票预订系统的数据模型不复杂,但字段设计要经得起推敲。核心是五张表:用户表、车次表、座位表、订单表、退票记录表。下面给出关键表的建表语句,字段名和类型可以直接照搬。
-- 车次表:一条记录代表一个可售车次 CREATE TABLE train ( train_id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(20) NOT NULL, -- 车次号,如 G1024 start_station VARCHAR(50) NOT NULL, -- 始发站 end_station VARCHAR(50) NOT NULL, -- 终点站 depart_time DATETIME NOT NULL, -- 发车时间 arrive_time DATETIME NOT NULL, -- 到达时间 total_seat INT NOT NULL DEFAULT 0, -- 总座位数 left_seat INT NOT NULL DEFAULT 0, -- 余票数 price DECIMAL(10,2) NOT NULL -- 票价 ); -- 订单表:一条记录代表一次购票行为 CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, -- 订单号,业务唯一 user_id INT NOT NULL, -- 下单用户 train_id INT NOT NULL, -- 关联车次 seat_no VARCHAR(10), -- 座位号 order_status TINYINT DEFAULT 0, -- 0待支付 1已支付 2已退票 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (train_id) REFERENCES train(train_id) );这里有两个设计点值得说。第一,余票没有单独建表,而是放在车次表的left_seat字段里,因为车票预订的余票是「按车次汇总」的,拆表反而增加一致性维护成本。第二,订单状态用 TINYINT 而不是字符串,是为了后面做状态机流转时判断方便,0→1→2的流转在代码里就是一个 if 判断。座位表单独拆出来,是为了支持选座,字段包括车次、车厢、座位号和占用状态,这里不展开。
3. 总体架构与模块实现:从请求进来到订单落库
3.1 三层结构怎么落地到目录
这套系统的目录结构直接反映分层思想,建议按下面这样组织,答辩时打开工程一眼就能看出层次:
ticket-system/ ├── src/ │ ├── com.ticket.bean/ -- 实体类:User、Train、Order │ ├── com.ticket.dao/ -- 数据访问:TrainDao、OrderDao │ ├── com.ticket.servlet/ -- 控制器:LoginServlet、BookServlet │ └── com.ticket.util/ -- 工具类:DBUtil、OrderNoUtil ├── WebContent/ │ ├── jsp/ -- 页面:search.jsp、order.jsp │ ├── css/ js/ -- 静态资源 │ └── WEB-INF/web.xml -- Servlet 映射配置bean层只放属性和 getter/setter,不写业务;dao层只碰数据库,返回实体对象;servlet层负责拿参数、调 dao、转发页面。这个边界守住了,后面加功能就是加一个 Servlet 加一个 JSP,不会牵一发动全身。
3.2 车次查询模块:模糊匹配与分页
查询是用户进来的第一个动作,体验好坏全看这里。实现上,Servlet 接收出发站、到达站、日期三个参数,拼成动态 SQL 去查。下面这段是 DAO 层的核心方法,注意参数用?占位,别用字符串拼接,这是防注入的基本功。
// TrainDao.java 片段:按条件查询车次,支持分页 public List<Train> searchTrain(String start, String end, String date, int page, int size) { List<Train> list = new ArrayList<>(); StringBuilder sql = new StringBuilder( "SELECT * FROM train WHERE start_station LIKE ? AND end_station LIKE ?"); // 日期非空才追加条件,避免空参数导致全表扫描 if (date != null && !date.isEmpty()) { sql.append(" AND DATE(depart_time) = ?"); } sql.append(" LIMIT ?, ?"); // 分页:偏移量 + 每页条数 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { int idx = 1; ps.setString(idx++, "%" + start + "%"); ps.setString(idx++, "%" + end + "%"); if (date != null && !date.isEmpty()) ps.setString(idx++, date); ps.setInt(idx++, (page - 1) * size); ps.setInt(idx, size); ResultSet rs = ps.executeQuery(); while (rs.next()) list.add(mapRow(rs)); // mapRow 负责 ResultSet 转实体 } catch (SQLException e) { e.printStackTrace(); // 生产环境应记日志,课设阶段至少打印堆栈 } return list; }参数说明:start/end用 LIKE 是为了支持「北京」匹配「北京南」这种场景;page从 1 开始,偏移量算成(page-1)*size;size建议固定 10,页面上一页十条,翻页体验刚好。这里有个容易忽略的点:日期条件用DATE(depart_time)会把索引废掉,数据量小无所谓,如果答辩时被问到优化,就答「可改为范围查询depart_time BETWEEN ? AND ?」,这是加分项。
3.3 下单与余票扣减:事务是绕不过去的坎
下单是整个系统最容易出问题的地方。用户点「预订」,系统要做三件事:校验余票、扣减余票、生成订单。这三步必须在一个事务里,否则出现「票扣了订单没生成」或者「订单生成了票没扣」都是灾难。下面给出核心逻辑,用 JDBC 手动控制事务。
// BookServlet.java 片段:下单主流程 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一步:带条件扣减余票,left_seat > 0 保证不会超卖 String updateSql = "UPDATE train SET left_seat = left_seat - 1 WHERE train_id = ? AND left_seat > 0"; PreparedStatement ps1 = conn.prepareStatement(updateSql); ps1.setInt(1, trainId); int affected = ps1.executeUpdate(); if (affected == 0) { // 影响行数为 0 说明余票不足 conn.rollback(); request.setAttribute("msg", "余票不足,请选择其他车次"); request.getRequestDispatcher("/jsp/search.jsp").forward(request, response); return; } // 第二步:插入订单 String insertSql = "INSERT INTO orders(order_no,user_id,train_id,seat_no,order_status) VALUES(?,?,?,?,0)"; PreparedStatement ps2 = conn.prepareStatement(insertSql); ps2.setString(1, OrderNoUtil.generate()); // 订单号生成工具 ps2.setInt(2, userId); ps2.setInt(3, trainId); ps2.setString(4, seatNo); ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 } catch (Exception e) { if (conn != null) conn.rollback(); // 任何异常都回滚 e.printStackTrace(); } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} }这段代码的关键在WHERE train_id = ? AND left_seat > 0这个条件。它把「判断余票」和「扣减余票」合并成一条原子 SQL,数据库层面保证不会扣成负数,比先查再扣的两步写法安全得多。这是车票预订系统里最值得讲的一个设计点,答辩时主动说出来,比等老师问强。订单号生成用「时间戳 + 随机数」拼,保证唯一即可,不用上雪花算法。
3.4 退票模块:状态回滚与余票归还
退票是下单的逆操作,但比下单多一层校验:只有「已支付」的订单才能退。实现上先查订单状态,确认是 1 才继续,然后把订单状态改成 2,同时把车次的left_seat加回去。这两步同样要放在一个事务里。退票记录单独写一张表,字段包括退票时间、订单号、退款金额,方便后面做统计。这里有个细节:退票时余票加回去要判断上限,不能超过total_seat,虽然正常流程不会超,但防御性代码该写还得写。
4. 避坑与常见问题排查:这几处翻车最多
4.1 中文乱码:现象是页面显示问号,根因在编码链
现象:查询「北京」返回空结果,或者页面上的中文变成???。原因通常是三处编码不一致:JSP 页面pageEncoding、Servlet 的request.setCharacterEncoding、数据库连接串的characterEncoding。解决方法是三处统一成 UTF-8,并且在 Servlet 取参数之前就设置编码。我一般会在web.xml里配一个编码过滤器,所有请求先过一遍,比每个 Servlet 里写一行靠谱。
4.2 余票超卖:现象是余票变负数,根因是先查后扣
现象:两个人同时下单,最后余票显示 -1。原因就是前面说的「先 SELECT 查余票,再 UPDATE 扣减」的两步写法,中间有并发窗口。解决办法是把判断条件塞进 UPDATE 的 WHERE 里,用影响行数判断成败,也就是 3.3 节那段代码的写法。这个坑几乎每届都有人踩,记住一句话:并发场景下,判断和修改要放在同一条 SQL 里。
4.3 订单重复提交:现象是同一用户生成两笔订单,根因是刷新重放
现象:用户点完「提交订单」后按 F5 刷新,结果生成两笔一样的订单。原因是表单提交后直接 forward 到结果页,浏览器刷新会重发 POST。解决办法是提交成功后用response.sendRedirect重定向到订单列表页,这是 PRG 模式(Post-Redirect-Get)。另外订单表对order_no加了唯一约束,就算真重放了,数据库也会拦下第二笔。
4.4 数据库连接泄漏:现象是跑一会儿就报连接超时,根因是没关资源
现象:系统用一段时间后所有查询都卡住,日志里一堆连接超时。原因是Connection、PreparedStatement、ResultSet用完没关。解决办法是用 try-with-resources 语法,或者 finally 里按「后开先关」的顺序逐个关闭。课设阶段连接池可以不上,但关闭资源的习惯必须养成,这是代码评审的硬指标。
4.5 日期格式转换异常:现象是提交日期报 500,根因是格式不匹配
现象:查询页面选完日期提交,后台抛IllegalArgumentException。原因是前端传的是yyyy-MM-dd字符串,后台用SimpleDateFormat解析时格式串写成了yyyy/MM/dd。解决办法是前后端约定统一格式,解析时捕获ParseException并给出友好提示,别让异常直接冒到页面上。
5. 验收与答辩:怎么把一套系统讲成一份合格方案
验收这件事,很多人以为是把功能点一遍就完了,其实老师或评审看的是「你有没有想清楚」。我一般会按三步走。第一步是功能验收,对照第 2 章那张角色表,每个角色的核心动作逐条走一遍,重点演示下单和退票这条主链,因为事务和状态流转都在这条线上。第二步是异常验收,故意制造余票不足、重复提交、非法日期这几种情况,看系统是报错还是给提示,这一步最能体现工程素养。第三步是数据验收,打开数据库看订单表和车次表的余票数对不对得上,退票后余票有没有加回去。
答辩陈述建议控制在八分钟,结构按「问题—方案—实现—验证」来。开场别念需求背景,直接放一张下单流程的截图,说「这是用户从查票到出票的完整链路,下面讲它怎么实现的」。技术点只讲两个:一个是 3.3 节的原子扣减,一个是 3.1 节的分层目录,讲透比讲多强。老师常问的问题无非三类:并发怎么处理、事务边界在哪、为什么不用框架,前两个在第 3 章有答案,第三个按 2.2 节的思路答。
| 验收项 | 操作 | 通过标准 |
|---|---|---|
| 主业务链 | 查票→下单→支付→退票 | 每步状态正确,余票数一致 |
| 并发扣减 | 两窗口同时下单最后一张票 | 只有一单成功,余票不为负 |
| 重复提交 | 下单后刷新页面 | 不产生重复订单 |
| 权限隔离 | 乘客访问管理员页面 | 被拦截或跳转登录 |
最后说个习惯。我每次带项目,验收前都会自己当一遍「最不守规矩的用户」,乱填日期、连点提交、退完票再退一次,把能想到的骚操作都试一遍。从那以后我每次交付前都强制走一遍这个「捣乱清单」,比对着功能列表点一遍管用得多。这套车票预订系统的完整工程、建表脚本和页面都在资源包里,拿去改改就能用,希望帮到你。
本文还有配套的精品资源,点击获取