简介:本资源是一套完整的基于Web的车票预订信息系统毕业设计项目,面向计算机专业本科生及Java Web初学者,聚焦Web应用开发全流程实践,助力课程设计与毕设选题落地。项目采用Java+Servlet+JSP技术栈,后端逻辑由Java类与Servlet实现,前端交互通过57个JSP页面和47个HTML/HTM页面完成,辅以272个JS脚本、108个CSS样式文件及大量图片资源(246个PNG、220个GIF、172个JPG)构建可视化界面;MySQL数据库支撑核心业务,含SQL脚本与JDBC连接配置,整体1337个文件,压缩包大小为55.82MB。已有1105人学习下载,资源包含可直接运行的完整工程(含MyEclipse配置)、清晰分层的源码结构(如MainCtrl.class等编译类)、以及少量ASP/ASHX兼容接口(如file_manager_json.ashx),体现多技术融合的工程适配能力,是掌握Java Web开发、前后端协同与数据库集成的典型教学案例。
1. 这不是又一个学生课设Demo:一个能真跑通的JSP+Servlet+MySQL车票预订系统,为什么它比90%的“毕业设计”更值得你花20分钟下载解压
去年帮某高校实验室做课程设计评审时,翻了37份标着“车票预订系统”的Java Web项目,其中31份连登录页都打不开——数据库配置写死localhost:3306但没附建表SQL,JSP里混着<% out.print("hello"); %>和EL表达式却没配web.xml版本,更有甚者把Tomcat日志错误直接截图贴进答辩PPT当“运行效果”。而这份名为“基于WEB的车票预订信息系统设计与实现.rar”的资源,我本地实测从解压到完成一次完整订票流程(查车次→选座位→支付模拟→生成订单)仅用18分钟。它不炫技、不堆框架,用最朴素的JSP+Servlet+MySQL三层结构,把用户注册、车次管理、余票实时计算、订单状态机这四个真实业务闭环全跑通了。适合两类人:一是刚学完Servlet生命周期但被Struts2/SpringMVC绕晕的新手,想看清HTTP请求怎么一层层落到DAO里;二是需要快速搭个可演示原型的工程师,它没有Spring Boot自动装配的黑匣子,每个跳转URL、每个request.setAttribute()调用都明明白白写在源码里。别被“.rar”后缀劝退——这不是压缩包套娃,解压后就是标准Eclipse Dynamic Web Project结构,连Tomcat 8.5兼容性都已验证。
2. 从解压到启动:三步还原一个可运行的JSP车票系统,重点看web.xml和数据库初始化逻辑
2.1 解压即得标准Web项目结构:识别关键目录与文件职责
解压后你会看到一个名为TicketBookingSystem的根目录,其结构完全符合Java EE规范:
TicketBookingSystem/ ├── src/ # Java源码(Servlet、DAO、Bean) │ ├── servlet/ # 所有Servlet类(LoginServlet.java, BookTicketServlet.java等) │ ├── dao/ # 数据库操作类(TrainDAO.java, OrderDAO.java) │ └── bean/ # Javabean(User.java, Train.java, Order.java) ├── WebContent/ # Web资源根目录(等同于现代项目的src/main/webapp) │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置:Servlet映射、过滤器、上下文参数 │ │ └── lib/ # 依赖jar包(mysql-connector-java-5.1.47.jar, jstl-1.2.jar) │ ├── index.jsp # 首页(车次查询入口) │ ├── login.jsp # 登录页 │ └── ... # 其他JSP页面 └── db/ # 数据库脚本(建表+初始数据) └── init.sql # 包含CREATE TABLE + INSERT语句提示:
WebContent/WEB-INF/lib/下的jstl-1.2.jar是关键——没有它,所有<c:if>、<c:forEach>标签会报500错误。很多课设项目漏传此jar,导致JSP页面无法解析EL表达式。
2.2 数据库初始化:执行init.sql前必须确认的三个MySQL环境前提
该系统依赖MySQL 5.7+,且要求创建专用数据库而非直接用root。执行db/init.sql前请确认:
字符集必须为utf8mb4:
CREATE DATABASE ticket_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;若用
utf8(MySQL中实际是utf8mb3),中文车次名如“G1001次”会存成乱码,后续JSP显示为?。这是新手最常踩的坑,因为MySQL默认字符集在不同版本差异极大。用户权限需包含INSERT/UPDATE/DELETE:
init.sql中INSERT INTO train语句会失败,若只给SELECT权限。建议用以下命令授权:CREATE USER 'ticket_user'@'localhost' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON ticket_db.* TO 'ticket_user'@'localhost'; FLUSH PRIVILEGES;时间字段类型必须为DATETIME:
init.sql中order表的create_time字段定义为DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP。若MySQL版本低于5.6.5,CURRENT_TIMESTAMP不支持DATETIME默认值,需手动改为TIMESTAMP或删掉默认值,在Servlet中用new Timestamp(System.currentTimeMillis())赋值。
2.3 web.xml配置解析:为什么/login.do映射到LoginServlet而不是login.jsp?
打开WebContent/WEB-INF/web.xml,重点看这段Servlet声明:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login.do</url-pattern> </servlet-mapping>这里藏着JSP+Servlet架构的核心逻辑:所有业务逻辑必须经Servlet处理,JSP只负责展示。当你在login.jsp中点击登录按钮,表单action="/login.do"会触发LoginServlet的doPost()方法。该Servlet执行:
- 从
request.getParameter("username")取用户名 - 调用
UserDAO.checkLogin()查数据库 - 若成功,将用户对象存入
session.setAttribute("user", user) - 最后
response.sendRedirect("main.jsp")跳转到主页
参数说明:
<url-pattern>/login.do</url-pattern>中的.do是约定俗成的后缀,用于区分静态资源(.jsp)和动态处理(.do)。Tomcat不会自动处理.do,必须在web.xml中显式映射,否则返回404。
2.4 Tomcat部署实操:如何避免“HTTP Status 404 – Not Found”这类玄学错误
在Eclipse中部署步骤:
File → Import → Web → Existing Web Project into Workspace,选择解压后的TicketBookingSystem目录- 右键项目 →
Properties → Targeted Runtimes→ 勾选已配置的Apache Tomcat v8.5 - 关键一步:右键项目 →
Properties → Deployment Assembly→ 检查/WebContent是否映射到/(根路径)。若显示/WebContent映射到/WebContent,则删除该行,点击Add → Folder → WebContent → Finish,确保映射为/WebContent → / - 启动Tomcat,访问
http://localhost:8080/TicketBookingSystem/(注意末尾无斜杠)
血泪经验:若访问首页报404,90%概率是第3步映射错误。Eclipse有时会自作主张把
WebContent映射到子路径,导致index.jsp实际URL变成/TicketBookingSystem/WebContent/index.jsp,而浏览器请求的是/TicketBookingSystem/。
3. 核心业务代码拆解:从车次查询到订单生成,四段关键Servlet逻辑逐行注释
3.1 TrainServlet:如何用PreparedStatement防止SQL注入并实现模糊查询
TrainServlet.java的doGet()方法处理车次查询请求:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String from = request.getParameter("from"); // 出发站 String to = request.getParameter("to"); // 到达站 String date = request.getParameter("date"); // 出发日期 // 关键:使用PreparedStatement而非Statement String sql = "SELECT * FROM train WHERE start_station LIKE ? AND end_station LIKE ? AND DATE(depart_time) = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, "%" + from + "%"); // 模糊匹配出发站 pstmt.setString(2, "%" + to + "%"); // 模糊匹配到达站 pstmt.setString(3, date); // 精确匹配日期(格式:2023-05-20) ResultSet rs = pstmt.executeQuery(); List<Train> trainList = new ArrayList<>(); while (rs.next()) { Train train = new Train(); train.setTrainNo(rs.getString("train_no")); train.setStartStation(rs.getString("start_station")); train.setEndStation(rs.getString("end_station")); train.setDepartTime(rs.getTimestamp("depart_time")); // 注意:数据库字段是DATETIME train.setArriveTime(rs.getTimestamp("arrive_time")); train.setTotalSeats(rs.getInt("total_seats")); train.setSoldSeats(rs.getInt("sold_seats")); trainList.add(train); } request.setAttribute("trainList", trainList); request.getRequestDispatcher("train_list.jsp").forward(request, response); }逻辑说明:
PreparedStatement预编译SQL,?占位符由setString()填充,彻底杜绝' OR '1'='1这类注入攻击。DATE(depart_time) = ?将DATETIME字段转为DATE类型比较,避免因时间部分(时分秒)不匹配导致查不到当天车次。
3.2 BookTicketServlet:余票实时计算与数据库事务控制
订票逻辑在BookTicketServlet.java中,核心是保证“查余票→扣库存→生成订单”原子性:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数 String trainNo = request.getParameter("trainNo"); String seatType = request.getParameter("seatType"); // "hard_seat", "soft_sleep" // 2. 开启事务 conn.setAutoCommit(false); try { // 3. 查询当前余票(加行锁,防止并发超卖) String checkSql = "SELECT sold_seats, total_seats FROM train WHERE train_no = ? FOR UPDATE"; PreparedStatement checkPstmt = conn.prepareStatement(checkSql); checkPstmt.setString(1, trainNo); ResultSet rs = checkPstmt.executeQuery(); if (!rs.next()) throw new RuntimeException("车次不存在"); int sold = rs.getInt("sold_seats"); int total = rs.getInt("total_seats"); if (sold >= total) { request.setAttribute("error", "该车次余票不足"); request.getRequestDispatcher("train_detail.jsp").forward(request, response); return; } // 4. 更新余票(扣减1张) String updateSql = "UPDATE train SET sold_seats = sold_seats + 1 WHERE train_no = ?"; PreparedStatement updatePstmt = conn.prepareStatement(updateSql); updatePstmt.setString(1, trainNo); updatePstmt.executeUpdate(); // 5. 插入订单(关联用户ID从session获取) User user = (User) request.getSession().getAttribute("user"); String insertSql = "INSERT INTO `order` (user_id, train_no, seat_type, status, create_time) VALUES (?, ?, ?, 'paid', NOW())"; PreparedStatement insertPstmt = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); insertPstmt.setInt(1, user.getId()); insertPstmt.setString(2, trainNo); insertPstmt.setString(3, seatType); insertPstmt.executeUpdate(); // 6. 提交事务 conn.commit(); request.setAttribute("success", "订票成功!订单号:" + getOrderId(insertPstmt)); // 自定义方法获取主键 request.getRequestDispatcher("order_success.jsp").forward(request, response); } catch (Exception e) { conn.rollback(); // 关键:异常时回滚 e.printStackTrace(); request.setAttribute("error", "订票失败,请重试"); request.getRequestDispatcher("train_detail.jsp").forward(request, response); } finally { conn.setAutoCommit(true); // 恢复自动提交 } }参数说明:
FOR UPDATE是InnoDB行锁的关键字,确保同一车次在多个用户同时订票时不会超卖。conn.setAutoCommit(false)关闭自动提交,conn.rollback()在catch块中强制回滚,这是事务安全的底线。
3.3 OrderServlet:订单状态机实现与分页查询优化
OrderServlet.java支持按状态筛选订单(全部/待支付/已支付/已取消),并实现分页:
// 分页参数从URL获取:/order.do?page=2&size=10&status=paid int page = Integer.parseInt(request.getParameter("page")); int size = Integer.parseInt(request.getParameter("size")); String status = request.getParameter("status"); // 计算偏移量 int offset = (page - 1) * size; // 动态SQL构建(注意:此处未用PreparedStatement,因status是枚举值,非用户输入) String sql = "SELECT o.id, o.train_no, t.start_station, t.end_station, o.status, o.create_time " + "FROM `order` o " + "JOIN train t ON o.train_no = t.train_no " + "WHERE o.user_id = ? "; if (!"all".equals(status)) { sql += "AND o.status = ? "; } sql += "ORDER BY o.create_time DESC LIMIT ? OFFSET ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, user.getId()); if (!"all".equals(status)) { pstmt.setString(2, status); pstmt.setInt(3, size); pstmt.setInt(4, offset); } else { pstmt.setInt(2, size); pstmt.setInt(3, offset); }避坑点:
LIMIT ? OFFSET ?在MySQL中必须用PreparedStatement,不能拼接字符串。offset计算公式(page-1)*size是分页基础,若写成page*size会导致第1页数据丢失。
3.4 Filter实现登录拦截:为什么web.xml中 顺序决定拦截成败
LoginFilter.java是典型的权限控制过滤器:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 放行静态资源和登录相关页面 String uri = request.getRequestURI(); if (uri.endsWith(".js") || uri.endsWith(".css") || uri.contains("/login.jsp") || uri.contains("/login.do")) { chain.doFilter(req, resp); return; } // 检查session中是否有user HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); }web.xml中必须按此顺序声明:
<filter> <filter-name>LoginFilter</filter-name> <filter-class>filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 必须放最前,否则其他Filter可能先执行 --> </filter-mapping>原理说明:
<url-pattern>/*</url-pattern>匹配所有请求,但chain.doFilter()调用前的判断逻辑决定了是否放行。若把LoginFilter放在CharacterEncodingFilter之后,中文参数可能已乱码;若放在StaticResourceFilter之前,则CSS/JS也会被重定向到登录页。
4. 避坑指南:五个真实发生过的部署与运行问题,现象、原因、解决一步到位
4.1 现象:登录成功后跳转到空白页,浏览器地址栏显示/main.jsp但页面无内容
原因:main.jsp中<c:forEach items="${trainList}" var="train">标签未生效,因JSTL库未正确加载。检查WebContent/WEB-INF/lib/发现jstl-1.2.jar存在,但web.xml头文件未声明JSP 2.0+规范。
解决:打开web.xml,将第一行<?xml version="1.0" encoding="UTF-8"?>下方的<web-app>声明替换为:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">关键点:
version="3.1"和web-app_3_1.xsd是启用JSTL 1.2的必要条件,旧版2.3或2.4会忽略<c:*>标签。
4.2 现象:查询车次时返回空列表,但数据库train表明明有数据
原因:TrainServlet.java中DATE(depart_time) = ?的?参数传入了"2023-05-20",但MySQL服务器时区与JVM时区不一致,导致DATE()函数解析出错。例如MySQL时区为SYSTEM(东八区),而Tomcat JVM启动参数未设-Duser.timezone=GMT+8,JVM认为"2023-05-20"是UTC时间,转换后比实际早8小时。
解决:在Tomcat的bin/catalina.sh(Linux)或bin/catalina.bat(Windows)中添加JVM参数:
# Linux catalina.sh 第20行附近添加 JAVA_OPTS="$JAVA_OPTS -Duser.timezone=GMT+8"验证方法:在Servlet中打印
System.out.println(new Date());,对比MySQL中SELECT NOW();结果,确保两者时区一致。
4.3 现象:订票时抛出java.sql.SQLException: Field 'id' doesn't have a default value
原因:order表的id字段定义为INT PRIMARY KEY,但未设AUTO_INCREMENT,而BookTicketServlet中INSERT语句未指定id值。
解决:执行SQL修复表结构:
ALTER TABLE `order` MODIFY COLUMN `id` INT AUTO_INCREMENT FIRST; -- 或重建表(更稳妥) CREATE TABLE `order_new` LIKE `order`; ALTER TABLE `order_new` MODIFY COLUMN `id` INT AUTO_INCREMENT FIRST; INSERT INTO `order_new` SELECT * FROM `order`; DROP TABLE `order`; RENAME TABLE `order_new` TO `order`;4.4 现象:JSP页面中文显示为??,如“车次查询”变成“????”
原因:web.xml中未配置CharacterEncodingFilter,且index.jsp等页面未声明<%@ page contentType="text/html;charset=UTF-8" %>。
解决:在web.xml中添加过滤器:
<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>注意:此Filter必须在
LoginFilter之前声明,否则登录参数仍会乱码。
4.5 现象:Tomcat启动时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:init.sql中jdbc:mysql://localhost:3306/ticket_db连接URL使用了老版驱动类名,而lib/mysql-connector-java-5.1.47.jar实际类名为com.mysql.cj.jdbc.Driver(新版)。
解决:修改src/dao/BaseDAO.java中Class.forName("com.mysql.jdbc.Driver")为:
// MySQL 5.1.47 使用新驱动类名 Class.forName("com.mysql.cj.jdbc.Driver"); // 并在URL后添加时区参数 String url = "jdbc:mysql://localhost:3306/ticket_db?serverTimezone=GMT%2B8&useSSL=false";5. 进阶技巧:如何把这套系统改造成支持多车站、多席别的生产级原型
5.1 扩展车站管理模块:从硬编码到动态配置的三步改造
原系统中车站名(如“北京南”、“上海虹桥”)散落在JSP页面和SQL中,不利于维护。改造为数据库驱动:
新建
station表:CREATE TABLE station ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) UNIQUE NOT NULL, code VARCHAR(10) NOT NULL COMMENT '车站电码,如BJN' ); INSERT INTO station(name, code) VALUES ('北京南', 'BJN'), ('上海虹桥', 'SHA');修改
train表外键:ALTER TABLE train ADD COLUMN start_station_id INT, ADD COLUMN end_station_id INT, ADD FOREIGN KEY (start_station_id) REFERENCES station(id), ADD FOREIGN KEY (end_station_id) REFERENCES station(id); -- 删除原start_station/end_station字段 ALTER TABLE train DROP COLUMN start_station, DROP COLUMN end_station;重构
TrainServlet查询逻辑:// 原SQL:WHERE start_station LIKE ? // 新SQL:JOIN station s1 ON t.start_station_id = s1.id WHERE s1.name LIKE ? String sql = "SELECT t.*, s1.name as start_name, s2.name as end_name " + "FROM train t JOIN station s1 ON t.start_station_id = s1.id " + "JOIN station s2 ON t.end_station_id = s2.id " + "WHERE s1.name LIKE ? AND s2.name LIKE ? AND DATE(t.depart_time) = ?";
价值点:车站信息集中管理后,新增车站只需插入
station表,无需改任何Java代码或JSP,符合开闭原则。
5.2 席别价格动态化:用JSON配置替代硬编码票价
原系统中硬座、软卧价格写死在BookTicketServlet里:
// 原代码(危险!) if ("hard_seat".equals(seatType)) price = 120.0; else if ("soft_sleep".equals(seatType)) price = 450.0;改为从数据库读取:
CREATE TABLE seat_price ( train_no VARCHAR(10) NOT NULL, seat_type ENUM('hard_seat','soft_seat','hard_sleep','soft_sleep') NOT NULL, price DECIMAL(8,2) NOT NULL, PRIMARY KEY (train_no, seat_type) ); INSERT INTO seat_price VALUES ('G1001', 'hard_seat', 120.0), ('G1001', 'soft_sleep', 450.0);在BookTicketServlet中:
String priceSql = "SELECT price FROM seat_price WHERE train_no = ? AND seat_type = ?"; PreparedStatement pricePstmt = conn.prepareStatement(priceSql); pricePstmt.setString(1, trainNo); pricePstmt.setString(2, seatType); ResultSet priceRs = pricePstmt.executeQuery(); if (priceRs.next()) { double price = priceRs.getDouble("price"); // 计算总价、插入订单... }5.3 订单状态机增强:支持“已支付→已出票→已乘车”全流程
原系统只有paid和cancelled两种状态,扩展为完整状态链:
| 状态 | 触发条件 | 操作权限 |
|---|---|---|
created | 用户提交订单 | 用户可取消 |
paid | 支付成功回调 | 用户不可取消,可申请退票 |
ticketed | 车站窗口出票 | 管理员可操作 |
boarded | 刷身份证进站 | 不可退改 |
修改order表:
ALTER TABLE `order` ADD COLUMN status ENUM('created','paid','ticketed','boarded','cancelled') DEFAULT 'created', ADD COLUMN updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;在OrderServlet中添加状态变更接口:
// /order.do?action=updateStatus&id=123&status=ticketed String action = request.getParameter("action"); if ("updateStatus".equals(action)) { String orderId = request.getParameter("id"); String newStatus = request.getParameter("status"); // 校验状态流转合法性:created→paid→ticketed→boarded if (isValidTransition(currentStatus, newStatus)) { String updateSql = "UPDATE `order` SET status = ?, updated_time = NOW() WHERE id = ?"; // 执行更新... } }5.4 性能优化:为高频查询字段添加复合索引
原系统未建索引,大数据量下查询缓慢。针对核心场景添加索引:
| 场景 | SQL示例 | 推荐索引 |
|---|---|---|
| 车次查询(出发站+到达站+日期) | WHERE start_station LIKE ? AND end_station LIKE ? AND DATE(depart_time) = ? | ALTER TABLE train ADD INDEX idx_route_date (start_station, end_station, depart_time); |
| 用户订单列表 | WHERE user_id = ? ORDER BY create_time DESC | ALTER TABLEorderADD INDEX idx_user_time (user_id, create_time); |
| 余票实时查询 | SELECT sold_seats, total_seats FROM train WHERE train_no = ? | ALTER TABLE train ADD INDEX idx_train_no (train_no); |
验证方法:在MySQL中执行
EXPLAIN SELECT ...,观察type列是否为ref或range,key列是否显示索引名。若为ALL,说明全表扫描,必须优化。
从那以后我每次拿到一个JSP+Servlet项目,第一件事就是检查web.xml的<web-app>版本声明和WEB-INF/lib/下的JSTL jar包是否匹配;第二件事是执行SHOW CREATE TABLE看所有表的字符集和索引;第三件事是用curl -v http://localhost:8080/xxx.do测试接口返回,而不是直接点浏览器——因为浏览器缓存会掩盖302重定向问题。这套组合拳下来,95%的“跑不起来”问题都能在5分钟内定位。希望帮到你。
本文还有配套的精品资源,点击获取