☰
Java Swing+Oracle飞机订票系统:JDBC事务与GUI线程实战教学
2026/10/20 18:38:01 网站建设 项目流程

简介:本资源是一套基于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落地到运行演示的全链路素材,含登录管理、航班查询、订单处理等典型模块源码,结构清晰、注释完整,适合课程设计参考、毕业项目复现与Swing+Oracle技术栈综合实训。

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.OracleDriver;config.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;若用 Docker,host 可能是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.invokeLater?
Swing 组件不是线程安全的。若在 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 添加为 Library;Eclipse 中未 Build Path → Add External JARs;命令行运行时未用-cp指定。
解决:

  • IDEA:File → Project Structure → Libraries → + → Java → 选择ojdbc8.jar→ OK。
  • 命令行(Windows):javac -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)之前,强制切换到 EDT:
    SwingUtilities.invokeLater(() -> { this.setVisible(true); // 然后立即触发首次查询 searchPanel.loadFlights(); });
  • 玄学经验:若仍空白,检查JTable的DefaultTableModel是否被重复new,或setModel()被多次调用覆盖。

4.3 现象:订票时提示“数据库连接失败”,但 Oracle 服务正常

原因:DBUtil.java中的连接 URL 格式错误。常见错误包括:

  • 使用jdbc:oracle:thin:@localhost:1521/orcl(错误:Oracle 用冒号:分隔端口和服务名,不是斜杠/);
  • 服务名orcl错误,实际为XE(Oracle XE 默认)或ORCLCDB(Oracle 12c+ CDB);
  • 防火墙阻止 1521 端口,或 Oracle 监听器未启动(lsnrctl status检查)。
    解决:
  • 在 Oracle 服务器执行SELECT value FROM v$parameter WHERE name = 'service_names';获取真实服务名;
  • 用 Oracle 自带sqlplus测试:sqlplus airticket/123456@localhost:1521/orcl,成功则 URL 正确。

4.4 现象:订票成功,但FLIGHTS.AVAILABLE_SEATS变为负数

原因:如 3.2 节所述,当前实现无并发控制。两个线程同时读到AVAILABLE_SEATS=1,各自执行-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 '1'='1,密码任意。若返回所有用户,则存在注入;若查询无结果,则预编译生效。这是每个 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。不是为了炫技,而是让每个“订票成功”的弹窗背后,都有可验证、可回溯、可兜底的确定性。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询