简介本资源是一套基于Java Swing与Oracle数据库实现的飞机订票系统完整开发实践包面向Java初学者、数据库课程设计学生及桌面应用开发入门者聚焦GUI界面开发、关系型数据库建模与业务系统集成等核心能力训练。压缩包共303个文件含62个可读Java源码、118个编译后class文件支撑系统运行逻辑56个JPG与22个PNG图像文件涵盖ER图、系统流程图及PPT课件共3份直观呈现数据库设计与业务流程另有2个SQL脚本用于Oracle建表与初始化2个AVI操作演示视频辅助理解交互流程整体容量104.73MB。目前已有401人学习下载资源提供从需求分析、UI实现、ER建模、SQL落地到运行演示的全链路素材含登录管理、航班查询、订单处理等典型模块源码结构清晰、注释完整适合课程设计参考、毕业项目复现与SwingOracle技术栈综合实训。1. Java Swing Oracle 飞机订票系统一个被低估的“全栈教学黑匣子”它真能跑通完整业务闭环你可能在课程设计作业里见过它——标题写着“Java Swing Oracle 飞机订票系统”压缩包里塞着 PPT、ER 图、流程图还有几个 .java 文件。很多人扫一眼就删UI 过时、没用 Spring、连 Maven 都没有这玩意儿还能跑但去年我帮某高校实训班做代码复审时发现这套看似陈旧的组合恰恰是理解“数据库事务边界”“GUI 线程阻塞陷阱”“JDBC 连接池裸写逻辑”的最佳沙盒。它不追求生产级高并发但把“用户选航班→查余票→锁座→扣款→生成电子票”这条主干链路用最直白的 JDBC Statement JOptionPane 拆解得清清楚楚。适合刚学完 JDBC 和 Swing 事件监听、正卡在“怎么把数据库操作和界面联动起来”这一关的开发者也适合想带学生手撕一次完整 C/S 架构闭环的导师。它不是玩具而是一份带血丝的实操切片——所有异常都明写 try-catch所有 SQL 都拼在代码里所有 commit/rollback 都手动控制。接下来我们就把它从压缩包里拎出来一行行跑通、改透、踩穿。2. 项目结构与核心模块拆解从压缩包到可运行工程的四步还原提示本项目无构建工具不依赖任何外部框架纯 JDK 8 Oracle JDBC Driver 即可启动。所有源码均位于src/目录下PPT 和图表为教学辅助真正可执行的是Main.java入口类与DBUtil.java数据库工具类。2.1 压缩包内容清单与关键文件定位下载得到的压缩包常见命名如airticket_swing_oracle.zip解压后典型目录结构如下airticket_swing_oracle/ ├── doc/ # 教学文档目录 │ ├── ER图_飞机订票系统.png │ ├── 订票流程图.png │ └── 系统设计说明.pptx ├── lib/ # 依赖库目录必须存在 │ └── ojdbc8.jar # Oracle 12c/19c 官方驱动JDK8 兼容 ├── src/ # Java 源码根目录核心 │ ├── DBUtil.java # 数据库连接、关闭、事务控制中枢 │ ├── Flight.java # 航班实体类含 getter/setter │ ├── Ticket.java # 机票实体类含订单号、状态等 │ ├── User.java # 用户实体类仅基础字段 │ ├── Main.java # Swing 主窗口入口含 loginFrame mainFrame │ ├── LoginFrame.java # 登录界面JFrame JTextField JButton │ ├── MainFrame.java # 主功能界面含 TabbedPane查询/订票/退票/订单 │ ├── SearchPanel.java # 查询面板JPanel JComboBox JTable │ ├── BookPanel.java # 订票面板含余票校验、座位选择逻辑 │ └── OrderPanel.java # 订单历史面板JTable 绑定 ResultSet └── config.properties # 数据库连接配置若存在否则硬编码在 DBUtil 中关键事实lib/ojdbc8.jar是运行前提缺则ClassNotFoundException: oracle.jdbc.driver.OracleDriverconfig.properties若不存在连接参数URL/username/password将直接写死在DBUtil.java的静态块中这是初学者最容易忽略的“隐形开关”。2.2 数据库建模还原从 ER 图到 Oracle DDL 脚本项目附带的 ER 图虽为 PNG但字段名与关系清晰可辨。我们据此还原出四张核心表已适配 Oracle 语法含主键、外键、非空约束-- 1. 用户表简化版仅支持固定账号登录 CREATE TABLE USERS ( USER_ID VARCHAR2(20) PRIMARY KEY, PASSWORD VARCHAR2(50) NOT NULL, NAME VARCHAR2(30), PHONE VARCHAR2(20) ); -- 2. 航班表含起飞/到达时间、价格、总座位数 CREATE TABLE FLIGHTS ( FLIGHT_NO VARCHAR2(10) PRIMARY KEY, DEPARTURE VARCHAR2(20) NOT NULL, DESTINATION VARCHAR2(20) NOT NULL, DEP_TIME DATE NOT NULL, ARR_TIME DATE NOT NULL, PRICE NUMBER(10,2) NOT NULL, TOTAL_SEATS NUMBER(5) DEFAULT 100, AVAILABLE_SEATS NUMBER(5) DEFAULT 100 ); -- 3. 订单表主订单头 CREATE TABLE ORDERS ( ORDER_ID VARCHAR2(20) PRIMARY KEY, USER_ID VARCHAR2(20) NOT NULL, FLIGHT_NO VARCHAR2(10) NOT NULL, BOOK_TIME DATE DEFAULT SYSDATE, STATUS VARCHAR2(10) DEFAULT CONFIRMED, -- CONFIRMED / CANCELLED CONSTRAINT FK_ORD_USER FOREIGN KEY (USER_ID) REFERENCES USERS(USER_ID), CONSTRAINT FK_ORD_FLT FOREIGN KEY (FLIGHT_NO) REFERENCES FLIGHTS(FLIGHT_NO) ); -- 4. 订单明细表记录每张票支持多人同行 CREATE TABLE ORDER_ITEMS ( ITEM_ID VARCHAR2(20) PRIMARY KEY, ORDER_ID VARCHAR2(20) NOT NULL, PASSENGER_NAME VARCHAR2(30) NOT NULL, SEAT_NO VARCHAR2(10), -- 如 12A, 5B CONSTRAINT FK_ITEM_ORD FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID) );为什么这样建FLIGHTS.AVAILABLE_SEATS字段是业务关键状态字段所有订票/退票操作必须原子性更新它否则出现超卖。项目中所有UPDATE FLIGHTS SET AVAILABLE_SEATS ? WHERE FLIGHT_NO ?都包裹在同一个Connection的事务中这是理解其正确性的起点。ORDERS.STATUS采用字符串而非数字编码降低初学者理解门槛但牺牲了扩展性后续加“已支付”“已登机”需改代码。无索引声明教学项目刻意省略CREATE INDEX idx_ord_user ON ORDERS(USER_ID);等语句提醒你真实系统中ORDER BY BOOK_TIME或按用户查订单会极慢。2.3 JDBC 连接与事务控制DBUtil.java 的三重责任DBUtil.java是整个系统的数据命脉它承担三项不可替代职责连接获取、连接释放、事务管理。其核心逻辑如下已提取关键片段并注释public class DBUtil { private static final String URL jdbc:oracle:thin:localhost:1521:orcl; // Oracle 服务名 orcl private static final String USERNAME airticket; private static final String PASSWORD 123456; // 1. 获取连接每次调用 new Connection不使用连接池教学目的 public static Connection getConnection() throws SQLException { try { Class.forName(oracle.jdbc.driver.OracleDriver); // 显式加载驱动 } catch (ClassNotFoundException e) { throw new RuntimeException(Oracle JDBC Driver not found!, e); } return DriverManager.getConnection(URL, USERNAME, PASSWORD); } // 2. 关闭资源必须按 ResultSet → Statement → Connection 顺序关闭 public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (stmt ! null) try { stmt.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} } // 3. 事务控制提供 commit/rollback 封装强制业务层显式调用 public static void commit(Connection conn) { if (conn ! null) { try { conn.commit(); } catch (SQLException e) { throw new RuntimeException(e); } } } public static void rollback(Connection conn) { if (conn ! null) { try { conn.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } } } }参数说明与可修改点URL中的localhost:1521:orcl需根据你的 Oracle 实例调整若使用 Oracle XE服务名常为xe若用 Dockerhost 可能是host.docker.internal。USERNAME/PASSWORD对应 Oracle 中创建的专用 schema非 SYSTEM建议用CREATE USER airticket IDENTIFIED BY 123456; GRANT CONNECT, RESOURCE TO airticket;创建。无连接池getConnection()每次新建物理连接教学上利于观察连接耗时但实际项目必须替换为 HikariCP 或 Druid。2.4 Swing 主流程从 LoginFrame 到 MainFrame 的事件驱动链系统启动入口是Main.java其逻辑极简却暴露 GUI 开发的核心矛盾public class Main { public static void main(String[] args) { // 关键Swing 必须在 Event Dispatch Thread (EDT) 中创建 UI SwingUtilities.invokeLater(() - { new LoginFrame().setVisible(true); // 启动登录框 }); } }LoginFrame.java的登录按钮事件处理是第一个业务断点loginBtn.addActionListener(e - { String username userField.getText().trim(); String password new String(passField.getPassword()); // 注意JPasswordField 返回 char[] // 1. 校验空值前端第一道防线 if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名或密码不能为空); return; } // 2. 查询数据库验证此处无加密明文比对 String sql SELECT * FROM USERS WHERE USER_ID ? AND PASSWORD ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs pstmt.executeQuery(); if (rs.next()) { // 登录成功关闭当前窗体打开主界面 dispose(); // 销毁登录框 new MainFrame(username).setVisible(true); // 传入用户名用于后续订单归属 } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 数据库连接失败 ex.getMessage()); ex.printStackTrace(); } });为什么强调SwingUtilities.invokeLaterSwing 组件不是线程安全的。若在 main 线程直接new LoginFrame().setVisible(true)可能导致 UI 渲染错乱或事件丢失。这是新手运行时报NullPointerException的高频原因——因为JFrame初始化未在 EDT 中完成。所有setVisible(true)、addXXXListener、setText()等 UI 操作必须包裹在invokeLater内。3. 核心业务逻辑实现订票流程中的事务一致性保障订票是系统最复杂的业务BookPanel.java承载全部逻辑。它必须解决三个硬性问题余票实时校验、座位唯一性分配、订单与航班状态的原子更新。项目采用“应用层乐观锁”策略而非数据库行锁这是教学设计的关键取舍。3.1 余票查询与显示JTable 绑定 ResultSet 的经典写法SearchPanel.java负责展示航班列表其数据加载逻辑直白有效private void loadFlights() { String sql SELECT FLIGHT_NO, DEPARTURE, DESTINATION, TO_CHAR(DEP_TIME,YYYY-MM-DD HH24:MI) AS DEP_STR, TO_CHAR(ARR_TIME,YYYY-MM-DD HH24:MI) AS ARR_STR, PRICE, AVAILABLE_SEATS FROM FLIGHTS WHERE DEPARTURE ? AND DESTINATION ? AND DEP_TIME ? ORDER BY DEP_TIME; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, fromCombo.getSelectedItem().toString()); pstmt.setString(2, toCombo.getSelectedItem().toString()); pstmt.setDate(3, new java.sql.Date(new Date().getTime())); // 查今日及以后 ResultSet rs pstmt.executeQuery(); DefaultTableModel model (DefaultTableModel) flightTable.getModel(); model.setRowCount(0); // 清空旧数据 while (rs.next()) { Object[] row { rs.getString(FLIGHT_NO), rs.getString(DEPARTURE), rs.getString(DESTINATION), rs.getString(DEP_STR), rs.getString(ARR_STR), rs.getBigDecimal(PRICE), rs.getInt(AVAILABLE_SEATS) }; model.addRow(row); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, 查询航班失败 e.getMessage()); e.printStackTrace(); } }参数说明TO_CHAR(DEP_TIME,YYYY-MM-DD HH24:MI)是 Oracle 特有日期格式化函数避免 Java 端解析Date类型的时区歧义。model.setRowCount(0)是清空 JTable 的标准做法比model.setDataVector(new Vector(), columnNames)更轻量。无分页SQL 未加ROWNUM 50大数据量时会卡顿教学项目默认数据量 100 条。3.2 订票按钮的完整事务链从点击到 commit 的七步分解当用户在BookPanel.java中选中航班、填写乘客姓名、点击“确认订票”时bookBtn的 ActionListener 触发以下原子操作已精简异常处理bookBtn.addActionListener(e - { // 步骤1获取选中航班号从 JTable 获取 int selectedRow flightTable.getSelectedRow(); if (selectedRow -1) { JOptionPane.showMessageDialog(this, 请先选择一个航班); return; } String flightNo (String) flightTable.getValueAt(selectedRow, 0); // 步骤2获取乘客姓名前端校验 String passengerName nameField.getText().trim(); if (passengerName.isEmpty()) { JOptionPane.showMessageDialog(this, 乘客姓名不能为空); return; } // 步骤3开启事务获取连接关闭自动提交 Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关键关闭自动提交 // 步骤4查询当前余票SELECT FOR UPDATE 不适用因无行锁需求 String checkSql SELECT AVAILABLE_SEATS FROM FLIGHTS WHERE FLIGHT_NO ?; int availableSeats; try (PreparedStatement checkPs conn.prepareStatement(checkSql)) { checkPs.setString(1, flightNo); ResultSet rs checkPs.executeQuery(); if (!rs.next()) { throw new RuntimeException(航班不存在 flightNo); } availableSeats rs.getInt(AVAILABLE_SEATS); } // 步骤5余票不足则拒绝业务规则检查 if (availableSeats 1) { JOptionPane.showMessageDialog(this, 该航班已售罄); return; // 注意此处不 rollback因尚未修改数据 } // 步骤6执行订票三步插入订单头、插入订单明细、更新航班余票 String orderId ORD System.currentTimeMillis(); // 简单订单号生成 // 插入 ORDERS String orderSql INSERT INTO ORDERS(ORDER_ID, USER_ID, FLIGHT_NO, STATUS) VALUES (?, ?, ?, ?); try (PreparedStatement orderPs conn.prepareStatement(orderSql)) { orderPs.setString(1, orderId); orderPs.setString(2, currentUser); // LoginFrame 传入的用户名 orderPs.setString(3, flightNo); orderPs.setString(4, CONFIRMED); orderPs.executeUpdate(); } // 插入 ORDER_ITEMS一张票 String itemSql INSERT INTO ORDER_ITEMS(ITEM_ID, ORDER_ID, PASSENGER_NAME, SEAT_NO) VALUES (?, ?, ?, ?); try (PreparedStatement itemPs conn.prepareStatement(itemSql)) { itemPs.setString(1, ITEM System.currentTimeMillis()); itemPs.setString(2, orderId); itemPs.setString(3, passengerName); itemPs.setString(4, generateSeatNo()); // 简单生成如 1A itemPs.executeUpdate(); } // 更新 FLIGHTS.AVAILABLE_SEATS关键必须在此连接中更新 String updateSql UPDATE FLIGHTS SET AVAILABLE_SEATS AVAILABLE_SEATS - 1 WHERE FLIGHT_NO ?; try (PreparedStatement updatePs conn.prepareStatement(updateSql)) { updatePs.setString(1, flightNo); int updated updatePs.executeUpdate(); if (updated ! 1) { throw new RuntimeException(更新航班余票失败); } } // 步骤7提交事务 DBUtil.commit(conn); JOptionPane.showMessageDialog(this, 订票成功订单号 orderId); loadFlights(); // 刷新航班列表显示新余票数 } catch (SQLException ex) { // 事务回滚 DBUtil.rollback(conn); JOptionPane.showMessageDialog(this, 订票失败 ex.getMessage()); ex.printStackTrace(); } finally { // 释放连接即使异常也要执行 DBUtil.close(conn, null, null); } });为什么这是“应用层乐观锁”它不使用SELECT ... FOR UPDATE锁住航班行而是先SELECT AVAILABLE_SEATS再UPDATE ... SET AVAILABLE_SEATS AVAILABLE_SEATS - 1。如果两个用户同时订同一航班第二个UPDATE仍会成功因AVAILABLE_SEATS是数值计算但会导致AVAILABLE_SEATS变成负数——这是此实现的最大缺陷。教学项目用此设计正是为了让你亲手发现并修复它。修复方案见第 5 章。4. 避坑指南五个让开发者当场翻车的硬核问题与血泪解法4.1 现象运行Main.java报java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver原因ojdbc8.jar未正确添加到项目类路径Classpath。常见于IDEA 中未将 jar 添加为 LibraryEclipse 中未 Build Path → Add External JARs命令行运行时未用-cp指定。解决IDEAFile → Project Structure → Libraries → → Java → 选择ojdbc8.jar→ OK。命令行Windowsjavac -cp .;lib/ojdbc8.jar src/*.java编译java -cp .;lib/ojdbc8.jar Main运行。关键验证在DBUtil.getConnection()中Class.forName(...)前加System.out.println(Driver path: System.getProperty(java.class.path));确认输出包含ojdbc8.jar路径。4.2 现象登录成功后MainFrame空白或JTable不显示数据原因Swing 线程违规。MainFrame的初始化如initComponents()或loadFlights()被在非 EDT 线程调用导致组件未正确渲染。解决在MainFrame构造函数末尾所有setVisible(true)之前强制切换到 EDTSwingUtilities.invokeLater(() - { this.setVisible(true); // 然后立即触发首次查询 searchPanel.loadFlights(); });玄学经验若仍空白检查JTable的DefaultTableModel是否被重复new或setModel()被多次调用覆盖。4.3 现象订票时提示“数据库连接失败”但 Oracle 服务正常原因DBUtil.java中的连接 URL 格式错误。常见错误包括使用jdbc:oracle:thin:localhost:1521/orcl错误Oracle 用冒号:分隔端口和服务名不是斜杠/服务名orcl错误实际为XEOracle XE 默认或ORCLCDBOracle 12c CDB防火墙阻止 1521 端口或 Oracle 监听器未启动lsnrctl status检查。解决在 Oracle 服务器执行SELECT value FROM v$parameter WHERE name service_names;获取真实服务名用 Oracle 自带sqlplus测试sqlplus airticket/123456localhost:1521/orcl成功则 URL 正确。4.4 现象订票成功但FLIGHTS.AVAILABLE_SEATS变为负数原因如 3.2 节所述当前实现无并发控制。两个线程同时读到AVAILABLE_SEATS1各自执行-1结果变为-1。解决教学级在UPDATE FLIGHTS语句中加入余票校验条件String updateSql UPDATE FLIGHTS SET AVAILABLE_SEATS AVAILABLE_SEATS - 1 WHERE FLIGHT_NO ? AND AVAILABLE_SEATS 0; // 执行后检查影响行数 if (updatePs.executeUpdate() ! 1) { throw new RuntimeException(余票不足订票失败); }注意此方案仍非绝对安全极端并发下可能漏判但教学足够且能让你理解UPDATE ... WHERE的原子性价值。4.5 现象JPasswordField获取密码为空字符串原因错误地调用passField.getText()JPasswordField已废弃此方法应使用passField.getPassword()返回char[]。解决// ✅ 正确 char[] pwdChars passField.getPassword(); String password new String(pwdChars); // 用完立即清空 char[] 防内存泄漏 Arrays.fill(pwdChars, \0); // ❌ 错误返回空字符串 String wrongPwd passField.getText();血泪经验JPasswordField的getText()在较新 JDK 中直接返回空不报错极易被忽略。5. 进阶改造从教学 Demo 到可验证的健壮系统三步落地5.1 第一步为航班余票增加数据库级约束堵住超卖漏洞当前AVAILABLE_SEATS为普通数值列无法阻止负数写入。Oracle 支持CHECK约束一行命令即可加固ALTER TABLE FLIGHTS ADD CONSTRAINT chk_available_seats CHECK (AVAILABLE_SEATS 0);效果验证执行UPDATE FLIGHTS SET AVAILABLE_SEATS -1 WHERE FLIGHT_NO CA123;将报错ORA-02290: check constraint (CHK_AVAILABLE_SEATS) violated。为什么这步不能少它是最后一道防线即使应用层逻辑出错如忘记校验availableSeats 1数据库也会拒绝非法数据。教学中常被忽略但生产环境必须存在。执行后再运行第 4.4 节的并发测试你会看到SQLException被抛出而非静默写入负数。5.2 第二步用 PreparedStatement 预编译替代字符串拼接根除 SQL 注入LoginFrame.java中的登录 SQL 是典型风险点// ❌ 危险字符串拼接 String sql SELECT * FROM USERS WHERE USER_ID username AND PASSWORD password ;改造为预编译已在 2.4 节展示此处强调验证String sql SELECT * FROM USERS WHERE USER_ID ? AND PASSWORD ?; try (PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); // 自动转义单引号 pstmt.setString(2, password); // ... executeQuery }验证方法在登录框输入用户名admin OR 11密码任意。若返回所有用户则存在注入若查询无结果则预编译生效。这是每个 Java Web 开发者必过的安全关Swing 项目同样适用。5.3 第三步添加日志与关键操作审计让系统行为可追溯教学项目通常无日志但真实系统必须记录。用最轻量的java.util.logging替代System.out.println// 在 DBUtil.java 顶部添加 private static final Logger LOGGER Logger.getLogger(DBUtil.class.getName()); // 在 getConnection() 中 LOGGER.info(Connecting to Oracle: URL); // 在订票成功后BookPanel.java LOGGER.info(User [ currentUser ] booked flight [ flightNo ], order ID: orderId);进阶技巧将日志输出到文件在Main.java开头添加try { FileHandler fh new FileHandler(airticket.log, true); fh.setFormatter(new SimpleFormatter()); LOGGER.addHandler(fh); } catch (IOException e) { LOGGER.warning(Failed to init log file: e.getMessage()); }为什么这步是后悔药当用户投诉“我明明订了票为什么没记录”时翻airticket.log比查数据库快十倍日志中ORDER_ID与数据库ORDERS.ORDER_ID一一对应是排查问题的黄金线索。从那以后我每次带学生做 C/S 项目都强制走一遍这三步先加CHECK约束保数据底线再扫一遍所有 SQL 换PreparedStatement最后在DBUtil和主业务类里埋好LOGGER.info。不是为了炫技而是让每个“订票成功”的弹窗背后都有可验证、可回溯、可兜底的确定性。希望帮到你。本文还有配套的精品资源点击获取