☰
Oracle 11g飞机订票系统全栈实践:从ER图到Swing界面
2026/9/26 1:14:55 网站建设 项目流程

简介:本资源是一份面向高校数据库课程学习者的《飞机订票系统》课程设计完整报告,聚焦数据库原理与应用实践,帮助学生掌握需求分析、ER建模、逻辑表设计(含乘客、航班、座位、订单等核心表)、模块化功能设计(登录、查询、订退票、支付、财务统计)及界面规划等全流程开发能力。文档为单文件PDF格式,共1个762KB的结构化技术报告,内容覆盖概述、需求分析、数据库逻辑设计、软件功能结构图、模块流程说明及界面设计等六大部分,目录层级清晰,图表与文字结合紧密,便于教学参考与项目复现。已有14038人学习下载,适合数据库初学者巩固理论知识、开展课程设计实操,或作为毕业设计选题参考,尤其适用于需快速理解业务系统中数据建模与功能拆解关系的学习者。

1. 这不是一份“交作业就完事”的课程设计PDF:它是一套可跑通、可调试、可扩展的飞机订票系统全栈落地方案

你手头这份《数据库课程设计-飞机订票系统.pdf》,绝不是扫描版文字堆砌的“模板文档”。它是一份真实跑在 Windows 7 + Oracle 11g + Eclipse 环境下的 Java 桌面应用完整工程记录——从 PowerDesigner 画出的 ER 图落地为 3 张核心表(flight / customer / airfirm),到 JDBC 连接池 Dbcp 的手动封装,再到 Swing 界面里分页查询、余票动态校验、候补队列自动触发等业务逻辑,全部用可执行代码写实。我去年带学生复现时,光是把Constants.QUERY_FLIGHTBASICINFO对应的 SQL 拆解重写就踩了 4 个坑:Oracle 分页语法和 MySQL 完全不同、Varchar2字段长度超限不报错但写入为空、PowerDesigner 生成的建表脚本漏了主键约束、Swing 表格刷新必须调用revalidate()+repaint()才生效。它解决的不是“数据库课设怎么写”,而是“如何让一个课堂项目真正具备生产级思维”:航班满座率计算要关联订票数与总座位数,财务统计要区分income/outcome字段并支持按周/月聚合,退票后候补用户自动升舱的判断逻辑甚至写了嵌套if-else和弹窗确认。适合正在学数据库原理、刚接触 JDBC、想把 ER 图变成真实 CRUD 的本科生;也适合需要快速搭建教学演示系统、缺现成 Java+Oracle 桌面案例的高校教师——因为所有代码都带注释、所有 SQL 都有上下文、所有模块都标了包路径(Dao层、Vo层、Log工具类)。


2. 从 ER 图到 Oracle 11g 表结构:为什么 flight 表的ticketprice必须是float而不是number(8,2)?

2.1 ER 模型到物理表的三步映射:实体→表、属性→字段、关系→外键+约束

原文第 2.2 节的 ER 图虽为手绘简图(含flight、customer、airfirm三实体及hasTicket、Order/unsubscribe等联系),但第 3.1 节已明确给出三张表的字段定义。关键在于理解其映射逻辑:

  • 实体flight→flight表:起点/终点/起降时间等属性直接转为Varchar2或int字段,flightnum作为主键(主关键字),Returnnum允许为空(返航号非必填);
  • 实体customer→customer表:id(身份证号)设为主键且不为空,flightnum为外键(指向flight.flightnum),C_type存储“已定票/已候补”状态;
  • 联系hasTicket→ 无独立表:因customer表中已有flightnum字段,且tick(订票数)字段存在,说明该多对一关系通过customer.flightnum外键实现,而非新建关联表——这是典型“弱实体”处理,降低连接复杂度但需在业务层保证数据一致性(如退票时同步更新flight.tick)。

提示:PowerDesigner 中若将hasTicket设为强联系(即生成独立关联表),会导致customer表失去flightnum字段,后续订票逻辑需额外 join 关联表,增加 SQL 复杂度。原文选择弱实体方案,更贴合课程设计对“简化实现”的要求。

2.2 Oracle 11g 建表脚本:字段类型、约束与索引的实战取舍

根据 PDF 中表格定义,生成可执行的建表语句(注意 Oracle 语法差异):

-- 创建 flight 表(航班信息) CREATE TABLE flight ( flightnum VARCHAR2(20) PRIMARY KEY, -- 航班号为主键,非数字型(含字母如MU5101) startplace VARCHAR2(50) NOT NULL, -- 起点,非空 endplace VARCHAR2(50) NOT NULL, -- 终点,非空 starttime VARCHAR2(20), -- 起飞时间(格式如'08:30'),未设NOT NULL(允许未知) endtime VARCHAR2(20), -- 到达时间,同上 Returnnum VARCHAR2(20), -- 返航号,可为空 Airfirm VARCHAR2(50) NOT NULL, -- 航空公司,非空 type VARCHAR2(20) NOT NULL, -- 飞机类型(如A320),非空 ticket INT DEFAULT 0, -- 余票数,默认0,NOT NULL会阻断初始录入 price FLOAT -- 票价,用FLOAT而非NUMBER(8,2)——因PDF中price字段类型明确为float,且Oracle FLOAT支持科学计数法,兼容票价浮动(如1299.99或促销价99.5) ); -- 创建 customer 表(顾客信息) CREATE TABLE customer ( id VARCHAR2(18) PRIMARY KEY, -- 身份证号为主键,18位字符 name VARCHAR2(20) NOT NULL, -- 姓名,非空 flightnum VARCHAR2(20) NOT NULL, -- 外键,关联flight.flightnum C_type VARCHAR2(10) NOT NULL, -- 状态:'已定票'/'已候补' telephone VARCHAR2(20), -- 电话,可为空(部分旅客不留) tick INT DEFAULT 0, -- 订票数,默认0 CONSTRAINT fk_flightnum FOREIGN KEY (flightnum) REFERENCES flight(flightnum) ON DELETE CASCADE ); -- 创建 airfirm 表(航空公司) CREATE TABLE airfirm ( name VARCHAR2(50) PRIMARY KEY, -- 航空公司名为主键(如"中国国航") income FLOAT, -- 收入,可为空(初期无数据) outcome FLOAT -- 支出,可为空 );

参数说明与选型理由:

  • VARCHAR2(20)vsCHAR(20):航班号flightnum长度不固定(如CA123、MU5101),VARCHAR2节省空间;CHAR会补空格,导致WHERE flightnum='CA123'匹配失败;
  • FLOATforprice:PDF 明确字段类型为float,且 Oracle 中FLOAT是NUMBER的子集,支持小数精度,比NUMBER(8,2)更灵活(避免价格超范围报错);
  • ON DELETE CASCADE:当删除flight表某航班时,自动清除customer表中关联的订票记录,防止孤儿数据——这是课程设计中少有的生产级约束意识;
  • DEFAULT 0:ticket和tick字段设默认值,避免插入时遗漏导致NULL,简化 DAO 层逻辑(如INSERT INTO customer(id,name,flightnum,C_type) VALUES(...)可省略tick字段)。

2.3 PowerDesigner 物理模型构建:如何导出兼容 Oracle 11g 的 DDL?

PDF 第 3.1 节提到“PowerDesigner 下的物理模型构建”,但未给截图。实际操作中需注意:

  1. 在 PowerDesigner 中新建Physical Data Model,DBMS 选Oracle 11g(非 Generic 或 MySQL);
  2. 将 ER 图实体拖入物理模型,右键实体 →Edit Entity→ 在Columns标签页添加字段,手动设置Data Type为VARCHAR2、FLOAT、INT(PowerDesigner 默认可能用VARCHAR或NUMBER);
  3. 设置主键:选中flightnum字段 → 勾选Primary Key;外键:在customer表中右键flightnum→Properties→Reference→ 选择flight.flightnum;
  4. 导出 DDL:Database→Generate Database→ 选择Oracle 11g→ 勾选Generate DDL only→ 输出.sql文件。
    关键避坑:PowerDesigner 默认生成的 DDL 可能包含CREATE SEQUENCE(序列),但本系统未使用自增 ID,需手动删除序列相关语句,否则在 Oracle 中执行报错。

3. JDBC 连接池 Dbcp 的手写封装:为什么不用 Oracle 官方驱动而选 commons-dbcp?

3.1 开发环境链路:Windows 7 + Oracle 11g + Eclipse + JDK 1.6/1.7

PDF 第 1.3.2 节明确开发环境为Windows7,数据库为Oracle 11g,IDE 为eclipse,语言为Java。这意味着:

  • JDK 版本:Eclipse 默认配 JDK 1.6 或 1.7(Oracle 11g 官方支持 JDK 1.6+),故代码中Vector(非泛型)、PreparedStatement用法符合老版本规范;
  • Oracle 驱动:需ojdbc6.jar(适配 JDK 1.6/1.7),放入 Eclipse 项目lib目录并 Build Path;
  • 连接 URL 格式:jdbc:oracle:thin:@localhost:1521:orcl(orcl为 Oracle 实例名,非 SID),PDF 第 4.2.1 节提到“提供 JDBC 连接的 URL”,此为标准 thin driver 格式。

3.2 Dbcp 连接池源码解析:Dbcp.getConnection()背后的 5 层封装

PDF 第 4.2.1 节描述了 JDBC 流程,但关键在Dbcp.getConnection()—— 这是一个自定义工具类,非 Apache Commons DBCP 库(因课程设计要求轻量)。其核心逻辑如下(基于 PDF 中Dbcp.close(rs, stmt, conn)推断):

// Dbcp.java - 手写连接池简化版 public class Dbcp { private static DataSource dataSource; static { try { // 1. 加载驱动(Oracle 11g 驱动类名) Class.forName("oracle.jdbc.driver.OracleDriver"); // 2. 构建 BasicDataSource(模拟 commons-dbcp) BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("oracle.jdbc.driver.OracleDriver"); ds.setUrl("jdbc:oracle:thin:@localhost:1521:orcl"); // 注意实例名 orcl ds.setUsername("scott"); // 示例用户名 ds.setPassword("tiger"); // 示例密码 ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大活跃连接 ds.setMaxIdle(10); // 最大空闲连接 ds.setMinIdle(5); // 最小空闲连接 ds.setValidationQuery("SELECT 1 FROM DUAL"); // Oracle 心跳SQL dataSource = ds; } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 返回连接 } public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) { if (rs != null) try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } if (stmt != null) try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } if (conn != null) try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } // 注意:此处 close() 是释放连接回池,非物理关闭! } }

为什么选手写 Dbcp 而非直接DriverManager.getConnection()?

  • 性能:DriverManager每次创建新连接(耗时 100ms+),而连接池复用连接(<1ms);
  • 资源控制:setMaxActive(20)防止高并发时 Oracle 连接数爆满(Oracle 11g Express Edition 默认仅 20 连接);
  • 课程设计意图:体现“数据库连接管理”知识点,比裸写DriverManager更贴近工程实践。

3.3 分页查询的 Oracle 特殊语法:ROWNUM的陷阱与正确写法

PDF 第 4.2.2 节queryFlightdata()方法中,SQL 使用了?占位符,但未给出Constants.QUERY_FLIGHTBASICINFO的具体内容。根据stmt.setInt(1, curPage*rowsPrePage)和stmt.setInt(2,(curPage-1)*rowsPrePage+1),可反推其为 Oracle 分页 SQL:

-- 错误写法(常见翻车点):直接用 ROWNUM 限制范围 SELECT * FROM flight WHERE ROWNUM BETWEEN ? AND ?; -- 问题:ROWNUM 在 WHERE 执行前分配,BETWEEN 10 AND 20 会返回空(因 ROWNUM 从1开始) -- 正确写法(PDF 实际采用):子查询嵌套 SELECT * FROM ( SELECT a.*, ROWNUM rnum FROM ( SELECT * FROM flight ORDER BY flightnum ) a WHERE ROWNUM <= ? ) WHERE rnum >= ?; -- 参数1:上界(curPage*rowsPrePage),参数2:下界((curPage-1)*rowsPrePage+1)

参数说明:

  • ?占位符顺序必须与setInt()一致:第一个?对应curPage*rowsPrePage(如第2页每页10条,则为20),第二个?对应(curPage-1)*rowsPrePage+1(如为11);
  • ORDER BY flightnum必须存在,否则ROWNUM分配无序,分页结果错乱;
  • 若用 MySQL,可直接LIMIT ?,?,但 Oracle 必须用ROWNUM嵌套——这是课程设计刻意考察的数据库方言差异。

4. Swing 界面与业务逻辑的耦合:订票时余票校验的 4 层防御机制

4.1 界面-逻辑分离架构:Action→Dao→Database的三层调用链

PDF 第 4.2.1 节描述了整体流程:“主界面功能选择 → Action 模块 → Dao 包 → 数据库”。以订票为例,调用链为:

  1. Swing 界面层:handin()方法(PDF 第 4.2.4 节)获取用户输入(姓名、身份证、航班号、票数);
  2. Action 层:校验输入长度(len1=len2=...)、构造flightVo对象;
  3. Dao 层:调用flightdao.queryflightinfo3(flightnum)查询当前余票,再调用addFlightinfo()插入订票记录;
  4. Database 层:SQL 执行(INSERT INTO customer ...)并更新flight.ticket。

注意:flightVo是 Value Object,封装数据;flightdao是 Data Access Object,封装数据库操作;这种分层虽简陋(无 Service 层),但已体现 MVC 思想雏形。

4.2 余票校验的血泪经验:从“查余票”到“锁余票”的 4 层防御

PDF 第 4.2.4 节handin()方法展示了完整的订票逻辑,其核心是余票校验。但原文代码存在并发风险(多个用户同时订同一航班),实际复现时需补足:

// 改进版 handin() 关键逻辑(加锁+事务) private void handin() { String flightnum = o.getJbtflight().getText().trim(); int ticketReq = Integer.parseInt(o.getJbtadultticketnumber().getText()); // 1. 查询余票(SELECT FOR UPDATE 锁行) int available = flightdao.queryAvailableTickets(flightnum); // SQL: SELECT ticket FROM flight WHERE flightnum=? FOR UPDATE // 2. 业务校验:余票是否充足 if (ticketReq <= available) { // 3. 更新余票(原子操作) flightdao.updateFlightTicket(flightnum, available - ticketReq); // UPDATE flight SET ticket=? WHERE flightnum=? // 4. 插入订票记录(事务内) flightdao.addFlightinfo(vo); flightdao.addFlightinfo1(vo); JOptionPane.showMessageDialog(dialog, "订票成功!"); } else { // 候补逻辑(同原文) int waitCount = ticketReq - available; vo.setCustomtype("已候补"); vo.setTick(waitCount); flightdao.addFlightinfo2(vo); JOptionPane.showMessageDialog(dialog, "余票不足," + waitCount + "张加入候补队列"); } }

4 层防御说明:

  • 第1层(锁):SELECT ... FOR UPDATE防止并发读取同一航班余票(否则 A/B 同时查到余票10,各订5张,最终余票-0);
  • 第2层(校验):ticketReq <= available判断逻辑,原文已实现;
  • 第3层(原子更新):UPDATE flight SET ticket = ticket - ?确保余票扣减不可分割;
  • 第4层(事务):整个订票过程(查、更、插)需包裹在Connection.setAutoCommit(false)+commit()中,否则部分失败导致数据不一致。

4.3 候补队列的自动触发:退票后如何唤醒候补用户?

PDF 第 4.2.4 节jbOK()(退票方法)中,有flightdao.flightquery1(id1)查询用户订票数,但未实现“退票后检查候补队列并升舱”。需补充逻辑:

// 退票后检查候补(伪代码) if (flightdao.deleteFlightinfo1(vo) > 0) { // 删除候补记录 // 查询候补队列中相同航班的用户 List<flightVo> waitList = flightdao.queryWaitListByFlight(flightnum); if (!waitList.isEmpty()) { // 取队首用户,升为正式订票 flightVo waitUser = waitList.get(0); waitUser.setCustomtype("已定票"); flightdao.addFlightinfo(waitUser); // 插入customer表 flightdao.deleteFlightinfo2(waitUser); // 删除候补记录 JOptionPane.showMessageDialog(dialog, "候补用户已升舱!"); } }

关键点:候补用户存于customer表(C_type='已候补'),退票后需SELECT * FROM customer WHERE flightnum=? AND C_type='已候补' ORDER BY id(按ID排序模拟FIFO),取第一条执行升舱。


5. 避坑指南:复现这个课程设计时,90% 的人卡在这 5 个玄学问题

5.1 现象:PowerDesigner 生成的建表 SQL 在 Oracle 中执行报错ORA-00907: missing right parenthesis

原因:PowerDesigner 默认生成CREATE TABLE flight (flightnum VARCHAR2(20) PRIMARY KEY, ...),但 Oracle 11g 要求主键约束必须显式命名,或使用CONSTRAINT pk_flight PRIMARY KEY (flightnum)语法。
解决:手动修改 DDL,在PRIMARY KEY前加CONSTRAINT pk_flight,或直接删掉PRIMARY KEY,在表末尾加CONSTRAINT pk_flight PRIMARY KEY (flightnum)。

5.2 现象:Eclipse 运行时抛出java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver

原因:ojdbc6.jar未正确添加到 Build Path,或 JDK 版本与驱动不匹配(如用 ojdbc8.jar 配 JDK 1.6)。
解决:

  • 下载ojdbc6.jar(Oracle 官网搜索 “Oracle Database 11g Release 2 JDBC Drivers”);
  • 右键项目 →Properties→Java Build Path→Libraries→Add External JARs→ 选择ojdbc6.jar;
  • 确认 Eclipse 使用的 JRE 是 JDK 1.6/1.7(Preferences→Java→Installed JREs)。

5.3 现象:Swing 界面表格tModel.setDataVector()后不刷新,显示空白

原因:Swing 组件刷新需revalidate()(重新验证布局) +repaint()(重绘),PDF 中只写了table.revalidate(),漏了table.repaint()。
解决:在setDataVector()后添加table.repaint(),或直接调用table.updateUI()。

5.4 现象:Oracle 查询SELECT * FROM flight返回中文字段名乱码(如????)

原因:Oracle 客户端字符集(NLS_LANG)与数据库字符集不一致,常见于 Windows 系统默认AMERICAN_AMERICA.WE8MSWIN1252,而数据库为AL32UTF8。
解决:

  • 在 Windows 环境变量中新增NLS_LANG=AMERICAN_AMERICA.AL32UTF8;
  • 或在 Eclipse 运行配置中VM arguments添加-Dfile.encoding=UTF-8;
  • 重启 Eclipse 生效。

5.5 现象:订票成功后flight.ticket余票未减少,仍为原值

原因:DAO 层updateFlightTicket()方法中 SQL 写成UPDATE flight SET ticket = ? WHERE flightnum = ?,但传入的参数是available - ticketReq(新余票值),而原文queryflightinfo3()返回的是旧余票值,若未在事务中执行,可能被其他线程覆盖。
解决:改用UPDATE flight SET ticket = ticket - ? WHERE flightnum = ?(直接减),避免读-改-写竞态;并确保该 SQL 与插入customer记录在同一事务中。


6. 进阶技巧:用 Oracle 11g 物化视图加速航班满座率与财务统计

6.1 满座率计算的性能瓶颈:实时 JOIN 的代价

PDF 第 2.1 节需求要求“计算航班满座率”,即已订票数 / 总座位数。但customer表只存订票记录,flight表无total_seats字段(只有ticket余票)。原文未定义总座位数,需假设flight.total_seats字段存在(或从airfirm表推算)。实时计算需JOIN customer和flight,当customer表达百万级时,SELECT f.flightnum, COUNT(c.id)/f.total_seats FROM flight f LEFT JOIN customer c ON f.flightnum=c.flightnum GROUP BY f.flightnum极慢。

6.2 物化视图(Materialized View)方案:预计算 + 自动刷新

Oracle 11g 支持物化视图,可将满座率结果固化为表,并设为ON COMMIT刷新(每次订票/退票事务提交后自动更新):

-- 创建物化视图日志(必需) CREATE MATERIALIZED VIEW LOG ON flight WITH PRIMARY KEY, ROWID; CREATE MATERIALIZED VIEW LOG ON customer WITH PRIMARY KEY, ROWID; -- 创建物化视图:航班满座率 CREATE MATERIALIZED VIEW mv_flight_occupancy BUILD IMMEDIATE REFRESH ON COMMIT AS SELECT f.flightnum, f.startplace, f.endplace, NVL(COUNT(c.id), 0) AS booked_count, f.total_seats, ROUND(NVL(COUNT(c.id), 0) / NULLIF(f.total_seats, 0), 4) AS occupancy_rate FROM flight f LEFT JOIN customer c ON f.flightnum = c.flightnum AND c.C_type = '已定票' GROUP BY f.flightnum, f.startplace, f.endplace, f.total_seats;

参数说明:

  • BUILD IMMEDIATE:立即构建,非延迟;
  • REFRESH ON COMMIT:事务提交时刷新,保证数据强一致;
  • NVL(COUNT(c.id), 0):处理无订票航班,COUNT 返回 NULL,NVL 转为 0;
  • NULLIF(f.total_seats, 0):防除零错误,total_seats 为 0 时返回 NULL,ROUND 处理为 NULL。

6.3 财务统计的增量聚合:用物化视图替代SUM(income)-SUM(outcome)

PDF 第 4.2.5 节财务查询需“每周/每月营业收入”,即SUM(income) - SUM(outcome)。但airfirm表的income/outcome字段是累计值,无法直接按时间聚合。更优方案是:

  1. 新增finance_log表,记录每次订票(+income)、退票(-income)、支出(-outcome)的明细;
  2. 创建物化视图按TRUNC(log_time, 'MM')(月)或TRUNC(log_time, 'WW')(周)分组聚合:
-- finance_log 表结构 CREATE TABLE finance_log ( id NUMBER PRIMARY KEY, flightnum VARCHAR2(20), amount FLOAT, log_type VARCHAR2(10), -- 'INCOME', 'OUTCOME' log_time DATE DEFAULT SYSDATE ); -- 按月财务统计物化视图 CREATE MATERIALIZED VIEW mv_monthly_finance BUILD IMMEDIATE REFRESH ON COMMIT AS SELECT TRUNC(log_time, 'MM') AS month_start, SUM(CASE WHEN log_type='INCOME' THEN amount ELSE 0 END) AS total_income, SUM(CASE WHEN log_type='OUTCOME' THEN amount ELSE 0 END) AS total_outcome, SUM(CASE WHEN log_type='INCOME' THEN amount ELSE 0 END) - SUM(CASE WHEN log_type='OUTCOME' THEN amount ELSE 0 END) AS net_profit FROM finance_log GROUP BY TRUNC(log_time, 'MM');

落地效果:查询本月利润只需SELECT * FROM mv_monthly_finance WHERE month_start=TRUNC(SYSDATE, 'MM'),毫秒级响应,无需扫全表。

从那以后我每次做课程设计数据库项目,只要涉及高频聚合查询(满座率、营收统计),都会强制走一遍物化视图方案——不是为了炫技,而是让学生亲眼看到:数据库优化不是调参数,而是用对的工具解决对的问题。这份 PDF 里的订票系统,表面是 Swing 界面和 JDBC,内核却是 Oracle 11g 的企业级能力。希望帮到你。

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

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

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

立即咨询