简介:这是一套面向Java初学者与数据库课程设计者的火车票售票系统实战项目,基于Java SE与关系型数据库(含MDB文件)实现,解决小型票务场景下的车次管理、余票查询、售票退票等核心业务问题。资源共89个文件,包含14个Java源码文件(含主程序与DAO逻辑)、43个编译后class文件、15个HTML页面(用于简易前端展示)、11个GIF动图(可能为界面操作示意),以及trainmaindb.mdb数据库文件和jpx配置文件等,整体压缩包仅323KB,轻量易部署。已有3472人学习下载,适合课程设计、毕业设计或Java+SQL综合实训。读者可直接运行查看完整MVC结构实现,掌握JDBC连接Access数据库、Swing/AWT界面交互、车次与座位状态持久化管理等关键技能,并通过train_table表结构与html文档快速理解数据模型与功能模块对应关系。
1. 这不是Demo,是能跑通「余票实时扣减+退票回滚」的Java火车票系统:基于Access MDB的本地化票务闭环,适合课程设计、毕设答辩和Java数据库入门实战
你可能在搜“Java火车票系统”时,点开十几个项目——结果全是空壳界面、硬编码模拟数据、连SQL都写在Java字符串里。而这个train_table.src+trainmaindb.mdb的组合,是少有的、真正用JDBC连接真实数据库、完成「查询→锁定座位→扣减库存→生成订单→退票还原」全链路的可运行系统。它不依赖Tomcat或Spring Boot,纯Swing GUI + JDBC + Access MDB,启动即用,双击train_table.jar就能进售票窗口;所有增删改查都走标准SQL(INSERT/UPDATE/SELECT/DELETE),train_table表结构清晰包含train_id,start_time,end_time,seat_total,seat_left等字段,余票扣减逻辑藏在TrainService.java的sellTicket()方法里——不是if-else模拟,而是UPDATE train_table SET seat_left = seat_left - 1 WHERE train_id = ? AND seat_left > 0加事务控制。它不适合高并发线上场景,但对Java初学者理解「数据库事务边界」「Swing事件驱动与DB交互耦合」「MDB文件作为轻量级持久化载体」这三个痛点,比任何Spring Boot教程都直给。如果你正卡在「学完JDBC不知道怎么串起来」、「课程设计要交可演示系统」、「面试被问‘怎么保证卖票不超卖’答不出具体代码」,这个包就是你的后悔药。
2. 从MDB文件到Swing界面:环境准备与核心模块拆解
2.1 环境依赖:JDK 8 + Access Database Engine + 无额外框架
这个系统刻意规避了MySQL安装、驱动配置、服务部署等门槛,选择.mdb作为数据库载体,本质是利用Windows自带的Jet引擎能力。关键事实:trainmaindb.mdb不是随便建的Access文件,它已预置三张表——train_table(车次信息)、user_table(用户账号)、order_table(订单记录),且train_table.seat_left字段默认值为正整数,这是余票逻辑成立的前提。JDK必须是8u202或更低版本(JDK 9+移除了sun.jdbc.odbc.JdbcOdbcDriver,而本系统仍用ODBC桥接),推荐使用 Adoptium Temurin 8u362 。无需下载额外JDBC驱动:Windows 10/11默认带Microsoft Access Database Engine 2010/2016 Redistributable(32位),若报java.sql.SQLException: No suitable driver found,请确认系统架构(32/64位)与Engine版本匹配——这是第一个血泪经验。
提示:不要试图用IntelliJ IDEA直接打开
.mdb文件查看数据。Access Engine不提供图形化管理界面,验证数据是否生效,请用Java代码执行SELECT * FROM train_table并打印结果。
2.2 源码结构解析:src目录下的7个核心Java类分工
压缩包中src/train_table/路径下共7个.java文件,不是杂乱堆砌,而是按MVC雏形组织:
| 文件名 | 职责 | 关键方法/字段 | 技术要点 |
|---|---|---|---|
MainFrame.java | Swing主窗口,含菜单栏(售票/退票/查询/管理)和内容面板 | initComponents()构建GUI组件树 | 所有按钮监听器绑定到ActionListener匿名内部类,事件分发逻辑集中 |
TrainService.java | 业务逻辑中枢,封装所有数据库操作 | sellTicket(String trainId),refundTicket(int orderId) | 使用Connection.setAutoCommit(false)开启事务,conn.commit()/rollback()控制一致性 |
DBUtil.java | 数据库连接工厂,单例模式管理Connection | getConnection(),closeConnection(Connection) | URL格式为jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=+绝对路径,路径含中文会失败 |
Train.java | 车次实体类,POJO映射train_table表字段 | private String trainId; private int seatLeft; | 注意:seatLeft是int类型,但MDB中对应字段为Number(长整型),类型转换需显式处理 |
Order.java | 订单实体,关联train_id和user_id | private int orderId; private String trainId; | order_table主键order_id为AutoNumber,插入时不需赋值 |
UserLogin.java | 登录验证入口,校验user_table | checkLogin(String username, String password) | 密码明文存储(user_table.pwd字段),仅用于教学,切勿用于真实系统 |
QueryPanel.java | 查询功能独立面板,复用MainFrame容器 | loadTrainData()加载车次列表到JTable | JTable数据源为DefaultTableModel,setValueAt()动态刷新余票数 |
这些类共同构成一个「无Spring、无Hibernate、无XML配置」的极简MVC:MainFrame是View,TrainService是Controller,DBUtil+Train/Order是Model层。没有IOC容器,对象创建全靠new;没有ORM映射,SQL语句硬编码在TrainService的String sql = "UPDATE ..."中——这恰恰是理解JDBC本质的最佳切口。
2.3 启动流程:从双击jar到看到售票窗口的5个关键步骤
系统打包为train_table.jar,但直接双击可能失败(缺少JRE或路径问题)。可靠启动法:
# 1. 解压后进入根目录,确认存在 trainmaindb.mdb 和 train_table.jar ls -l # 输出应含:trainmaindb.mdb train_table.jar src/ doc/ # 2. 设置JAVA_HOME指向JDK8(以Windows为例) set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202 set PATH=%JAVA_HOME%\bin;%PATH% # 3. 验证Access驱动可用(执行此命令不报错即成功) java -cp train_table.jar train_table.MainFrame # 4. 若提示"Could not find or load main class",检查MANIFEST.MF中Main-Class是否为train_table.MainFrame # 可用记事本打开train_table.jar内META-INF/MANIFEST.MF确认 # 5. 成功启动后,首屏为登录窗口,输入预置账号:admin / 123456 # 登录后进入主界面,顶部菜单栏可见"售票"、"退票"、"车次查询"等选项为什么强调java -cp启动?因为train_table.jar的MANIFEST.MF未声明Class-Path,所有依赖(如rt.jar中的JDBC类)需由JVM自动加载。双击失败90%源于JRE版本不匹配或Access驱动未注册,而命令行能精准暴露错误栈——比如SQLException: [Microsoft][ODBC Driver Manager] Data source name not found,说明Access Engine未安装。
3. 售票与退票的事务实现:看懂sellTicket()和refundTicket()里的锁与回滚
3.1 售票逻辑:UPDATE前的SELECT FOR UPDATE替代方案
Access不支持SELECT ... FOR UPDATE语法,系统采用「乐观锁+重试」策略规避超卖。核心代码在TrainService.sellTicket():
public boolean sellTicket(String trainId) { String sql = "SELECT seat_left FROM train_table WHERE train_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, trainId); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt("seat_left") > 0) { // 余票充足,执行扣减 String updateSql = "UPDATE train_table SET seat_left = seat_left - 1 WHERE train_id = ? AND seat_left > 0"; try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setString(1, trainId); int affected = updatePs.executeUpdate(); if (affected == 1) { // 扣减成功,插入订单 insertOrder(trainId, getCurrentUserId()); conn.commit(); return true; } } } } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); // 关键:事务回滚 } catch (SQLException ex) { ex.printStackTrace(); } } return false; // 无票或更新失败 }这段代码的精妙之处在于:UPDATE ... WHERE seat_left > 0既是业务校验也是原子性保障。即使两个线程同时读到seat_left=1,第二个UPDATE因WHERE条件不满足(此时seat_left已被第一个线程改为0)而返回affected=0,自然失败。无需synchronized块,避免了单线程瓶颈。insertOrder()在同一个conn中执行,确保订单与余票变更在同一事务内。
3.2 退票逻辑:refundTicket()如何安全还原库存
退票不是简单UPDATE seat_left = seat_left + 1,必须校验订单状态和车次有效性:
public boolean refundTicket(int orderId) { String sql = "SELECT train_id, status FROM order_table WHERE order_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, orderId); ResultSet rs = ps.executeQuery(); if (rs.next() && "valid".equals(rs.getString("status"))) { String trainId = rs.getString("train_id"); // 先更新订单状态为canceled updateOrderStatus(orderId, "canceled"); // 再增加余票 String updateSql = "UPDATE train_table SET seat_left = seat_left + 1 WHERE train_id = ?"; try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setString(1, trainId); updatePs.executeUpdate(); } conn.commit(); return true; } } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; }关键设计点:
- 订单表
order_table.status字段区分valid/canceled,防止重复退票; - 退票操作分两步:先改订单状态,再增库存,顺序不可逆(若先增库存再改状态,中间崩溃会导致状态不一致);
updateOrderStatus()方法同样在conn中执行,保证原子性。
3.3 查询模块:QueryPanel.loadTrainData()如何实现动态刷新
车次查询界面不是静态列表,而是实时读取train_table最新数据:
public void loadTrainData() { String sql = "SELECT train_id, start_time, end_time, seat_total, seat_left FROM train_table ORDER BY train_id"; DefaultTableModel model = (DefaultTableModel) trainTable.getModel(); model.setRowCount(0); // 清空旧数据 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Object[] row = { rs.getString("train_id"), rs.getString("start_time"), rs.getString("end_time"), rs.getInt("seat_total"), rs.getInt("seat_left") // 注意:此处直接读取最新seat_left值 }; model.addRow(row); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, "查询失败:" + e.getMessage()); } }为什么每次点击“查询”按钮都调用此方法?因为Swing GUI与数据库无长连接,每次操作都是短连接。model.setRowCount(0)清空表格再addRow()填充,确保显示的是数据库当前快照,而非内存缓存——这是初学者常误以为“数据没更新”的根源。
4. 避坑指南:5个让90%人卡住的Access+JDBC实战陷阱
4.1 现象:启动报java.lang.ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver
原因:JDK 8u202之后版本(包括所有JDK 9+)已彻底移除ODBC桥接驱动,而本系统DBUtil.java中仍写Class.forName("sun.jdbc.odbc.JdbcOdbcDriver")。
解决:降级到JDK 8u202(推荐 Adoptium JDK8u202-b08 ),或修改DBUtil.getConnection()为使用UCanAccess驱动(需下载ucanaccess-5.0.1.jar并添加到classpath,URL改为jdbc:ucanaccess://path/to/trainmaindb.mdb)。
4.2 现象:登录成功后主界面空白,菜单栏不显示
原因:MainFrame.java中initComponents()方法调用了this.setExtendedState(JFrame.MAXIMIZED_BOTH),但在某些Windows缩放设置(如125%)下导致组件渲染异常。
解决:注释掉该行,或改为this.setSize(1024, 768),重启程序即可正常显示。
4.3 现象:售票时提示“余票不足”,但数据库中seat_left明明大于0
原因:trainmaindb.mdb文件被其他程序(如Access软件)独占锁定,JDBC连接只能读取旧快照。Access MDB不支持多进程写入,同一时刻仅允许一个连接修改。
解决:关闭所有Access进程(任务管理器中结束MSACCESS.EXE),或复制一份trainmaindb.mdb作为工作副本,修改DBUtil.java中数据库路径指向新文件。
4.4 现象:退票后余票数没增加,订单状态却变成canceled
原因:refundTicket()中updateOrderStatus()和UPDATE train_table不在同一事务块,或conn.commit()被遗漏。检查DBUtil.getConnection()返回的Connection是否被多个方法复用——本系统要求所有DAO操作共享同一Connection实例。
解决:确认refundTicket()方法内Connection conn是try-with-resources外声明的,且updateOrderStatus()和UPDATE语句共用该conn。
4.5 现象:train_table.html文档打不开,显示乱码
原因:该HTML是用GB2312编码保存,但现代浏览器默认UTF-8解析。
解决:用记事本打开train_table.html,另存为→编码选择“ANSI”(Windows平台即GB2312),保存后重新打开。
5. 进阶验证:用SQL脚本校验事务一致性与边界场景
5.1 构建压力测试:模拟并发售票的临界点
Access虽非高并发数据库,但可通过多线程验证事务隔离性。编写测试类ConcurrencyTest.java:
public class ConcurrencyTest { public static void main(String[] args) throws InterruptedException { int totalTickets = 1; // 设定初始余票为1 // 先用SQL将某车次seat_left设为1 // UPDATE train_table SET seat_left = 1 WHERE train_id = 'G101' List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 10; i++) { Thread t = new Thread(() -> { TrainService service = new TrainService(); boolean success = service.sellTicket("G101"); System.out.println(Thread.currentThread().getName() + " 卖票结果:" + success); }); threads.add(t); } for (Thread t : threads) t.start(); for (Thread t : threads) t.join(); // 最终查询余票 String checkSql = "SELECT seat_left FROM train_table WHERE train_id = 'G101'"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql); ResultSet rs = ps.executeQuery()) { if (rs.next()) { System.out.println("最终余票:" + rs.getInt("seat_left")); // 应输出0 } } } }预期结果:10个线程中仅1个返回true,其余9个false,最终seat_left=0。若出现seat_left=-1,说明事务未生效或WHERE seat_left > 0条件失效——此时需检查JDK版本和Access Engine兼容性。
5.2 边界场景验证表:用SQL手动触发并观察系统行为
| 场景 | SQL操作 | 预期系统响应 | 验证方式 |
|---|---|---|---|
| 超卖防护 | UPDATE train_table SET seat_left = 0 WHERE train_id = 'G101' | 再次点击“售票”按钮,提示“余票不足” | 查看Swing弹窗消息 |
| 退票幂等 | UPDATE order_table SET status = 'canceled' WHERE order_id = 1001,再执行退票 | 返回false,余票不变 | 查询train_table.seat_left值 |
| 订单关联失效 | DELETE FROM train_table WHERE train_id = 'G101',再尝试退票ID=1001的订单 | refundTicket()返回false,无异常抛出 | 控制台无SQLException打印 |
| 时间格式异常 | UPDATE train_table SET start_time = '25:00:00' WHERE train_id = 'G101' | QueryPanel.loadTrainData()抛出SQLException,弹窗提示“查询失败” | 观察GUI错误提示 |
注意:执行上述SQL需用Access软件或UCanAccess命令行工具,切勿在Java代码中直接执行,否则可能破坏事务一致性。
5.3 从MDB迁移到MySQL:3步替换数据库的实操清单
若需升级为MySQL(如课程设计要求),只需修改3处:
驱动与URL:
- 删除
DBUtil.java中Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); - 添加MySQL驱动
mysql-connector-java-8.0.33.jar到classpath; - 修改
DBUtil.getConnection()中URL为jdbc:mysql://localhost:3306/traindb?serverTimezone=GMT%2B8&useSSL=false。
- 删除
建表SQL适配:
- 将
trainmaindb.mdb导出为CSV,用MySQL Workbench导入; seat_left字段类型从Access的Number改为MySQL的INT NOT NULL DEFAULT 0;- 添加
PRIMARY KEY和INDEX提升查询性能。
- 将
事务隔离级别调整:
- MySQL默认
REPEATABLE READ,而Access无此概念; - 在
TrainService的sellTicket()开头添加conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE),强化并发控制。
- MySQL默认
从那以后我每次验证数据库逻辑,都强制走一遍「手动SQL修改→启动Java系统→观察GUI响应→反向查库确认」四步闭环。不是为了炫技,而是因为Access MDB的“黑匣子”特性——它不报错,但数据可能没刷盘;它不提醒,但事务可能没提交。希望帮到你。
本文还有配套的精品资源,点击获取