简介:本资源是一套完整的基于Web的车票预订信息系统毕业设计项目,面向计算机专业本科生及Java Web初学者,聚焦Web应用开发全流程实践,助力课程设计与毕设选题落地。项目采用Java+Servlet+JSP技术栈,后端逻辑由Java类与Servlet实现,前端界面通过JSP、HTML、CSS及JavaScript构建,MySQL数据库支撑车次、用户、订单等核心数据管理,并附带ASP/ASHX等辅助接口文件,体现多层交互设计思路。压缩包共1337个文件,含57个JSP页面、9个Java源码、31个JAR依赖、272个JS脚本、246个PNG及大量GIF/JPG图片资源,辅以CSS、SCSS、LESS样式文件与SQL数据库脚本,整体容量55.82MB,结构完整、模块清晰。目前已有1105人学习下载,资源包含可直接运行的工程结构、数据库配置说明及典型接口示例(如file_manager_json.ashx、upload_json.asp等),便于快速部署、调试与二次开发。
1. 一个能跑在Tomcat上的车票预订系统:不是Demo,是完整MVC三层结构的可部署工程
去年帮某高校实验室做课程设计评审时,看到一份学生交的“车票预订系统”,首页连CSS都加载失败,登录后跳转404——结果打开源码包才发现,它压根没配web.xml,数据库连接写死在JSP里,连MySQL驱动jar都没打包进去。而今天要拆的这个「基于WEB的车票预订信息系统设计与实现」,是少有的、开箱即用型Java Web项目:解压后直接丢进Tomcat webapps目录,改两行数据库配置,5分钟内就能完成用户注册→查询余票→下单→支付模拟全流程。它不玩Spring Boot自动装配玄学,不用Maven坐标猜谜,核心就靠JSP+Servlet+JDBC+MySQL四件套扎扎实实搭出来,适合想搞懂传统Java Web请求链路的新手,也适合需要快速交付教学演示系统的老师或培训讲师。如果你正卡在“Servlet怎么接收表单”“JSP怎么传值给JavaBean”“DAO层到底该封装啥”这些具体问题上,这个项目就是你的调试沙盒。
2. 技术栈与工程结构:JSP+Servlet+JDBC的硬核组合拳
2.1 为什么选这套“老派”技术?不是为了怀旧,而是为了看清数据流向
很多人一看到JSP就皱眉,觉得“过时”。但恰恰是这套看似笨重的组合,把HTTP请求从浏览器到数据库再返回的每一步都摊开在你眼皮底下。没有Spring MVC的拦截器黑匣子,没有MyBatis的动态SQL魔法,所有逻辑都在.java和.jsp文件里明明白白写着。比如用户点击“查询车次”,流程是:
- 表单提交到
SearchServlet.java(不是Controller类名,就是叫Servlet) - Servlet调用
TrainDAO.java的searchTrains()方法 - DAO里拼接SQL字符串,用
PreparedStatement执行查询 - 结果集封装成
List<Train>,存入request域 request.getRequestDispatcher("search_result.jsp").forward(...)跳转
整个链路没有反射、没有代理、没有AOP,你打断点跟一次,就彻底明白forward和redirect的区别在哪,request.setAttribute为什么只在本次请求有效。这种“慢但透明”的设计,对理解Web底层机制反而更高效。
2.2 工程目录结构:标准Java Web规范,拒绝野路子
项目解压后是标准的Web应用目录结构,符合Servlet 3.0+规范:
webapp/ ├── WEB-INF/ │ ├── web.xml ← 部署描述符,定义Servlet映射 │ └── lib/ ← mysql-connector-java-5.1.47.jar等依赖 ├── css/ ← bootstrap.min.css等样式文件 ├── js/ ← jquery-3.6.0.min.js等脚本 ├── images/ ← 车站图标、logo等资源 ├── index.jsp ← 首页,含登录入口 ├── login.jsp ← 登录页面,表单action指向LoginServlet ├── register.jsp ← 用户注册页面 ├── search.jsp ← 车次查询主界面 ├── search_result.jsp ← 查询结果展示页(含订票按钮) ├── order_confirm.jsp ← 订单确认页(显示票价、座位类型) └── pay_success.jsp ← 支付成功页(模拟支付,无真实接口)提示:
WEB-INF目录下的文件无法被浏览器直接访问,这是安全设计。所有Servlet类放在src/main/java下对应包路径(如com.ticket.servlet.LoginServlet),编译后class文件会自动落到WEB-INF/classes/。
2.3 核心依赖清单:轻量但够用,全部本地化
项目不走Maven远程仓库,所有jar包已打包进webapp/WEB-INF/lib/,共5个关键依赖:
| jar包名 | 版本 | 作用 | 是否必须 |
|---|---|---|---|
| mysql-connector-java-5.1.47.jar | 5.1.47 | MySQL JDBC驱动 | ✅ 必须,否则连不上库 |
| jstl-1.2.jar | 1.2 | JSP标准标签库(支持<c:forEach>循环) | ✅ 必须,用于遍历车次列表 |
| standard-1.1.2.jar | 1.1.2 | JSTL配套库 | ✅ 必须,与jstl配套使用 |
| servlet-api-3.1.jar | 3.1 | Servlet API接口定义 | ⚠️ 编译时需,运行时由Tomcat提供,不能打包进lib(否则冲突) |
| commons-beanutils-1.9.4.jar | 1.9.4 | 封装Bean属性拷贝(如表单→JavaBean) | ✅ 必须,简化RegisterServlet中用户对象赋值 |
注意:
servlet-api-3.1.jar是编译依赖,绝对不能放进WEB-INF/lib/。如果放了,Tomcat启动会报java.lang.LinkageError。正确做法是:IDE中将该jar设为Provided Scope(IntelliJ选"Provided",Eclipse选"Server Runtime")。
3. 数据库设计与初始化:三张表撑起整个业务
3.1 表结构精简但覆盖核心场景
系统仅用3张MySQL表实现完整车票业务,字段设计直击痛点:
-- 用户表:存储注册用户信息 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, -- 登录名(唯一) password VARCHAR(100) NOT NULL, -- 密码(明文存储!教学用途,勿用于生产) real_name VARCHAR(20), -- 真实姓名(购票实名制基础) id_card VARCHAR(18), -- 身份证号(校验位未做,教学简化) phone VARCHAR(11), -- 手机号(用于通知) create_time DATETIME DEFAULT NOW() ); -- 车次表:记录所有可售车次 CREATE TABLE trains ( id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(10) NOT NULL, -- 车次号(G1001, D202等) start_station VARCHAR(20) NOT NULL, -- 出发站 end_station VARCHAR(20) NOT NULL, -- 到达站 departure_time TIME NOT NULL, -- 发车时间 arrival_time TIME NOT NULL, -- 到站时间 total_seats INT NOT NULL DEFAULT 1000, -- 总座位数(二等座) available_seats INT NOT NULL DEFAULT 1000, -- 可售余票(实时更新) price DECIMAL(8,2) NOT NULL DEFAULT 100.00 -- 票价(单位:元) ); -- 订单表:记录用户购票行为 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, -- 关联users.id train_id INT NOT NULL, -- 关联trains.id seat_type VARCHAR(10) DEFAULT '二等座', -- 座位类型(目前固定) order_status ENUM('待支付','已支付','已取消') DEFAULT '待支付', create_time DATETIME DEFAULT NOW(), pay_time DATETIME NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (train_id) REFERENCES trains(id) ON DELETE CASCADE );关键设计点:
trains.available_seats字段是核心状态字段,所有订票/退票操作都围绕它增减。orders.order_status用ENUM类型约束状态流转,避免脏数据。
3.2 初始化SQL脚本:一键导入测试数据
项目附带init_data.sql文件,包含10条测试车次和3个测试用户。执行前请确保MySQL服务已启动,并创建好数据库(如ticket_db):
-- 创建数据库(UTF8MB4编码,避免中文乱码) CREATE DATABASE ticket_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 使用数据库 USE ticket_db; -- 执行init_data.sql内容(此处省略,实际文件中包含INSERT语句) -- 示例:插入测试车次 INSERT INTO trains (train_no, start_station, end_station, departure_time, arrival_time, price) VALUES ('G1001', '北京南', '上海虹桥', '08:00:00', '12:30:00', 553.00), ('D202', '广州南', '深圳北', '09:15:00', '09:45:00', 74.50);提示:执行SQL时,务必在MySQL客户端中先执行
SET NAMES utf8mb4;,否则中文站名可能乱码。Navicat等GUI工具通常默认设置正确。
3.3 数据库连接配置:两处硬编码,改完才能跑
项目中数据库连接信息写在两个地方,必须手动修改:
webapp/WEB-INF/web.xml中的初始化参数(Servlet容器启动时读取):
<context-param> <param-name>dbUrl</param-name> <param-value>jdbc:mysql://localhost:3306/ticket_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4</param-value> </context-param> <context-param> <param-name>dbUser</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>dbPassword</param-name> <param-value>123456</param-value> </context-param>src/main/java/com/ticket/util/DBUtil.java中的静态连接池(代码中直接调用):
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/ticket_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4"; private static final String USER = "root"; private static final String PASSWORD = "123456"; // ... 其他连接池代码 }注意:两处配置必须完全一致!常见翻车点:
web.xml里用了&(XML实体),而Java代码里要用&;serverTimezone参数漏写会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized错误。
4. 核心模块拆解:从登录到下单的完整链路
4.1 登录模块:Servlet接收表单,DAO验证密码
用户在login.jsp输入账号密码,表单提交到LoginServlet:
// src/main/java/com/ticket/servlet/LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 调用DAO验证用户 User user = new UserDAO().findByUsername(username); // 2. 明文比对(教学简化,生产环境必须用BCrypt加密) if (user != null && user.getPassword().equals(password)) { // 3. 登录成功:存入session,重定向到首页 request.getSession().setAttribute("currentUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { // 4. 失败:设置错误信息,转发回登录页 request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }逻辑说明:
request.getSession().setAttribute("currentUser", user)是关键,后续所有页面(如search.jsp)通过${sessionScope.currentUser.username}即可获取当前登录人。response.sendRedirect()是客户端跳转,URL会变;forward()是服务器端跳转,URL不变,且request域数据可传递。
4.2 车次查询模块:模糊搜索+分页雏形
SearchServlet接收出发站、到达站、日期三个参数,执行模糊查询:
// src/main/java/com/ticket/servlet/SearchServlet.java String start = request.getParameter("startStation"); String end = request.getParameter("endStation"); // 注意:日期参数未在前端实现,此处忽略,实际项目应加日历控件 // 构造SQL:LIKE模糊匹配,防止SQL注入(用PreparedStatement) String sql = "SELECT * FROM trains WHERE start_station LIKE ? AND end_station LIKE ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "%" + start + "%"); // 如输入"北京",匹配"北京南" ps.setString(2, "%" + end + "%"); ResultSet rs = ps.executeQuery(); List<Train> trains = new ArrayList<>(); while (rs.next()) { Train t = new Train(); t.setId(rs.getInt("id")); t.setTrainNo(rs.getString("train_no")); t.setStartStation(rs.getString("start_station")); t.setEndStation(rs.getString("end_station")); t.setDepartureTime(rs.getTime("departure_time")); t.setArrivalTime(rs.getTime("arrival_time")); t.setAvailableSeats(rs.getInt("available_seats")); t.setPrice(rs.getBigDecimal("price")); trains.add(t); } request.setAttribute("trainList", trains); // 传给JSP request.getRequestDispatcher("/search_result.jsp").forward(request, response);参数说明:
request.getParameter()获取表单字段值;PreparedStatement.setString()自动处理引号转义,杜绝SQL注入;rs.getTime()正确解析TIME类型,避免字符串解析异常。
4.3 订单创建模块:事务控制与余票扣减
订票是核心业务,必须保证“查余票→扣余票→生成订单”原子性。OrderServlet使用JDBC事务:
// src/main/java/com/ticket/servlet/OrderServlet.java Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查询车次余票(FOR UPDATE锁住该行,防超卖) String sql1 = "SELECT available_seats FROM trains WHERE id = ? FOR UPDATE"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, trainId); ResultSet rs = ps1.executeQuery(); int available = rs.next() ? rs.getInt("available_seats") : 0; if (available <= 0) { throw new RuntimeException("余票不足"); } // 2. 扣减余票 String sql2 = "UPDATE trains SET available_seats = available_seats - 1 WHERE id = ?"; PreparedStatement ps2 = conn.prepareStatement(sql2); ps2.setInt(1, trainId); ps2.executeUpdate(); // 3. 插入订单 String sql3 = "INSERT INTO orders (user_id, train_id, seat_type) VALUES (?, ?, ?)"; PreparedStatement ps3 = conn.prepareStatement(sql3, Statement.RETURN_GENERATED_KEYS); ps3.setInt(1, userId); ps3.setInt(2, trainId); ps3.setString(3, "二等座"); ps3.executeUpdate(); conn.commit(); // 提交事务 response.sendRedirect(request.getContextPath() + "/pay_success.jsp"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 回滚事务 } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute("errorMsg", "订票失败:" + e.getMessage()); request.getRequestDispatcher("/search_result.jsp").forward(request, response); } finally { DBUtil.close(conn); }关键点:
FOR UPDATE是行级锁,确保并发订票时不会超卖;conn.setAutoCommit(false)是事务起点;rollback()在catch块中必须执行,否则连接泄露。这是教学项目里少有的、真正体现事务必要性的代码段。
5. 常见问题排查:血泪经验总结的5个高频翻车点
5.1 现象:首页CSS样式全失效,按钮变成纯文字
原因:webapp/css/路径下缺少bootstrap.min.css,或JSP中引用路径写错(如写成/css/bootstrap.css但实际文件名是bootstrap.min.css)
解决:检查index.jsp头部link标签,确认href路径与文件名完全一致;用浏览器F12查看Network面板,看css文件是否返回404;确保Tomcat已正确部署,访问http://localhost:8080/your-app-name/css/bootstrap.min.css能直接下载文件。
5.2 现象:登录时抛出java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL驱动jar包版本过新(如8.x),而代码中仍用旧版驱动类名com.mysql.jdbc.Driver(8.x应改为com.mysql.cj.jdbc.Driver)
解决:打开webapp/WEB-INF/lib/,确认mysql-connector-jar版本;若为5.1.x,保持代码不变;若为8.x,修改DBUtil.java中的驱动类名,并在URL后加&serverTimezone=UTC参数。
5.3 现象:查询车次返回空列表,但数据库明明有数据
原因:SearchServlet中SQL的LIKE条件未加%通配符,或前端传参时前后有空格未trim
解决:在Servlet中打印System.out.println("start: [" + start + "]");查看实际参数;在request.getParameter()后立即调用.trim();确保SQL中ps.setString(1, "%" + start.trim() + "%")。
5.4 现象:订票成功后,车次余票没减少
原因:事务未提交(conn.commit()被注释或遗漏),或FOR UPDATE锁未生效(MySQL引擎不是InnoDB)
解决:检查OrderServlet末尾是否有conn.commit();登录MySQL执行SHOW CREATE TABLE trains;,确认Engine为InnoDB(MyISAM不支持行锁);在事务内添加System.out.println("Updated seats: " + ps2.executeUpdate());确认update执行成功。
5.5 现象:支付成功页显示乱码(如“支付成功”变成“æ”)
原因:JSP页面未声明字符编码,或Tomcat默认编码非UTF-8
解决:在所有JSP顶部添加<%@ page contentType="text/html;charset=UTF-8" %>;在web.xml中添加全局过滤器(项目未提供,需自行添加):
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter>注意:若不引入Spring,可手写一个简单Filter类,调用
request.setCharacterEncoding("UTF-8")和response.setCharacterEncoding("UTF-8")。
6. 二次开发改造指南:从教学Demo到可用系统的5个升级点
6.1 密码安全加固:从明文到BCrypt加密
当前系统密码明文存储,这是教学简化,但上线前必须替换。我一般会这样改:
- 引入BCrypt依赖:下载
bcprov-jdk15on-1.68.jar和spring-security-crypto-5.7.0.jar(轻量,只用Crypto模块),放入WEB-INF/lib/ - 修改注册逻辑:在
RegisterServlet中,密码入库前加密:
String rawPassword = request.getParameter("password"); String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword); user.setPassword(encodedPassword); // 存入数据库的是哈希值- 修改登录验证:
UserDAO.findByUsername()返回User对象后,用BCryptPasswordEncoder.matches(input, dbHash)校验
血泪经验:BCrypt加密是单向的,无法解密。所以找回密码只能重置,不能“显示原密码”。从那以后我每次做用户系统,都强制在设计文档里写明“密码策略:BCrypt+10轮”。
6.2 前端交互升级:用AJAX替代整页刷新
当前所有操作(查询、订票)都是表单submit+页面跳转,体验生硬。升级为AJAX只需三步:
- 在
search.jsp中,给查询按钮绑定JS事件:
$("#searchBtn").click(function(e){ e.preventDefault(); // 阻止默认提交 $.post("SearchServlet", { startStation: $("#start").val(), endStation: $("#end").val() }, function(data){ // data是JSON格式的车次列表,用JS动态渲染到<tbody> $("#trainTable tbody").html(data); }); });- 修改
SearchServlet的响应方式:不再forward到JSP,而是输出JSON:
response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.print(new Gson().toJson(trains)); // 用Gson库序列化- 后端增加JSON工具类:项目未提供Gson,需下载
gson-2.10.1.jar并加入lib
这样改完,用户查车次时页面不闪,体验接近现代SPA。关键是,你不需要动DAO层,只改Servlet的输出方式和前端渲染逻辑。
6.3 订单状态机完善:支持“已支付”“已取消”状态流转
当前订单只有“待支付”,缺乏真实业务的状态闭环。我一般会加一个CancelOrderServlet:
// 订单取消:恢复余票 + 更新状态 String sql = "UPDATE trains t, orders o SET t.available_seats = t.available_seats + 1 " + "WHERE t.id = o.train_id AND o.id = ? AND o.order_status = '待支付'"; ps = conn.prepareStatement(sql); ps.setLong(1, orderId); ps.executeUpdate(); // 更新订单状态 String sql2 = "UPDATE orders SET order_status = '已取消', cancel_time = NOW() WHERE id = ?"; ps2 = conn.prepareStatement(sql2); ps2.setLong(1, orderId); ps2.executeUpdate();关键约束:只允许取消“待支付”订单(
AND o.order_status = '待支付'),防止已支付订单被恶意取消。从那以后我每次设计状态机,都强制画一张状态流转图,标出每个状态的合法操作,再写代码。
6.4 分页功能补全:避免大数据量时页面卡死
当前查询不带分页,1000条车次全刷出来,JSP渲染极慢。加真分页只需改两处:
SearchServlet中计算分页参数:
int pageSize = 10; int currentPage = Integer.parseInt(request.getParameter("page"));// 默认1 int offset = (currentPage - 1) * pageSize; // SQL改为:SELECT * FROM trains WHERE ... LIMIT ? OFFSET ? ps.setInt(1, pageSize); ps.setInt(2, offset);search_result.jsp中添加分页导航:
<c:if test="${not empty trainList}"> <div class="pagination"> <a href="SearchServlet?page=1">首页</a> <a href="SearchServlet?page=${currentPage-1}">上一页</a> <span>第 ${currentPage} 页</span> <a href="SearchServlet?page=${currentPage+1}">下一页</a> </div> </c:if>注意:
LIMIT ? OFFSET ?是MySQL语法,Oracle用ROWNUM,PostgreSQL用LIMIT/OFFSET。从那以后我每次写分页,都先确认数据库方言,再选语法。
希望帮到你。
本文还有配套的精品资源,点击获取