资讯详情

JSP+Servlet+MySQL车票系统实战:从部署到事务控制全解析

📅 2026/10/29 11:20:42 | 华诺云谱 👁 阅读
JSP+Servlet+MySQL车票系统实战:从部署到事务控制全解析
简介本资源是一套完整的基于Web的车票预订信息系统毕业设计项目面向计算机专业本科生及Java Web初学者聚焦Web应用开发全流程实践助力课程设计与毕设选题落地。项目采用JavaServletJSP技术栈后端逻辑由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一个能真跑通的JSPServletMySQL车票预订系统为什么它比90%的“毕业设计”更值得你花20分钟下载解压去年帮某高校实验室做课程设计评审时翻了37份标着“车票预订系统”的Java Web项目其中31份连登录页都打不开——数据库配置写死localhost:3306但没附建表SQLJSP里混着% out.print(hello); %和EL表达式却没配web.xml版本更有甚者把Tomcat日志错误直接截图贴进答辩PPT当“运行效果”。而这份名为“基于WEB的车票预订信息系统设计与实现.rar”的资源我本地实测从解压到完成一次完整订票流程查车次→选座位→支付模拟→生成订单仅用18分钟。它不炫技、不堆框架用最朴素的JSPServletMySQL三层结构把用户注册、车次管理、余票实时计算、订单状态机这四个真实业务闭环全跑通了。适合两类人一是刚学完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/ # JavabeanUser.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前请确认字符集必须为utf8mb4CREATE DATABASE ticket_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;若用utf8MySQL中实际是utf8mb3中文车次名如“G1001次”会存成乱码后续JSP显示为?。这是新手最常踩的坑因为MySQL默认字符集在不同版本差异极大。用户权限需包含INSERT/UPDATE/DELETEinit.sql中INSERT INTO train语句会失败若只给SELECT权限。建议用以下命令授权CREATE USER ticket_userlocalhost IDENTIFIED BY 123456; GRANT ALL PRIVILEGES ON ticket_db.* TO ticket_userlocalhost; FLUSH PRIVILEGES;时间字段类型必须为DATETIMEinit.sql中order表的create_time字段定义为DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP。若MySQL版本低于5.6.5CURRENT_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-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login.do/url-pattern /servlet-mapping这里藏着JSPServlet架构的核心逻辑所有业务逻辑必须经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/注意末尾无斜杠血泪经验若访问首页报40490%概率是第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(); ListTrain 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 11这类注入攻击。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?page2size10statuspaid 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-nameLoginFilter/filter-name filter-classfilter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/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} vartrain标签未生效因JSTL库未正确加载。检查WebContent/WEB-INF/lib/发现jstl-1.2.jar存在但web.xml头文件未声明JSP 2.0规范。解决打开web.xml将第一行?xml version1.0 encodingUTF-8?下方的web-app声明替换为web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1关键点version3.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.timezoneGMT8JVM认为2023-05-20是UTC时间转换后比实际早8小时。解决在Tomcat的bin/catalina.shLinux或bin/catalina.batWindows中添加JVM参数# Linux catalina.sh 第20行附近添加 JAVA_OPTS$JAVA_OPTS -Duser.timezoneGMT8验证方法在Servlet中打印System.out.println(new Date());对比MySQL中SELECT NOW();结果确保两者时区一致。4.3 现象订票时抛出java.sql.SQLException: Field id doesnt 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 contentTypetext/html;charsetUTF-8 %。解决在web.xml中添加过滤器filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/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?serverTimezoneGMT%2B8useSSLfalse;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查询逻辑// 原SQLWHERE start_station LIKE ? // 新SQLJOIN 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?actionupdateStatusid123statusticketed 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 DESCALTER 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或rangekey列是否显示索引名。若为ALL说明全表扫描必须优化。从那以后我每次拿到一个JSPServlet项目第一件事就是检查web.xml的web-app版本声明和WEB-INF/lib/下的JSTL jar包是否匹配第二件事是执行SHOW CREATE TABLE看所有表的字符集和索引第三件事是用curl -v http://localhost:8080/xxx.do测试接口返回而不是直接点浏览器——因为浏览器缓存会掩盖302重定向问题。这套组合拳下来95%的“跑不起来”问题都能在5分钟内定位。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑