简介:这份资源是面向高校软件工程课程设计场景的Java民航订票管理系统完整源码包,适合正在做课设、需要参考完整业务闭环与数据库设计的计算机专业学生,也可作为Swing桌面应用与JDBC持久层练手项目。系统覆盖航班信息查询、客户订票与退票、航班与航线管理、航班延误处理、已订票客户信息管理及会员信息管理等核心模块,业务链条较为完整。压缩包共120个文件,约2.29MB,其中30个java源文件承载各功能模块与DAO实现,75个class为编译产物,另有2个sql脚本用于建库建表、5个xml与若干工程配置文件支撑Eclipse或IDEA导入,并附docx说明文档。目前已有890人学习下载。读者可据此快速理清订票系统的实体关系、界面分层与数据库表结构,对照源码理解登录、查询、下单、退票等流程的实现思路,也可在原有基础上扩展支付、改签等功能,作为课设答辩与二次开发的参考底稿。
1. 从一份能跑起来的 Java Swing 订票系统说起
很多软件工程课程设计的选题里,飞机订票系统几乎是出现频率最高的一个,但真正拿到手能直接跑通、数据库脚本齐全、功能覆盖到退票和延误管理的源码包并不多。这份 JAVA 民航订票管理系统源码,配套 SQL Server 数据库脚本,用 Swing 做界面、JDBC 做数据访问,覆盖了航班信息查询、客户订票、退票、航班信息管理、航线管理、航班延误管理、已订票客户信息管理、会员信息管理这几条主线。从类名就能看出分层意图:BookTicket、FlightQuery、Login负责交互入口,FlightInformationDao、CustomersBTInformationDao负责数据落地,AirLineInformation、VIPRegister把航线和会员单独抽出来。适合两类人:一是课程设计需要一份结构完整、能讲清楚分层逻辑的参考实现;二是刚接触 JDBC + Swing 组合、想找一个业务闭环完整的小系统练手的人。下面按「资源是什么 → 怎么跑起来 → 坑在哪 → 怎么改」的顺序拆开讲。
2. 环境搭建与数据库初始化:把 SQL Server 和 JDBC 先接通
2.1 开发工具与运行环境选型
这份源码标注的开发工具是 Eclipse 和 IntelliJ IDEA,数据库用 SQL Server,界面库是 Java Swing,数据访问走原生 JDBC。选型上没什么花哨的东西,但恰恰是这种「老三样」组合,在课程设计场景里最稳:Swing 不需要额外依赖,JDK 自带;JDBC 驱动只要把 jar 包丢进 classpath 就能用;SQL Server 在 Windows 上装起来比 MySQL 少一层配置心智负担。常见做法是 JDK 8 或 JDK 11,再高的版本 Swing 本身没问题,但部分老驱动 jar 在高版本 JDK 上会有反射警告,能跑但日志不干净。
需要提前准备的东西:JDK(建议 8 或 11)、Eclipse 或 IDEA、SQL Server(Express 版就够)、对应的 JDBC 驱动 jar(mssql-jdbc或老版本的sqljdbc4.jar)。驱动版本要和 SQL Server 版本对上,SQL Server 2019 之后建议用mssql-jdbc-9.x以上,否则登录阶段容易报 TLS 相关错误。
2.2 建库建表:先跑脚本再连代码
数据库脚本一般随源码包一起给,导入顺序不能乱。先建库,再建表,最后插初始数据。如果脚本里没有显式CREATE DATABASE,需要手动建一个库名,再把表建进去。下面是一段典型的建库建表语句,字段名按源码里 DAO 的映射来对齐:
-- 创建数据库 CREATE DATABASE FlightTicketDB; GO USE FlightTicketDB; GO -- 航班信息表 CREATE TABLE FlightInformation ( FlightNo VARCHAR(20) PRIMARY KEY, -- 航班号 AirLineName VARCHAR(50), -- 所属航线 StartCity VARCHAR(30), -- 出发城市 EndCity VARCHAR(30), -- 到达城市 StartTime DATETIME, -- 起飞时间 EndTime DATETIME, -- 到达时间 Price DECIMAL(10,2), -- 票价 SeatCount INT, -- 剩余座位 FlightState VARCHAR(20) -- 航班状态:正常/延误/取消 ); GO -- 客户订票信息表 CREATE TABLE BookTicketOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, CustomerName VARCHAR(30), IdCard VARCHAR(20), FlightNo VARCHAR(20), BookTime DATETIME, SeatNo VARCHAR(10), OrderState VARCHAR(20) -- 已订/已退 ); GO建表时最容易忽略的是字段类型和 DAO 里ResultSet.getXxx()的对应关系。比如Price用DECIMAL,DAO 里就得用getBigDecimal或getDouble,用getInt会直接抛异常。FlightState这种状态字段建议用VARCHAR而不是BIT,因为延误、取消、正常是三态,用布尔存不下。
2.3 配置 JDBC 连接:URL、账号、驱动三件事
连接配置通常写在某个工具类里,比如DBUtil或直接在 DAO 的构造函数里。核心就是四要素:驱动类名、URL、用户名、密码。SQL Server 的 URL 写法有坑,实例名和端口二选一,混着写会连不上。
public class DBUtil { // SQL Server 驱动类名,老版本是 com.microsoft.sqlserver.jdbc.SQLServerDriver private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; // 若为默认实例,用 1433 端口;命名实例则用 instanceName private static final String URL = "jdbc:sqlserver://127.0.0.1:1433;databaseName=FlightTicketDB;encrypt=false"; private static final String USER = "sa"; private static final String PASSWORD = "your_password"; public static Connection getConnection() throws Exception { Class.forName(DRIVER); // 加载驱动 return DriverManager.getConnection(URL, USER, PASSWORD); } }encrypt=false这一项在新版驱动里很关键,SQL Server 默认开启加密协商,本地开发环境没配证书时会直接握手失败,报「PKIX path building failed」或者「The driver could not establish a secure connection」。加上这一项本地能通,但生产环境不能这么写,课程设计阶段够用。databaseName必须和实际建的库名一致,大小写不敏感但拼错就是「Cannot open database」错误。
2.4 导入项目并跑通第一个界面
把源码导入 IDE 后,先确认三件事:JDBC 驱动 jar 是否加进了 Build Path 或 Maven 依赖;DBUtil里的账号密码是否改成自己本地的;数据库服务是否已启动。然后从Login.class对应的源文件启动,登录界面能弹出来,说明 Swing 部分没问题;输入账号能进主界面,说明 JDBC 通了。如果登录按钮点了没反应,先看控制台有没有异常堆栈,八成是驱动没加载或 URL 写错。
3. 核心业务模块拆解:订票、退票、延误管理怎么串起来
3.1 航班查询与订票主流程
FlightQuery负责查询,BookTicket负责下单,两者通过FlightInformationDao共享数据。查询一般支持按出发城市、到达城市、日期三个条件组合,SQL 用动态拼接或者PreparedStatement占位。订票流程是:查到航班 → 选座位 → 写订单 → 扣减座位数。扣座位这一步必须和写订单在同一个事务里,否则会出现「订单写了但座位没扣」或者「座位扣了订单没写」的脏数据。
// 订票核心逻辑:事务包裹写订单 + 扣座位 public boolean bookTicket(BookTicketOrder order) { Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psSeat = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 写入订单 String sqlOrder = "INSERT INTO BookTicketOrder " + "(CustomerName, IdCard, FlightNo, BookTime, SeatNo, OrderState) " + "VALUES (?, ?, ?, GETDATE(), ?, '已订')"; psOrder = conn.prepareStatement(sqlOrder); psOrder.setString(1, order.getCustomerName()); psOrder.setString(2, order.getIdCard()); psOrder.setString(3, order.getFlightNo()); psOrder.setString(4, order.getSeatNo()); psOrder.executeUpdate(); // 2. 扣减剩余座位,同时校验不能超卖 String sqlSeat = "UPDATE FlightInformation SET SeatCount = SeatCount - 1 " + "WHERE FlightNo = ? AND SeatCount > 0"; psSeat = conn.prepareStatement(sqlSeat); psSeat.setString(1, order.getFlightNo()); int rows = psSeat.executeUpdate(); if (rows == 0) { conn.rollback(); // 座位不足,回滚订单 return false; } conn.commit(); // 两步都成功才提交 return true; } catch (Exception e) { try { if (conn != null) conn.rollback(); } catch (Exception ignored) {} e.printStackTrace(); return false; } finally { // 关闭资源,顺序:Statement -> Connection try { if (psOrder != null) psOrder.close(); } catch (Exception ignored) {} try { if (psSeat != null) psSeat.close(); } catch (Exception ignored) {} try { if (conn != null) conn.close(); } catch (Exception ignored) {} } }setAutoCommit(false)是事务的开关,忘了写这一步,两条 SQL 各自提交,超卖就防不住。WHERE SeatCount > 0这个条件配合executeUpdate返回的影响行数,是防超卖的经典写法:返回 0 说明没扣到,直接回滚。GETDATE()用数据库时间而不是 Java 的new Date(),避免客户端时区不一致导致订单时间错乱。
3.2 退票与座位回补
退票逻辑和订票相反:把订单状态改成「已退」,同时把座位加回去。这里有个容易翻车的地方——退票要判断订单当前状态,已经退过的订单不能再退,否则座位会被重复加回,越退越多。
public boolean refundTicket(int orderId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查询订单状态和航班号 String sqlQuery = "SELECT FlightNo, OrderState FROM BookTicketOrder WHERE OrderId = ?"; PreparedStatement psQuery = conn.prepareStatement(sqlQuery); psQuery.setInt(1, orderId); ResultSet rs = psQuery.executeQuery(); if (!rs.next() || "已退".equals(rs.getString("OrderState"))) { conn.rollback(); return false; // 订单不存在或已退,直接拒绝 } String flightNo = rs.getString("FlightNo"); // 2. 更新订单状态 PreparedStatement psUpdate = conn.prepareStatement( "UPDATE BookTicketOrder SET OrderState = '已退' WHERE OrderId = ?"); psUpdate.setInt(1, orderId); psUpdate.executeUpdate(); // 3. 座位回补 PreparedStatement psSeat = conn.prepareStatement( "UPDATE FlightInformation SET SeatCount = SeatCount + 1 WHERE FlightNo = ?"); psSeat.setString(1, flightNo); psSeat.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { if (conn != null) conn.rollback(); } catch (Exception ignored) {} e.printStackTrace(); return false; } finally { try { if (conn != null) conn.close(); } catch (Exception ignored) {} } }状态判断放在事务里做,而不是先查再开事务,否则并发下两个退票请求可能都读到「已订」,都执行回补。OrderState用中文字符串存,虽然直观,但要注意数据库排序规则和 Java 源文件编码一致,否则比较时可能匹配不上。
3.3 航班延误管理与状态联动
FlightInformation里有个FlightState字段,延误管理就是改这个字段。但延误不是孤立事件,它会影响已订票客户的行程。常见做法是:管理员把某航班标记为「延误」后,系统能按航班号查出所有OrderState = '已订'的客户,供人工通知或批量短信。这部分源码里通常是一个查询 + 一个更新,逻辑不复杂,但字段联动要理清。
| 操作 | 涉及表 | 关键字段 | 注意事项 |
|---|---|---|---|
| 标记延误 | FlightInformation | FlightState | 只改状态,不动座位 |
| 查询受影响客户 | BookTicketOrder | FlightNo, OrderState | 只查「已订」的 |
| 取消航班 | FlightInformation | FlightState | 需批量退票并回补座位 |
| 恢复航班 | FlightInformation | FlightState | 状态改回「正常」 |
取消航班比延误多一步:要把该航班所有已订订单批量退掉,座位统一回补。这一步如果逐条调退票接口,事务边界会碎掉,正确做法是在一个事务里批量更新订单状态 + 一次性回补座位数。
3.4 会员管理与 VIP 注册
VIPRegister和CustomersBTInformationDao负责会员这块。会员信息一般比普通客户多几个字段:会员等级、累计里程、折扣率。订票时根据会员等级算折扣价,这个折扣计算要放在写订单之前,把折后价存进订单表,而不是每次查询时现算——现算的话,会员等级一变,历史订单价格就跟着变了,对账会出问题。
// 根据会员等级计算折后价 public double calcDiscountPrice(double originalPrice, String vipLevel) { double rate; switch (vipLevel) { case "钻石": rate = 0.80; break; case "黄金": rate = 0.90; break; case "白银": rate = 0.95; break; default: rate = 1.00; break; // 普通客户无折扣 } // 保留两位小数,避免浮点误差 return Math.round(originalPrice * rate * 100.0) / 100.0; }Math.round(... * 100.0) / 100.0是保留两位小数的常用写法,直接Math.round会丢精度。折扣率用switch硬编码在代码里,课程设计够用,但真要扩展得挪到配置表或枚举里,否则每加一个等级都要改代码重新编译。
4. 避坑与排查:这份源码跑不起来时先看这几条
4.1 驱动加载失败或连接被拒
现象:启动后点登录,控制台报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver,或者Connection refused。原因通常是 JDBC 驱动 jar 没加进 classpath,或者 SQL Server 的 TCP/IP 协议没启用。解决:确认 jar 在 Build Path 里;打开 SQL Server 配置管理器,把 TCP/IP 协议启用,端口设为 1433,重启服务。命名实例的话 URL 要改成;instanceName=你的实例名,不能再用端口。
4.2 中文乱码
现象:界面显示正常,但数据库里存进去的中文变成问号,或者查出来的中文显示成方块。原因有两个层面:数据库字段排序规则不是中文兼容的(比如SQL_Latin1_General),或者 JDBC URL 没指定编码。解决:建库时选Chinese_PRC_CI_AS排序规则;URL 里加;sendStringParametersAsUnicode=true;Java 源文件编码统一设成 UTF-8。三处都对齐基本就不会乱。
4.3 座位超卖或退票后座位不恢复
现象:两个人同时订最后一张票,都显示成功;或者退票后座位数没变。原因:订票没开事务,或者退票只改了订单状态没回补座位。解决:订票和退票都必须用setAutoCommit(false)包住多条 SQL,扣座位用WHERE SeatCount > 0加影响行数判断,退票先查状态再回补。这是血泪经验,课程设计答辩时老师最爱问这个点。
4.4 Swing 界面卡死
现象:点查询按钮后界面无响应,过几秒才刷新。原因:数据库查询直接在事件分发线程(EDT)里执行,网络或磁盘慢的时候整个界面就冻住。解决:把耗时查询放到SwingWorker的doInBackground里,结果在done里更新界面。课程设计不强制要求,但加上去是个加分项,也避免演示时尴尬。
4.5 日期格式转换异常
现象:插入订单时报Conversion failed when converting date and/or time from character string。原因:Java 的String日期直接拼进 SQL,格式和数据库期望的不一致。解决:统一用PreparedStatement.setTimestamp()传java.sql.Timestamp,或者用SimpleDateFormat转成yyyy-MM-dd HH:mm:ss再传。不要用字符串拼接日期,格式稍有出入就翻车。
5. 二次开发与验证:把课程设计改成能讲出亮点的作品
5.1 用 DAO 分层做一次代码走查
拿到源码后别急着改功能,先做一次分层走查:Login里有没有直接写 SQL?BookTicket有没有绕过 DAO 直接连库?如果界面类里混着 JDBC 代码,说明分层没做干净,二次开发前先把这些 SQL 抽到 DAO 里。走查方法很简单,全局搜DriverManager和PreparedStatement,看它们出现在哪些文件里。理想情况下只应该出现在 DAO 和工具类里,出现在 Swing 面板类里就是坏味道。
5.2 加一个「订单查询」功能验证分层
想验证自己是否理解了这套结构,最直接的办法是加一个按身份证号查订单的功能。步骤:在BookTicketOrder对应的 DAO 里加一个queryOrdersByIdCard(String idCard)方法;在 Swing 里加一个查询面板,输入身份证号,表格展示结果;表格用DefaultTableModel,列名和 DAO 返回的字段对齐。这个功能加完,你就把「界面 → DAO → 数据库」整条链路走了一遍,比看十遍代码都管用。
// DAO 层新增:按身份证号查订单 public List<BookTicketOrder> queryOrdersByIdCard(String idCard) throws Exception { List<BookTicketOrder> list = new ArrayList<>(); String sql = "SELECT OrderId, CustomerName, FlightNo, BookTime, SeatNo, OrderState " + "FROM BookTicketOrder WHERE IdCard = ? ORDER BY BookTime DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, idCard); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { BookTicketOrder o = new BookTicketOrder(); o.setOrderId(rs.getInt("OrderId")); o.setCustomerName(rs.getString("CustomerName")); o.setFlightNo(rs.getString("FlightNo")); o.setBookTime(rs.getTimestamp("BookTime")); o.setSeatNo(rs.getString("SeatNo")); o.setOrderState(rs.getString("OrderState")); list.add(o); } } } return list; }try-with-resources写法比手动finally关资源干净,JDK 7 以上都支持。ORDER BY BookTime DESC让最近的订单排前面,符合使用直觉。返回List而不是ResultSet,是为了让 DAO 之外的地方不依赖 JDBC 类型,这是分层的基本要求。
5.3 验证清单:答辩前自己先过一遍
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 登录 | 输入正确/错误账号 | 正确进主界面,错误提示 |
| 查询 | 按城市+日期查 | 返回匹配航班 |
| 订票 | 订一张有余票的 | 订单生成,座位减一 |
| 超卖 | 订最后一张后并发再订 | 第二单失败 |
| 退票 | 退已订订单 | 状态变已退,座位加一 |
| 重复退票 | 对已退订单再退 | 拒绝,座位不变 |
| 延误 | 标记航班延误 | 状态更新,可查受影响客户 |
| 会员折扣 | 不同等级订票 | 价格按折扣计算 |
这张表建议答辩前逐项跑一遍,尤其是超卖和重复退票这两项,是区分「能跑」和「跑对」的分水岭。从那以后我每次拿到这类课程设计源码,都先把事务边界和状态判断这两处翻出来看一遍,再决定要不要在它基础上改——这两处干净,后面加功能就顺;这两处糊弄,加什么都是在沙子上盖楼。希望帮到你。
本文还有配套的精品资源,点击获取