news 2026/10/29 20:10:49

JAVA学生火车票订票系统实战:从建库到并发扣减余票

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JAVA学生火车票订票系统实战:从建库到并发扣减余票

简介:这份资源是面向计算机专业学生与JavaWeb初学者的一套学生火车票订票系统完整项目源码,基于JAVAEE技术栈开发,涵盖后台管理与前台购票两大模块,重点解决学生群体寒暑假半价优惠、团购购票等实际需求。压缩包共318个文件,约24.27MB,以jar依赖包、java源码与class编译文件、jsp页面为主,辅以gif/jpg/png图片素材、js与css前端资源,另含一份sql数据库脚本,可用于初始化车次、订单与用户数据表。项目采用MVC分层结构,后端业务逻辑与前端页面分离,管理员可维护列车类型、出发与到达时间及途经城市信息,学生端支持车次搜索、选座、信息填写与支付流程。目前已有3250人学习下载,适合作为课程设计、毕业设计参考或JAVAEE入门练手项目,读者可从中了解企业级Web应用的目录组织、数据库设计与前后端协作方式,并在此基础上扩展移动端适配或智能推荐等功能。

1. 从一张抢不到的下铺票说起:这个订票系统到底解决什么问题

每年寒暑假前,总有人被同一个问题折磨:帮同学、帮家里人订火车票,窗口排队排到腿软,电话订票按半天按键,网页端一到放票时间就卡成幻灯片。这个基于 JAVA 的学生火车票订票系统,本质上就是把「查车次、选座位、下单、退改签」这条链路搬到一个能自己部署、自己改的后台里。它面向的不是铁路真实生产环境,而是课程设计、毕业设计、二次开发练手这类场景——你需要一个结构完整、能跑起来、能改得动的订票业务骨架,而不是一个只能看不能碰的黑匣子。技术栈走的是 JAVA 后端加关系型数据库的经典组合,适合刚学完 Servlet/JSP 或 Spring 想找个完整业务练手的人,也适合要交一份能演示、能讲清架构的项目的同学。下面我按「它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序拆一遍,中间会穿插我踩过的坑。

2. 技术选型与目录结构:一个订票系统该长什么样

2.1 为什么是 JAVA + 关系型数据库这套组合

订票业务的核心矛盾是「并发下的座位一致性」——同一张票不能被两个人同时买走。这个场景天然适合关系型数据库的事务能力,所以项目把车次、余票、订单都落在表里,用事务保证「扣余票 + 生成订单」要么都成功要么都回滚。JAVA 侧的选择通常是 Servlet + JSP 的老派 MVC,或者 Spring Boot + MyBatis 的现代组合,具体看源码里有没有pom.xml和application.properties。老派 MVC 的好处是依赖少、启动快、适合教学演示;Spring Boot 的好处是分层清晰、好扩展。我一般会先看根目录有没有pom.xml,有就按 Maven 项目处理,没有就按传统 Web 项目丢进 Tomcat。

数据库常见做法是 MySQL 5.7/8.0,字符集用utf8mb4,因为学生姓名、车站名里偶尔会有生僻字。连接池用 Druid 或 HikariCP 都行,教学项目里手写 JDBC 工具类的也不少。这里不纠结版本号,源码里写什么就用什么,重点是理解「一张票从查询到落库」经过哪几层。

2.2 目录结构逐层拆解

一个典型的订票系统源码目录大致是这样(不同实现会有出入,按你手上的为准):

train-ticket/ ├── src/main/java/ │ ├── controller/ # 接收前端请求,参数校验 │ ├── service/ # 业务逻辑:查余票、锁座、生成订单 │ ├── dao/ # 数据库访问,SQL 集中在这里 │ ├── entity/ # 实体类:车次、座位、订单、用户 │ └── util/ # 数据库连接、日期工具、MD5 加密 ├── src/main/resources/ │ ├── mapper/ # MyBatis 的 XML(若用 MyBatis) │ └── application.properties ├── src/main/webapp/ │ ├── WEB-INF/views/ # JSP 页面 │ └── static/ # css、js、图片 └── sql/ └── train_ticket.sql # 建库建表 + 初始数据

controller层只做三件事:接参数、调 service、返回视图或 JSON。service层是重点,订票的「查余票 → 判断够不够 → 扣减 → 写订单」全在这里,事务注解也加在这一层。dao层别写业务判断,只负责 SQL。entity和数据库表一一对应,字段名建议和表字段保持一致,省得映射时来回对。sql目录里的脚本一定要先跑,否则启动后所有查询都是空表,页面一片空白,新手最容易卡在这。

2.3 核心数据表设计

订票系统的表不多,但关系要理清。常见四张主表加一张关联表:

表名作用关键字段
train车次信息车次号、始发站、终点站、发车时间、到达时间
seat座位/余票车次ID、座位类型、总数量、剩余数量、单价
orders订单订单号、用户ID、车次ID、座位类型、数量、状态
user用户用户名、密码(加密存储)、角色
passenger乘车人关联订单,姓名、证件号

seat表里的「剩余数量」是并发争抢的焦点,后面讲锁的时候会重点说。orders表的状态字段建议用枚举值(0 待支付、1 已支付、2 已取消、3 已退票),别用中文直接存,否则后面统计和查询都难受。

3. 跑起来:从建库到第一个订单的完整步骤

3.1 环境准备与建库

先确认本机有 JDK(8 或 11 都行,看源码编译级别)、Maven、MySQL 和 Tomcat(传统项目需要,Spring Boot 内置则不用)。建库这一步别偷懒,直接执行sql/train_ticket.sql:

# 登录 MySQL 后执行 mysql -u root -p CREATE DATABASE train_ticket DEFAULT CHARACTER SET utf8mb4; USE train_ticket; SOURCE /你的路径/sql/train_ticket.sql;

执行完用SHOW TABLES;确认五张表都在,再SELECT * FROM train LIMIT 5;看有没有初始车次数据。没有数据的话,页面查车次会返回空列表,很多人以为是代码问题,其实是脚本没跑全。我一般会顺手检查seat表里余票数量是否大于 0,否则下单时永远提示「余票不足」。

3.2 改配置、启动项目

找到数据库配置文件,把账号密码改成你本机的:

# application.properties 或 jdbc.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/train_ticket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码

serverTimezone这个参数别漏,MySQL 8 不配时区会报The server time zone value is unrecognized,血泪经验。改完如果是 Maven 项目,mvn clean package打出 war 包丢进 Tomcat 的webapps,或者直接mvn spring-boot:run。启动日志里看到 Tomcat started 或 Started Application 就算成功,浏览器访问http://localhost:8080/看登录页出不出来。

3.3 走一遍完整业务流程

登录后按「查车次 → 选座位 → 填乘车人 → 提交订单 → 查看订单」走一遍。这一步是验证环境是否真的通了,比看日志靠谱。查车次时注意看返回的余票数是不是和数据库一致;下单成功后回到seat表看剩余数量有没有减 1,orders表有没有新记录。如果订单生成了但余票没减,说明事务或扣减逻辑有问题,后面排查章节会讲。退票流程也走一遍,确认余票能加回去、订单状态变成已退票。

提示:第一次跑建议把日志级别调到 DEBUG,SQL 打印出来,能直观看到每条语句执行顺序,排查问题时省一半时间。

4. 核心模块拆解:订票链路上的三段关键代码

4.1 车次查询与余票展示

查询逻辑本身不复杂,难在「按条件动态拼 SQL」。用户可能只填始发站,也可能始发终点都填,还可能加日期。常见做法是用 MyBatis 的动态 SQL 或手写拼接加参数校验:

// TrainService.java 查询车次 public List<TrainVO> searchTrains(String from, String to, String date) { StringBuilder sql = new StringBuilder( "SELECT t.*, s.seat_type, s.remain_count, s.price " + "FROM train t JOIN seat s ON t.id = s.train_id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (StringUtils.isNotBlank(from)) { sql.append("AND t.start_station = ? "); params.add(from); } if (StringUtils.isNotBlank(to)) { sql.append("AND t.end_station = ? "); params.add(to); } if (StringUtils.isNotBlank(date)) { sql.append("AND DATE(t.start_time) = ? "); params.add(date); } sql.append("ORDER BY t.start_time ASC"); return jdbcTemplate.query(sql.toString(), params.toArray(), new TrainRowMapper()); }

这段的关键点:WHERE 1=1是为了后面统一用AND拼接,省得判断第一个条件;所有用户输入都走?占位符,别字符串直接拼,否则就是 SQL 注入的活教材;JOIN seat一次把余票带出来,避免页面循环查库。参数说明上,from/to是车站名,date是yyyy-MM-dd格式,前端传空字符串时isNotBlank会跳过该条件。返回的TrainVO里同时含车次和座位信息,页面直接遍历渲染。

4.2 下单扣减余票:事务和并发是重点

下单是整个系统最容易翻车的地方。正确顺序是:开事务 → 查余票 → 判断够不够 → 扣减 → 写订单 → 提交。扣减时用「带条件的 UPDATE」而不是「先查再改」:

@Transactional(rollbackFor = Exception.class) public String createOrder(Long userId, Long trainId, String seatType, int count) { // 1. 条件扣减:只有余票足够才扣得动,返回影响行数 int affected = seatDao.reduceRemain(trainId, seatType, count); if (affected == 0) { throw new BizException("余票不足,请选择其他车次"); } // 2. 生成订单号并落库 String orderNo = "T" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000)); ordersDao.insert(orderNo, userId, trainId, seatType, count, 0); return orderNo; }

对应的 DAO SQL 是灵魂:

UPDATE seat SET remain_count = remain_count - #{count} WHERE train_id = #{trainId} AND seat_type = #{seatType} AND remain_count >= #{count}

remain_count >= #{count}这个条件把「判断」和「扣减」合成一个原子操作,数据库行锁保证同一行不会被并发改乱。如果先SELECT再UPDATE,两个线程可能都查到余票 1,然后都去扣,最后余票变 -1,这就是超卖的经典翻车现场。@Transactional的rollbackFor = Exception.class别省,默认只回滚运行时异常,业务异常不配这个就不回滚。订单号用时间戳加随机数,教学够用,生产环境得用雪花算法或号段服务。

4.3 退票与状态流转

退票不是删订单,是改状态加回余票,同样要在一个事务里:

@Transactional(rollbackFor = Exception.class) public void refund(String orderNo) { Orders order = ordersDao.findByNo(orderNo); if (order == null || order.getStatus() != 1) { throw new BizException("订单状态不允许退票"); } // 先改状态,用状态做乐观锁,防止重复退 int updated = ordersDao.updateStatus(orderNo, 1, 3); if (updated == 0) { throw new BizException("退票失败,请勿重复操作"); } seatDao.addRemain(order.getTrainId(), order.getSeatType(), order.getCount()); }

updateStatus(orderNo, 1, 3)的意思是「只有当前状态是 1(已支付)才改成 3(已退票)」,返回 0 说明状态已经被别的请求改过,直接抛异常。这样即使用户连点两次退票按钮,也只有一次能成功,余票不会多加。这个套路叫状态机加乐观锁,比加分布式锁轻量得多,订票这种单库场景完全够用。

5. 避坑与排查:五个真实会卡住你的问题

5.1 页面查不到车次,数据库明明有数据

现象是查询页返回空列表,但SELECT * FROM train有记录。原因多半是日期条件对不上——DATE(t.start_time) = ?里传的日期格式和库里存的差一天,或者时区导致start_time偏移了 8 小时。解决方法是先把日期条件去掉单独测,确认是条件问题后统一用yyyy-MM-dd格式传参,并在 JDBC URL 里加serverTimezone=Asia/Shanghai。

5.2 下单成功但余票没减,或者减了订单没生成

现象是数据不一致。原因通常是事务没生效:@Transactional加在了 Controller 上、或者同类内部方法自调用、或者异常被try-catch吞掉了没往外抛。解决方法是把事务注解放到 Service 的 public 方法上,catch 里要么重新抛RuntimeException,要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。改完用两个浏览器同时下单压一下,看余票会不会变负。

5.3 中文车站名存进去变问号

现象是数据库里显示???。原因是建库时字符集不是utf8mb4,或者 JDBC URL 没带characterEncoding=utf8。解决方法是重建库指定DEFAULT CHARACTER SET utf8mb4,连接串补上编码参数,已经乱码的数据只能清掉重导。这个坑在初始脚本没写字符集时特别常见。

5.4 启动报 404,页面就是不出来

现象是 Tomcat 起来了但访问根路径 404。原因通常是 war 包名成了访问前缀(比如train-ticket-1.0),或者web.xml里welcome-file没配、Spring Boot 的视图解析器前缀写错。解决方法是访问http://localhost:8080/项目名/,或者把 war 改名成ROOT.war,再检查视图解析配置里prefix和suffix拼出来的路径和 JSP 实际位置是否一致。

5.5 并发下单出现负余票

现象是压测时remain_count变成负数。原因就是前面说的「先查后改」写法,或者扣减 SQL 漏了remain_count >= #{count}条件。解决方法是统一改成条件 UPDATE,并确认数据库引擎是 InnoDB(MyISAM 不支持行锁和事务)。改完用 JMeter 或简单多线程脚本跑 50 个并发,看余票是否精确等于「初始值 - 成功订单数」。

6. 二次开发:把它改成你自己的订票业务

拿到这套源码,最值钱的改造点是「把单机订票扩展成带候补和改签的完整链路」。我的做法是先加一张waitlist候补表,退票时不是直接加余票,而是先看候补队列有没有人,有就按先来后到自动生成订单并通知,没有才把余票加回去。核心改动在退票方法里插一段:

// 退票时优先满足候补 Waitlist w = waitlistDao.findFirst(order.getTrainId(), order.getSeatType()); if (w != null) { ordersDao.insert(genOrderNo(), w.getUserId(), order.getTrainId(), order.getSeatType(), 1, 1); // 直接置为已支付 waitlistDao.remove(w.getId()); } else { seatDao.addRemain(order.getTrainId(), order.getSeatType(), order.getCount()); }

这样余票不会先加后减来回抖,候补用户也能第一时间拿到票。改签则是「退旧单 + 下新单」的组合,注意两个操作放同一个事务,且新单要先扣减成功再退旧单,顺序反了会出现「旧票退了新票没抢到」的尴尬。

验证方法上,我习惯写一个小的并发测试类,模拟 100 个线程抢 10 张票,断言成功订单数等于 10、余票等于 0、没有负值。这个测试比手工点页面靠谱得多,每次改完扣减逻辑都跑一遍。另外建议把订单状态流转画成一张状态表贴在代码注释里,改状态的地方都对照检查,避免出现「已退票又能支付」这种脏数据。

从那以后我每次接这类订票或库存类项目,都强制先把「条件扣减 + 状态机」这两段代码写死再动别的,因为超卖和重复退票这两个问题一旦上线就是灾难,事后补的后悔药成本远高于一开始多写一行 WHERE。希望这套拆解能帮你把项目跑起来,也帮你少走几个我走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 20:08:47

工业机器视觉缺陷检测实战:图像采集、HALCON部署与AI落地避坑指南

简介&#xff1a;本资源是一份面向制造业工程师、自动化视觉系统初学者及高校相关专业学习者的工业机器视觉缺陷检测技术总结PPT&#xff0c;聚焦解决行业缺乏系统化讲义与工程实践参考的痛点。内容覆盖机器视觉系统三大核心模块——图像采集&#xff08;相机、镜头、光源、数据…

作者头像 李华
网站建设 2026/10/29 20:07:35

高帧率拍摄全攻略:从快门规则到后期变速的实战指南

1. 从一次翻车说起&#xff1a;为什么我最终把镜头对准了 hyperframes三年前我第一次用相机拍家里的猫跳上茶几&#xff0c;慢动作回放时发现画面发飘&#xff0c;爪子落地那一瞬间全是虚影。当时我用的还是 60fps&#xff0c;以为是没对上焦&#xff0c;反复试了几次才发现问题…

作者头像 李华
网站建设 2026/10/29 20:03:52

“双师”,不是评出来的,是长出来的——教师能力清单与成长逻辑

先看一个很普遍的现象&#xff1a;一说要建双师型教师队伍&#xff0c;很多学校和老师的第一反应是“考证”——赶紧考个职业资格证、技能等级证&#xff0c;把证书凑齐&#xff0c;双师比例就上去了。于是出现了一种很拧巴的“双师”&#xff1a;证有了&#xff0c;但设备没怎…

作者头像 李华
网站建设 2026/10/29 20:02:53

想转Agent开发?别急着学框架,先改这3个思维定式!收藏这份转型指南

本文针对想从后端转行Agent开发的程序员&#xff0c;提出转型时不应急于学习LangChain、RAG等框架&#xff0c;而应首先转变思维模式。文章强调后端开发的确定性思维与Agent开发中的概率性思维差异&#xff0c;指出自学Agent时缺乏反馈机制的问题&#xff0c;并给出两条转型建议…

作者头像 李华