公司动态

Java+MySQL仿12306购票系统实战:从表设计到并发扣票

📅 2026/9/3 3:44:30
Java+MySQL仿12306购票系统实战:从表设计到并发扣票
简介一份基于Java与MySQL的仿12306购票窗口Demo定位于正在学习Java GUI编程、JDBC数据库操作及并发控制的开发者。项目完整模拟用户登录、车次查询、选座下单、支付回写等购票核心流程将Swing/JFX界面与MySQL数据存储贯通重点覆盖数据库表设计、事务原子性、多线程并发购票等实战知识点非常适合作为课程设计或毕业设计参考。压缩包共61个文件包含12个Java源码、39个编译class、3个Jar依赖包Druid、MySQL Connector等以及properties配置等包体仅4.73MBsrc、lib、bin目录明确便于快速导入Eclipse/IDEA运行调试。已有665人学习下载。通过该项目可掌握连接池配置、DBUtils封装、异常处理与并发控制技巧对构建完整业务系统很有帮助。 从简历里翻出一个早年写的 Demo 项目正好是“Java MySQL 仿 12306 窗口购票”这套组合myticket.zip 这个文件名我到现在还记得。今天把这个项目的完整思路、表结构设计、核心代码逻辑、还有当初踩过的坑一次性拆开讲清楚。不管你是刚学完 Java 基础想找个综合练手项目还是在准备面试需要拿得出手的实战案例这篇内容都能直接照着做。先说这个 Demo 能干什么用 Java Swing 写一个桌面窗口程序模拟 12306 的购票流程——注册登录、查询车次、查看余票、购买车票、生成订单所有数据存到 MySQL 里。麻雀虽小五脏俱全涵盖了 Java 面向对象、JDBC 操作、SQL 编写、GUI 事件处理这些 Java 学习阶段最重要的知识点比单纯刷题或者照着教程敲代码有用得多。1. 项目整体拆解与设计思路1.1 这个 Demo 到底在做什么很多初学者做完图书管理系统、学生管理系统之后总觉得还差点意思——那些项目太“平”了无非是增删改查。仿 12306 购票系统不一样它有真实的业务逻辑余票是动态变化的买票要扣库存订单要记录状态一个用户不能重复买同一趟车。这些规则撑起来之后项目就有了“业务感”。整个系统我拆成了四个功能块用户模块注册、登录、个人信息查看车次模块查询车次、展示余票数量购票模块选择车次、选择乘客、提交订单订单模块订单列表、退票操作每个模块对应一到两个窗口界面界面之间通过登录后的用户 ID 串联数据。这样设计的好处是边界清楚写代码的时候不会东一榔头西一棒子。1.2 技术选型为什么是 Java Swing MySQL当年选 Swing 而不是 JavaFX主要原因是 Swing 足够经典网上资料多遇到问题随便一搜就有答案。JavaFX 虽然界面更现代但配置繁琐对刚接触 GUI 编程的人不太友好。Swing 的 JFrame、JTable、JButton 这几个组件足够完成 90% 的功能而且事件监听机制是 Java 面试常问的点做一遍印象非常深。数据库用 MySQL 也是综合考虑MySQL 安装简单、社区版免费、SQL 语法标准而且大部分公司生产环境就是 MySQL学这个性价比最高。数据访问层用 JDBC 原生 API 来写刻意没有引入 MyBatis 这类框架——因为这是学习项目JDBC 能让你把 Connection、PreparedStatement、ResultSet 这些底层的流程吃透之后再学框架就很轻松。1.3 分层架构别把代码全塞在窗口里这是我想重点强调的一点。很多新手写 GUI 程序喜欢把数据库查询写在按钮的点击事件里窗口类几百行起步改一个功能要翻半天。我写这个项目的时候强制自己分层view包存放所有窗口类只负责界面展示和事件转发dao包数据访问层负责所有 SQL 操作entity包实体类对应数据库表结构util包工具类比如数据库连接管理窗口类里绝不出现DriverManager.getConnection()这种代码按钮点击后调用的是 dao 层的方法拿到结果再刷新界面。这样每个类的职责都单一出了问题定位也快。分层带来的额外好处是如果以后想换成控制台界面或者 Web 界面dao 层完全不用动。2. 数据库设计与核心表结构2.1 三张核心表用户表、车次表、订单表数据库我建了myticket库里面三张表设计的时候参考了真实售票系统的思路但做了简化。用户表t_user字段类型说明idINT 主键自增用户唯一标识usernameVARCHAR(50) UNIQUE登录用户名passwordVARCHAR(100)登录密码MD5 加密后存储real_nameVARCHAR(50)真实姓名购票时用id_cardVARCHAR(18)身份证号购票时用车次表t_train字段类型说明train_noVARCHAR(20) 主键车次号如 G101、D305start_stationVARCHAR(50)出发站end_stationVARCHAR(50)到达站start_timeVARCHAR(10)发车时间如 08:30end_timeVARCHAR(10)到达时间ticket_priceDECIMAL(10,2)票价remaining_ticketsINT余票数订单表t_order字段类型说明order_idVARCHAR(32) 主键订单号UUID 生成user_idINT下单用户train_noVARCHAR(20)购买的车次order_timeTIMESTAMP下单时间statusINT订单状态1 已支付0 已退票这里有个取舍真实 12306 的订单表会拆分出乘车人子表因为我这个 Demo 每单只买一张票就把乘车人信息冗余到订单表里了。如果以后要扩展成一次买多张票再单独建一张t_order_item表就行。2.2 关键 SQL余票查询、扣减库存、订单生成查询车次是最基础的功能支持按出发站和到达站模糊查询SELECT * FROM t_train WHERE start_station LIKE ? AND end_station LIKE ?购票是整个项目最核心的 SQL 逻辑设计成了事务操作分三步-- 第一步查询余票确认有票 SELECT remaining_tickets FROM t_train WHERE train_no ? FOR UPDATE; -- 第二步扣减余票 UPDATE t_train SET remaining_tickets remaining_tickets - 1 WHERE train_no ?; -- 第三步插入订单 INSERT INTO t_order (order_id, user_id, train_no, order_time, status) VALUES (?, ?, ?, NOW(), 1);FOR UPDATE是行级锁加上事务就能保证并发情况下不会超卖。这个知识点放到后面第 4 部分详聊。退票的逻辑刚好相反先把订单状态改成 0再把余票加回去同样用事务包起来。2.3 JDBC 连接配置DriverManager 的老实写法数据库连接我写了一个DBUtil工具类最原始的 JDBC 方式public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/myticket?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }有几个点必须注意。serverTimezoneAsia/Shanghai这段不能少MySQL 8.x 驱动不指定时区会报错characterEncodingutf8是中文不乱码的关键com.mysql.cj.jdbc.Driver是 MySQL 8.x 的驱动类名如果是 5.x 版本要写成com.mysql.jdbc.Driver。3. 核心功能实现细节3.1 注册登录密码加密与登录状态注册功能用的是 MD5 加密存储密码。为什么要加密不存明文因为明文密码一旦数据库泄露就是灾难虽然 Demo 里没有安全问题但从第一天就该养成好习惯。加密代码很简单public static String md5(String source) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(source.getBytes(utf-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); return null; } }虽然现在生产环境推荐 BCrypt 这类加盐算法MD5 其实不够安全了但作为学习项目理解“密码不能明文存储”这个思路才是重点。登录验证就是拿用户名去库里查比对 MD5 后的密码是否一致。登录成功后把用户 ID 和姓名存到一个全局变量或者传给下一个窗口的构造函数里后续购票、查订单都要用到。3.2 车次查询JTable 展示结果查询窗口的交互流程是输入出发站、到达站点查询按钮下方 JTable 显示所有符合条件的车次。JTable 的数据填充需要用到DefaultTableModel我封装了一个方法从 ResultSet 遍历数据逐行添加private void refreshTable(ListTrain trainList) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (Train t : trainList) { model.addRow(new Object[]{ t.getTrainNo(), t.getStartStation(), t.getEndStation(), t.getStartTime(), t.getEndTime(), t.getTicketPrice(), t.getRemainingTickets() }); } }这里有一个很常见的坑JTable 默认不可编辑但如果直接往 JTable 里加数据会发现单元格居然能双击编辑。需要重写isCellEditable返回 falsetable new JTable(model) { Override public boolean isCellEditable(int row, int column) { return false; } };另外做还加了一个双击获取选中行数据的功能购票按钮读取选中行避免用户选错车次。3.3 购票流程事务与状态流转用户点击购买后系统取出选中的车次信息弹出一个确认窗口显示车次、发车时间、票价用户填乘车人身份证号确认提交。这里我用了一条主干流程来展示事务处理Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 查余票并加锁 String querySql SELECT remaining_tickets FROM t_train WHERE train_no ? FOR UPDATE; PreparedStatement ps1 conn.prepareStatement(querySql); ps1.setString(1, trainNo); ResultSet rs ps1.executeQuery(); if (rs.next() rs.getInt(remaining_tickets) 0) { JOptionPane.showMessageDialog(null, 余票不足购票失败); conn.rollback(); return; } // 扣减余票 String updateSql UPDATE t_train SET remaining_tickets remaining_tickets - 1 WHERE train_no ?; PreparedStatement ps2 conn.prepareStatement(updateSql); ps2.setString(1, trainNo); ps2.executeUpdate(); // 生成订单 String orderSql INSERT INTO t_order (order_id, user_id, train_no, order_time, status) VALUES (?, ?, ?, NOW(), 1); PreparedStatement ps3 conn.prepareStatement(orderSql); ps3.setString(1, UUID.randomUUID().toString().replace(-, )); ps3.setInt(2, currentUserId); ps3.setString(3, trainNo); ps3.executeUpdate(); conn.commit(); JOptionPane.showMessageDialog(null, 购票成功订单号已生成); } catch (Exception e) { if (conn ! null) conn.rollback(); e.printStackTrace(); } finally { if (conn ! null) conn.close(); }这段代码是整个项目的精华。setAutoCommit(false)之后的三个 SQL 要么全部成功要么全部回滚不会出现“票扣了但订单没生成”这种脏数据。3.4 窗口布局JFrame 的常用套路Swing 布局没有想象中那么难。我每个窗口基本遵循同一个套路主容器用BorderLayout顶部放查询条件JPanel用FlowLayout排组件中间放JTable的滚动面板底部放操作按钮。有一点想提醒Swing 是单线程模型所有界面刷新必须在事件分发线程里做。虽然这个 Demo 的数据量小基本不会卡顿但如果在查询按钮里执行很耗时的 SQL界面会假死。标准的做法是用SwingWorker做异步处理不过作为入门项目理解这个原理就够了。4. 实操踩坑记录与常见问题排查4.1 数据库连接失败最常见的五个原因这个项目我最开始跑不起来问题全出在连接数据库上。我把踩过的坑整理成了一张速查表异常信息原因解决方案ClassNotFoundException: com.mysql.cj.jdbc.Driver没有引入 MySQL 驱动 jar 包下载 mysql-connector-java 放入项目并 Add to Build PathAccess denied for user rootlocalhost用户名或密码错误检查 DBUtil 里的账号密码Unknown database myticket数据库没创建先执行CREATE DATABASE myticketThe server time zone value Öйú±ê׼ʱ¼ä is unrecognizedJDBC URL 缺时区参数URL 末尾加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.x 安全机制URL 加allowPublicKeyRetrievaltrue第三条那个乱码看着很吓人其实就是时区问题千万别去改系统时区。4.2 中文乱码从数据库到界面的全链路排查购票系统里全是中文站名乱码问题几乎避不开。乱码的根源只有一个字符集不一致。我自己遇到过三种情况第一种数据库表是 latin1 编码。建表的时候没指定MySQL 默认用了不支持中文的字符集。解决建表语句加DEFAULT CHARSETutf8mb4已经建好的表用ALTER TABLE t_train CONVERT TO CHARACTER SET utf8mb4。第二种JDBC 连接字符集没指定。URL 里的characterEncodingutf8一定不能少这是 Java 程序和 MySQL 之间的“翻译官”。第三种Swing 窗口里输入框的乱码。这种情况基本都是编辑器编码问题把 Eclipse 或 IDEA 的项目编码统一改成 UTF-8 就好。Windows 系统下如果只改单个文件运行起来很容易因为编译字节码和源码编码不一致出现乱码。4.3 余票扣减的并发问题为什么必须用事务加锁这是面试官最爱追问的点。假设余票只剩 1 张两个用户同时点购买如果不加处理可能两个人都查询到有余票然后都执行了扣减操作最终余票变成 -1但两个人都收到了购票成功提示——这就是典型的超卖。我在 2.2 部分写的SELECT ... FOR UPDATE就是解决这个问题。这条 SQL 会锁住选中那一行第二个事务必须等第一个事务提交或回滚之后才能读到这行数据。配合事务的原子性就能保证余票扣减和订单生成要么一起成功要么一起失败。当然这只是单机场景的解决方案真实的高并发系统会用 Redis 分布式锁、消息队列等手段但 Demo 阶段理解行锁和事务就足够了。你可以做一个实验加深印象在购票代码里加个Thread.sleep(5000)然后开两个窗口同时买最后一趟车的票看看第二个窗口是不是要等第一个事务结束才返回结果。4.4 其他典型报错速查java.sql.SQLException: No suitable driver found驱动类没有加载检查Class.forName是否执行jar 包路径是否正确。OutOfMemoryError: insufficient memoryJVM 内存不足Eclipse/IDEA 里调大启动参数-Xmx512m或者可能是循环构造大数据集合导致内存泄漏。Column xxx not foundSQL 里字段名写错了。建议 SQL 语句里的字段一律和表结构保持一致别用驼峰映射的思维去写因为 JDBC 不会帮你自动转换。窗口打开后中文按钮全部变成“口口”字体问题。Windows 下指定new Font(微软雅黑, Font.PLAIN, 14)就能解决。写在最后这个项目的后续扩展思路项目做到能跑通、能正常买票退票已经算完成了但如果你想拿它去面试或者在简历上多写两行可以试试这几个扩展方向第一把 JDBC 换成 MyBatis 或 Spring JDBC对比一下开发效率的差异这个经历能体现你的学习能力。第二增加一个简单的管理端让管理员能添加车次、修改票价这就多了一层角色权限的设计。第三把单线程的 Swing 重构成 JavaFX界面美观度会提升一大截。我个人当时做这个项目最大的收获不是学会了 Swing 组件怎么用而是第一次理解了“一个功能从点击按钮到数据库返回结果”的完整链路。中间遇到的每一个报错本质上都是对某个知识点理解不到位排查的过程比写代码本身更有学习价值。你把这个项目从头到尾自己敲一遍再试着改几个功能Java 基础和 MySQL 操作基本就稳了。本文还有配套的精品资源点击获取