资讯详情

JavaWeb课设实战:Servlet+JSP+MySQL三步搭建火车售票系统

📅 2026/10/7 17:48:45 | 华诺云谱 👁 阅读
JavaWeb课设实战:Servlet+JSP+MySQL三步搭建火车售票系统
简介基于JavaWeb的火车售票系统完整项目面向计算机相关专业正在完成期末大作业或课程设计的学生也适合需要JavaWeb实战练习的开发者。项目曾获导师指导认可、评审分99分代码与文档齐全可直接运行并作为设计参考。压缩包共1290个文件约33.99MB涵盖html、css、js前端页面jsp与java后端逻辑以及class、jar等编译运行文件并附有sql数据库脚本和pdf文档说明便于理解系统设计。已有239人学习下载适合用于项目答辩与功能扩展。源码中包括后台管理、会员管理、订票处理等模块注释清晰可快速搭建车次查询、购票等核心流程同时帮助学习者梳理JavaWeb分层开发思路与数据表设计方法。1. 火车售票系统凭什么成为 JavaWeb 课设的“万金油”项目做期末大作业最怕的不是功能难而是选题太散图书管理系统被做了上百遍电商项目又容易陷在支付和库存里出不来。火车售票系统是个少有的平衡点——它既有登录注册、数据增删改查这些 JavaWeb 必考动作又有余票查询、购票下单这种带并发语义的业务场景作为课程设计案例源码来交付非常合适。这套系统的核心就三件事用户管理、车次余票、订单事务。数据库是它的命门文档说明负责把设计思路讲圆。它适合第一次完整走一遍 Servlet JSP MySQL 技术栈的人也适合答辩前才想明白“原来老师问的都是这些点”的补救型选手。2. 从零搭 JavaWeb 火车售票系统技术栈与三层架构拆解2.1 Servlet JSP MySQL为什么这套老组合仍是课设最优解见过不少同学一上来就上 Spring Boot MyBatis Plus结果课设答辩被老师问“MyBatis 和 JDBC 是什么关系”就卡住了。对期末大作业和课程设计来说Servlet JSP MySQL 的竞争力恰恰在“老”每一层都能被单独拷问每一行代码都是你亲手写的没有框架帮你把黑匣子盖住。常见做法是分层Servlet 做请求分发和控制JSP 做页面展示DAO 层用 JDBC 操作数据库。这正好对应经典的三层架构文档里画个分层图就是最标准的设计说明。有人担心 JSP 过时了但课设考察的是你对 HTTP 请求、会话状态、SQL 事务的理解而不是框架熟练度。把这个组合跑通再去看 Spring MVC 就很容易理解“DispatcherServlet 到底在调度什么”。MySQL 的使用同样如此。课设场景下的表不超过十张单机并发几十个人不存在分库分表问题。把索引、事务、连接池用明白比背一堆中间件名词管用。后面第 4 章会专门讲连接池怎么配这一章先把工程骨架立起来。2.2 建表与关系设计用户、车次、余票、订单的 ER 模型火车售票系统的表可以设计成很多种形态这里给出一种最适合课设讲解的一站直达模型一个车次就是一趟完整的运行没有中途停站。这样既保留了“余票”这一核心字段又不至于让多站点联表查询把课设拖垮。数据库里一共有四张表用户表sys_user、车次表train、订单表orders再加一张可选的日志表。用户和订单是 1 对 N订单和车次是 N 对 1这两条关系足够演示外键和联表查询了。下面是初始化建库建表的 SQLCREATE DATABASE IF NOT EXISTS train_db DEFAULT CHARSET utf8mb4; USE train_db; -- 用户表密码列存的是加盐后的 MD5 摘要不是明文 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, salt VARCHAR(16) NOT NULL, real_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 车次表seat_left 是剩余票数也是并发控制的关键字段 CREATE TABLE train ( id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(20) NOT NULL UNIQUE, from_station VARCHAR(50) NOT NULL, to_station VARCHAR(50) NOT NULL, depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL, total_seats INT NOT NULL, seat_left INT NOT NULL ); -- 订单表status 为 0 表示已支付1 表示已退票 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, train_id INT NOT NULL, buy_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0, FOREIGN KEY (user_id) REFERENCES sys_user(id), FOREIGN KEY (train_id) REFERENCES train(id) );这里有两处要注意。第一用户表不要起名userMySQL 自带mysql.user系统表见名容易混淆一些可视化工具还会给非关键字加蓝框纯粹的麻烦。第二password字段长度给 64是因为 MD5 摘要转十六进制后固定 32 位再加点冗余给以后换成 SHA-256 留余地。seat_left必须单独成列而不是每次用total_seats - 已售数量算出来否则购票时做原子扣减会很别扭。2.3 用 Maven 骨架在 IDEA 里跑通最小工程pom.xml 与项目结构现在打开 IDEA新建一个 Maven 项目勾选 Web 支持。这是很多人第一次在 IDEA 里跑 JavaWeb 项目最容易卡住的环节我把最小可运行的pom.xml直接贴出来properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet APITomcat 提供scope 用 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API 同理 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库JSP 里做循环和格式化用 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8.x 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies依赖就这五个别再往里面塞 Spring、MyBatis 这些框架。注意 Servlet 和 JSP 的依赖必须用provided意思是“编译时需要运行时 Tomcat 自己有”不写这个会导致打包出的 war 里塞进两份 servlet-api运行时报各种类找不到。工程结构上我一般这么组织src/main/java下按 controller、dao、entity、util 分包src/main/resources放druid.properties和 JDBC 工具类src/main/webapp下放 JSP、CSS、JS 和WEB-INF/web.xml。写完代码后在 IDEA 的运行配置里选 Tomcat Server LocalDeployment 页签把 war exploded 加上Application context 写成/train这就在 IDEA 里跑通了一个最小的 JavaWeb 工程。所有 JavaWeb 笔记都会强调 Servlet 生命周期但课设里真正要紧的是先把 Tomcat 跑起来再管生命周期。3. 核心功能实现注册登录、余票查询与购票下单3.1 注册登录与 MD5 加盐别再把密码明文存数据库答辩时老师常问的第一个问题是“密码怎么存的”。最减分的回答是“明文存的无所谓”这会直接拉低对后面所有设计的印象。正确做法是加盐后做摘要把盐和摘要一起存数据库。验证时用同样的盐重算摘要比对即可。注册的 Servlet 里核心逻辑是这样一段// 生成 8 位随机盐 String salt UUID.randomUUID().toString().replace(-, ).substring(0, 8); // 密码 md5(盐 明文) String passwordMd5 MD5Util.md5(salt password); String sql INSERT INTO sys_user (username, password, salt, real_name) VALUES (?, ?, ?, ?); try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, passwordMd5); ps.setString(3, salt); ps.setString(4, realName); ps.executeUpdate(); }MD5Util可以自己用MessageDigest写一个十几行的方法不需要额外引依赖。登录时先按用户名查出salt和password再对用户输入的密码做同样运算比较摘要是否一致。一致就把用户对象放进 Sessionsession.setAttribute(loginUser, user);后面 JSP 里判断用户是否登录直接看session.getAttribute(loginUser)是否为空。这里要提醒一句Session 只在同一个浏览器会话里有效如果你用 Postman 测接口每次请求都是新会话这是个容易让人误以为程序出 bug 的坑。3.2 余票查询按出发地、到达地和出发日期三条件动态拼 SQL余票查询的关键不是查表而是“动态条件”。用户可能只填出发地也可能三个条件都填。最偷懒的写法是拼三个if生成不同 SQL但用StringBuilder 参数列表的方式更干净也好扩展StringBuilder sql new StringBuilder(SELECT * FROM train WHERE 11); ListObject params new ArrayList(); if (from ! null !from.isEmpty()) { sql.append( AND from_station ?); params.add(from); } if (to ! null !to.isEmpty()) { sql.append( AND to_station ?); params.add(to); } if (departDate ! null !departDate.isEmpty()) { // DATE(depart_time) 只比较日期部分忽略时刻 sql.append( AND DATE(depart_time) ?); params.add(departDate); } sql.append( AND seat_left 0); try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); // 遍历结果集封装成 Train 对象放进 List }WHERE 11看起来不优雅但可以让后续每个条件都无脑AND开头少写一堆flag判断是典型的课设代码风格。为什么用PreparedStatement而不是Statement第一防注入第二参数有专门 API。如果你非要用字符串拼接用户的输入那碰到from_station填 OR 11 --时整张表都查出来了答辩直接变成安全事故现场。日期参数我建议用字符串传前端给的2025-06-01直接传给 SQL由 MySQL 自动转换不要自己在 Java 里再转一遍Date凭空引入时区问题。3.3 购票下单与事务扣余票和插订单必须同生共死购票是整个系统最值钱的代码没有之一。它包含两个写操作把train表的seat_left减一往orders表插一条订单。这两个操作必须在一个事务里要么都成功要么都失败。更关键的是扣余票时必须用一行带条件判断的 SQL否则并发时会超卖。Connection conn null; PreparedStatement psUpdate null; PreparedStatement psInsert null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); // 手动控制事务 // 扣余票seat_left 0 是并发防线影响行数为 0 说明抢不到 String updateSql UPDATE train SET seat_left seat_left - 1 WHERE id ? AND seat_left 0; psUpdate conn.prepareStatement(updateSql); psUpdate.setInt(1, trainId); int rows psUpdate.executeUpdate(); if (rows 0) { conn.rollback(); // 提示“余票不足”返回购票失败页面 return; } // 生成订单号并插入订单 String orderNo T System.currentTimeMillis(); String insertSql INSERT INTO orders (order_no, user_id, train_id) VALUES (?, ?, ?); psInsert conn.prepareStatement(insertSql); psInsert.setString(1, orderNo); psInsert.setInt(2, userId); psInsert.setInt(3, trainId); psInsert.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw new RuntimeException(购票失败, e); } finally { // 归还连接池里的连接关闭语句对象 JdbcUtil.close(psUpdate, psInsert, conn); }第一眼看这段代码很多人会问为什么不先SELECT seat_left判断再UPDATE因为 select 和 update 之间有时间差两个并发请求都查到余票为 1然后各自扣减最后就把余票扣成 -1 了。把判断条件塞进UPDATE的WHERE子句让数据库的行锁替你做原子判断影响行数是 0 就说明没抢到。这是最常见也最好讲的并发控制方案答辩时把这个点主动讲出来整个项目的档次立刻不一样。事务要记住三句话setAutoCommit(false)开启commit()成功提交任何异常rollback()回滚。如果忘记回滚下一次复用时连接池会把未提交的事务带出来出现“上一个请求的数据出现在下一个请求里”的诡异现象。4. 把数据库与 JDBC 连接池调稳从连接建立到连接关闭4.1 JDBC 增删改查的规范写法从 DriverManager 到 PreparedStatement很多课程设计案例源码里每一处 DAO 方法都写一遍Class.forName、DriverManager.getConnection、finally close代码冗余不说连接还在每次操作后物理关闭性能很差。规范做法是把获取连接和关闭资源封装成一个工具类所有 DAO 都走它。public class JdbcUtil { private static DataSource dataSource; static { // 从 resources 根目录加载连接池配置 Properties props new Properties(); try (InputStream in JdbcUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { throw new RuntimeException(e); } } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }这里只有一个静态代码块类加载时初始化一次连接池。getConnection()每次从池里借连接用完close()只是归还而不是断开这才符合“数据库连接池”的语义。关闭方法的参数设计成可变长AutoCloseableConnection、Statement、ResultSet都能传调用方少写五行样板代码。所有 DAO 都遵循同一个模板拿连接预编译 SQL绑定参数执行关闭。这套模板写熟之后系统的增删改查就全是体力活。如果哪一步抛了异常优先看异常发生在绑定参数还是执行阶段前者多半是类型转换后者多半是 SQL 语法或约束冲突。4.2 用 Druid 连接池替换零散连接核心参数与配文件落地Druid 是阿里的连接池配置比 C3P0 精简还自带监控页。课设用它不是图监控是图druid.properties十行就能配完给老师讲参数也讲得清。先看配置文件driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/train_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 minIdle5 maxActive20 maxWait5000 validationQuerySELECT 1参数不多但每个都值得在文档里写一句。initialSize是启动时预建几个连接maxActive是池子最大连接数maxWait是借不到连接时的最长等待毫秒数validationQuery是取连接前做的保活探测。课设并发量不大maxActive20足够如果你把maxActive配成 1两个页面同时查库就会互相等待表现成“页面卡死”这是新人最容易踩的配置坑。连接串里的四个尾巴参数也讲一下characterEncodingutf8管住数据库读写的中文编码useSSLfalse关掉本地开发用不上的加密握手避免连不上时一堆 SSL 报错serverTimezoneAsia/Shanghai解决 MySQL 8 的时区差八小时问题useUnicodetrue严格说可以省略但写上更保险。加载方式和 4.1 节代码里写的一致用类加载器的getResourceAsStream(druid.properties)读取。一个常见问题是把配置文件放在src/main/java下面编译后它不在 classpath 根路径里运行时报空指针。请务必放在src/main/resources根目录下。4.3 初始化数据脚本车次、车站与余票数据一次到位连接池配好数据库得有数据才能调功能。写一个init_data.sql插入几条真实感强的车次供页面展示USE train_db; -- 密码为占位符正式数据请用 MD5Util 生成的摘要替换 INSERT INTO sys_user (username, password, salt, real_name) VALUES (zhangsan, 占位密文, abc12345, 张三); INSERT INTO train (train_no, from_station, to_station, depart_time, arrive_time, price, total_seats, seat_left) VALUES (G1, 北京南, 上海虹桥, 2025-06-01 08:00:00, 2025-06-01 13:28:00, 553.00, 100, 100), (Z161, 北京西, 昆明, 2025-06-01 10:30:00, 2025-06-02 13:00:00, 318.00, 80, 80), (D921, 广州南, 长沙南, 2025-06-01 09:15:00, 2025-06-01 11:02:00, 314.00, 60, 60);插入数据有几个细节。票价用DECIMAL(10,2)别用FLOAT否则 553 会存成 553.0000001。depart_time和arrive_time用DATETIME而不是TIMESTAMP课设范围内没有跨时区需求DATETIME不存在 2038 年问题也少一套转换逻辑。total_seats和seat_left初始化相等这样演示时“买一张票余票从 100 变 99”的效果是肉眼可见的。写好的脚本在 IDEA 的 Database 面板里右键执行即可。如果你习惯命令行一句mysql -uroot -p init_data.sql也能完成。数据是课设的演示道具数据太少体现不出查询功能这几条已经足够支撑余票查询的条件筛选。5. 期末答辩避坑指南课设里最常翻车的 5 个地方5.1 改了代码没生效热部署失效与浏览器缓存现象Servlet 改了一行逻辑重启 Tomcat 后页面还是旧行为怎么试都不对。原因有两层。第一层是 IDEA 的 Tomcat 运行配置里 “On frame deactivation” 默认是 “Update resources”只更新静态资源Java 代码改了需要手动点 “Redeploy” 或 “Restart Server”。第二层是浏览器缓存了旧的 JS 和 CSSJSP 本身是动态的但外链的.js文件会被浏览器强缓存。解决把 Tomcat 配置改成 “Update classes and resources”IDE 失焦时自动重新部署演示前用 CtrlF5 强制刷新页面改完 JS 顺手给script srcapp.js?v2加个版本参数。这套配置属于 IDEA 运行 JavaWeb 项目配置里最常被忽略的一环答辩前务必演练一遍。5.2 中文乱码请求参数乱码与响应乱码是三件事现象表单提交的中文在数据库里变成??或者页面输出繁体字变成乱码。原因乱码问题至少涉及三个独立环节只修一处没用。请求参数在 Servlet 里读出来乱码是因为还没读参数时没有执行request.setCharacterEncoding(UTF-8)响应页面乱码是因为没有设置response.setContentType(text/html;charsetUTF-8)数据库里存进去就是乱码是连接串缺characterEncodingutf8或表不是utf8mb4。解决写一个过滤器统一处理所有请求和响应的编码这是课设里最优雅的写法数据库建库时DEFAULT CHARSET utf8mb4保证第一道关。演示时如果页面还是乱码先 F12 看响应头的Content-Type再查数据库表结构三步排查法能省一小时。5.3 数据库连不上时区、SSL 与驱动版本的三连坑现象启动时抛Communications link failure或者Unable to load authentication plugin caching_sha2_password。原因这些报错大部分不是数据库没启动而是连接参数不是这年代的样子。MySQL 8 默认认证插件换了老驱动不认连接串没带serverTimezone驱动拿本地时区去校准本地时区不是标准 GMT 就报错useSSLfalse没加两边来回握手失败。解决驱动统一用mysql-connector-java8.0.x连接串把serverTimezoneAsia/Shanghai和useSSLfalse都写上再用telnet localhost 3306确认端口通不通。这套参数组合我在课设里给学弟处理过不下十次说它是玄学一点不过分其实全是这三个固定原因之一。5.4 购票多卖一张票没有事务和门槛判断的超卖现象余票只剩 1 张两个人同时点击购票数据库里出现两条成功订单余票变成 -1。原因代码先SELECT seat_left判断再UPDATE扣减两条请求同时读到“还有 1 张”各自通过检查双双更新。select 和 update 之间的时间窗口就是漏洞。解决按第 3.3 节的写法把余票判断放进UPDATE的WHERE子句配合事务提交回滚。答辩时这几乎是必考题主动讲出“为什么不能先查再改”会让老师觉得你真懂了并发。5.5 课程设计源码分享出去跑不起来Maven 仓库与项目结构不一致现象把自己打包的源码工程发给同学对方 IDEA 打开后 pom 爆红JDK 版本不对Tomcat 配置丢失直接说不运行。原因Maven 依赖在你自己机器上下载过别人的仓库是空的你用的 JDK 15对方是 8IDEA 的运行配置是保存在你本地的.idea目录里的不会跟着源码走。解决压缩源码时别带.idea和target目录附一个README.txt写明 JDK 1.8、Maven 3.6、Tomcat 8.5、MySQL 8 这四个前置条件。pom.xml里显式声明maven.compiler.source和target为 1.8对方导入后右键 Maven Reload再去 Project Structure 里把 SDK 指到本机 JDK 8 即可。这个“跑了这机器就没问题、换机器就废”的问题是源码交付里最有价值的血泪经验。6. 给答辩加分的小技巧从“能跑”到“能讲清楚”项目能跑只是及格线答辩讲得清才是加分项。我见过太多同学代码是好的一上台就只会点页面老师一问“为什么用连接池”就愣住。这里有一个很实用的准备套路先按 30 秒、90 秒、3 分钟三档各准备一段演示脚本。30 秒版只展示注册、登录、买车票、看余票变化90 秒版加一句“订单和扣余票在一个事务里失败会回滚”3 分钟版主动展开“WHERE seat_left 0”的并发防线。文档说明别写成流水账。标准骨架是需求分析、ER 图、功能设计、数据库设计、测试用例这五段每段控制在两页以内。ER 图不用工具画直接截图数据库面板里的表关系图就够。测试用例至少写三条重点同余票并发购票、错误密码登录、空条件余票查询。每条按“输入、预期、实际”三列写就能体现课程的工程素养。我最想提醒你的是答辩前把数据库备份一个backup.sql万一演示时手滑删了订单还能现场救命。我当年吃过的亏是只顾演示流畅没准备数据恢复手段结果车次被误删只能灰溜溜重启系统。后来给学弟辅导我必问一句“你这个购票不做事务会怎样”答得上来的人基本都能拿高分。这个方向做下来技术栈不新但底子扎实源码、数据库、文档三件套都是你自己的未来面试聊项目也不心虚。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑