news 2026/10/8 19:06:48

Java火车票系统实战:Access MDB+Swing实现余票扣减与退票回滚

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java火车票系统实战:Access MDB+Swing实现余票扣减与退票回滚

简介:这是一套面向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.javaSwing主窗口,含菜单栏(售票/退票/查询/管理)和内容面板initComponents()构建GUI组件树所有按钮监听器绑定到ActionListener匿名内部类,事件分发逻辑集中
TrainService.java业务逻辑中枢,封装所有数据库操作sellTicket(String trainId),refundTicket(int orderId)使用Connection.setAutoCommit(false)开启事务,conn.commit()/rollback()控制一致性
DBUtil.java数据库连接工厂,单例模式管理ConnectiongetConnection(),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_idprivate int orderId; private String trainId;order_table主键order_id为AutoNumber,插入时不需赋值
UserLogin.java登录验证入口,校验user_tablecheckLogin(String username, String password)密码明文存储(user_table.pwd字段),仅用于教学,切勿用于真实系统
QueryPanel.java查询功能独立面板,复用MainFrame容器loadTrainData()加载车次列表到JTableJTable数据源为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处:

  1. 驱动与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。
  2. 建表SQL适配:

    • 将trainmaindb.mdb导出为CSV,用MySQL Workbench导入;
    • seat_left字段类型从Access的Number改为MySQL的INT NOT NULL DEFAULT 0;
    • 添加PRIMARY KEY和INDEX提升查询性能。
  3. 事务隔离级别调整:

    • MySQL默认REPEATABLE READ,而Access无此概念;
    • 在TrainService的sellTicket()开头添加conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE),强化并发控制。

从那以后我每次验证数据库逻辑,都强制走一遍「手动SQL修改→启动Java系统→观察GUI响应→反向查库确认」四步闭环。不是为了炫技,而是因为Access MDB的“黑匣子”特性——它不报错,但数据可能没刷盘;它不提醒,但事务可能没提交。希望帮到你。

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

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

从开发到上架:Eros 双端打包、签名与发布全流程指南

从开发到上架&#xff1a;Eros 双端打包、签名与发布全流程指南 【免费下载链接】eros &#x1f4f1; 一套 Vue 代码&#xff0c;两端原生应用 &#xff0c;或许可以叫我 weex-native。 项目地址: https://gitcode.com/gh_mirrors/er/eros Eros 是一套「一份 Vue 代码&am…

作者头像 李华
网站建设 2026/10/8 19:05:55

二维Morlet小波图像去噪:时频局部化降噪原理与工程实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 19:01:30

Flask+SQLite博客系统:从架构设计到数据库迁移完整实战

简介&#xff1a;基于Python的个人博客系统设计与实现毕业设计项目&#xff0c;完整提供后端源码、数据库脚本与软件说明书&#xff0c;面向正在学习Python Web开发或需要完成毕业设计的学生。项目参照主流Python Web框架的典型结构搭建&#xff0c;实现文章发布、评论互动、用…

作者头像 李华
网站建设 2026/10/8 18:57:18

SCADA北向接口全解析:从数据建模到MQTT/OPC UA实操

1. 北向接口到底在解决什么问题做SCADA项目最容易被忽略、又最绕不开的一个环节&#xff0c;就是数据怎么从系统里拿出来给别家用。很多人一开始接触SCADA&#xff0c;都以为它就是个“画面上显示数据、做报警、存历史曲线”的组态软件&#xff0c;等真正进入到工厂数据集成阶段…

作者头像 李华