C++与Qt实战:飞机订票系统开发全流程解析
2026/7/26 5:44:08 网站建设 项目流程

1. 项目概述与核心价值

最近在整理过往项目时,翻到了一个用C++和Qt框架实现的飞机订票系统,这是2024年7月完成的一个综合练习。虽然市面上成熟的订票系统多如牛毛,但自己动手从零搭建一套,对于深入理解桌面应用开发、数据库交互、以及面向对象设计模式来说,其价值远超使用现成框架。这个项目麻雀虽小,五脏俱全,它模拟了从用户查询航班、选择座位、下单支付到后台管理的完整闭环。如果你正在学习C++,想摆脱控制台的黑白世界,或者对Qt这个强大的跨平台GUI框架感兴趣,希望通过一个完整的项目来串联知识点,那么这个订票系统的实现过程会是一个绝佳的练手材料。它不仅能巩固你的C++核心语法和面向对象思想,更能让你亲身体验如何将一个复杂的业务逻辑,通过清晰的架构设计,转化为稳定、可维护的桌面应用程序。

2. 技术栈选型与架构设计思路

2.1 为什么是C++和Qt?

在开始敲代码之前,明确技术选型的理由至关重要。我选择C++和Qt组合,主要基于以下几点考量:

C++的核心优势:飞机订票系统涉及大量的数据操作,如航班信息、用户订单、座位状态等,这些数据在内存中的处理效率直接影响到应用的响应速度。C++以其接近硬件的执行效率和精细的内存控制能力著称。通过合理使用STL容器(如std::vector,std::map)和智能指针,我们可以在保证性能的同时,有效管理业务对象的生命周期,避免内存泄漏。此外,系统的核心业务逻辑,如票价计算规则、座位分配算法等,用C++实现也更为得心应手。

Qt框架的不可替代性:Qt不仅仅是一个GUI库。它是一个完整的应用程序框架,其信号与槽(Signals & Slots)机制完美解决了C++中对象间通信的难题,让界面与后台逻辑的解耦变得异常优雅。对于订票系统这种交互复杂的应用,我们需要大量的按钮点击、数据更新、界面跳转等事件响应,信号与槽机制让代码组织清晰,避免了传统回调函数带来的混乱。其次,Qt提供了极其丰富的UI控件(QTableWidget显示航班列表、QCalendarWidget选择日期)、数据库模块(Qt SQL)以及跨平台支持,这让我们能将精力集中在业务实现上,而非底层细节。最后,Qt Designer工具可以快速进行界面原型设计,通过.ui文件描述界面,再与C++代码结合,极大地提升了开发效率。

2.2 系统整体架构设计

一个清晰的架构是项目成功的基石。我采用了典型的三层架构思想,但在桌面应用中,其表现形式略有不同:

  1. 表示层(UI Layer):由Qt的窗口(QMainWindow)、对话框(QDialog)和各类控件构成。这一层只负责接收用户输入和展示数据,不包含任何业务逻辑。例如,查询航班界面只负责收集出发地、目的地和日期,然后将这些参数传递给下一层。

  2. 业务逻辑层(Business Logic Layer):这是系统的核心。我将其设计为一系列C++类,例如FlightManager(航班管理)、OrderManager(订单管理)、UserManager(用户管理)。这些类封装了所有的业务规则和算法,如检查座位余量、计算含税票价、验证用户支付等。它们完全独立于Qt的UI模块,这意味着未来如果需要将系统移植为Web服务或移动应用,这部分核心代码可以最大程度地复用。

  3. 数据访问层(Data Access Layer):负责与数据库进行所有交互。我创建了DatabaseManager单例类,统一管理数据库连接(QSqlDatabase)。并为每个主要的业务实体(如Flight, Order, User)设计了对应的数据访问对象(DAO),例如FlightDAOOrderDAO。这些DAO类提供标准的CRUD(增删改查)接口,业务逻辑层通过调用这些接口来持久化数据,从而将业务逻辑与特定的数据库技术(如SQLite)隔离开。

注意:在项目初期就明确禁止UI线程进行任何耗时的数据库操作或复杂计算。所有可能阻塞界面的任务,都应放入单独的线程(使用Qt的QThread或QtConcurrent)中处理,否则会导致界面卡死,用户体验极差。这是桌面应用开发的一个关键原则。

3. 核心模块实现与关键技术点

3.1 数据库设计与Qt SQL集成

数据是系统的血液。我选择了轻量级的SQLite作为本地数据库,它无需单独部署服务器,非常适合桌面应用。

数据表设计要点

  • 航班表 (flights):包含航班号、起降机场、时间、机型、总座位数、剩余座位数、基础票价等字段。这里“剩余座位数”是一个需要高频更新和并发控制的敏感字段。
  • 订单表 (orders):关联用户ID和航班ID,包含订单号、座位号、订单状态(待支付、已出票、已取消)、支付金额、创建时间等。订单号我采用了“日期+随机数”的生成方式以确保唯一性。
  • 用户表 (users):存储用户名、密码哈希值(绝对不要明文存储)、联系方式等。
  • 座位表 (seats):这是一个关键设计。并非简单地在航班表中存一个剩余数字,而是为每个航班的每个座位(如1A,10C)建立一条记录,关联航班ID,并有一个is_booked字段标识是否已被预订。这样能精准管理每一个座位,支持用户选座。

Qt SQL操作实战: 在DatabaseManager的初始化函数中,建立并打开数据库连接:

bool DatabaseManager::init(const QString &path) { m_database = QSqlDatabase::addDatabase("QSQLITE"); m_database.setDatabaseName(path); if (!m_database.open()) { qCritical() << "无法打开数据库:" << m_database.lastError().text(); return false; } // 执行建表SQL脚本 QSqlQuery query; if (!query.exec(createTableSql)) { qCritical() << "建表失败:" << query.lastError().text(); return false; } return true; }

执行查询时,务必使用预处理语句(QSqlQuery::prepare)来防止SQL注入攻击,尤其是在处理用户输入(如登录名、搜索关键词)时。

QSqlQuery query; query.prepare("SELECT * FROM flights WHERE departure = ? AND arrival = ? AND date = ?"); query.addBindValue(departureCity); query.addBindValue(arrivalCity); query.addBindValue(date); if (query.exec()) { while (query.next()) { // 解析航班信息 } }

3.2 用户界面与业务逻辑的绑定

这是Qt最擅长的部分。我使用Qt Designer绘制了主界面、查询界面、订单详情界面和管理员界面。.ui文件会被编译成对应的头文件(ui_xxxx.h),在C++窗口类中通过ui对象访问界面控件。

关键交互示例:查询航班列表

  1. 用户在界面上输入条件,点击“查询”按钮。
  2. 按钮的clicked()信号连接到窗口类的一个槽函数onSearchButtonClicked()
  3. 在该槽函数中,从UI控件(如QLineEdit, QDateEdit)获取用户输入。
  4. 调用业务逻辑层的FlightManager::searchFlights(...)方法,传入查询条件。
  5. FlightManager内部调用FlightDAO从数据库获取数据,并返回一个QVector<Flight>对象列表。
  6. 槽函数接收到列表后,将其填充到UI的QTableWidget中,更新显示。

信号与槽的高级用法:除了简单的按钮点击,我还大量使用了自定义信号。例如,当订单状态发生变化时(如支付成功),OrderManager会发射一个orderStatusChanged(int orderId)信号。主界面或订单列表界面可以连接这个信号,一旦收到就自动刷新相关订单的显示,实现了低耦合的实时更新。

3.3 订单与座位的并发控制

这是系统的核心难点。想象一下,多个用户同时预订同一航班的最后一个座位。简单的“先查后改”逻辑会导致超卖。

我采用的解决方案是“数据库事务+行级锁”

  1. 当用户开始选座流程时,系统立即开启一个数据库事务。
  2. 在事务内,使用SELECT ... FOR UPDATE(在SQLite中可用BEGIN IMMEDIATE TRANSACTION配合普通SELECT实现类似效果)查询目标座位的is_booked状态。这条语句会锁定该行记录。
  3. 如果座位未被预订,立即执行UPDATE seats SET is_booked = 1, order_id = ? WHERE seat_id = ?进行预订,并更新航班表的剩余座位数(减1)。
  4. 提交事务。如果过程中有任何错误(如座位已被他人锁定),则回滚事务,并提示用户“座位已被占用,请重新选择”。

这个流程确保了从查询到更新的原子性,有效避免了并发冲突。在Qt中,所有的数据库操作默认在调用线程中执行,因此需要确保整个“查询-更新”流程在一个连续的数据库连接会话中完成。

4. 详细实现步骤与代码剖析

4.1 项目环境搭建与工程配置

首先,你需要安装Qt Creator和Qt库(建议使用Qt 5.15或Qt 6.x的MSVC或MinGW套件)。在Qt Creator中新建一个Qt Widgets Application项目。

.pro项目文件的关键配置

QT += core gui sql # 必须添加sql模块 QT += network # 如果需要模拟网络支付,可以添加network greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 # 如果你的数据库文件需要随程序发布 RESOURCES += \ resources.qrc # 在qrc文件中添加数据库文件、图标等 # 开启更严格的编译警告 QMAKE_CXXFLAGS += -Wall -Wextra

在代码中,包含必要的头文件,如<QApplication>,<QSqlDatabase>,<QSqlQuery>,<QMessageBox>等。

4.2 核心类设计与实现

Flight类(数据模型)

class Flight { public: Flight() = default; Flight(int id, const QString &number, const QString &dep, const QString &arr, ...); // Getter和Setter int id() const { return m_id; } QString flightNumber() const { return m_flightNumber; } // ... 其他属性 // 业务方法 bool isSeatAvailable(const QString &seatNum) const; // 检查座位是否可用 double calculateFinalPrice(double basePrice) const; // 计算含税、保险的最终价格 private: int m_id; QString m_flightNumber; QString m_departure; QString m_arrival; QDateTime m_departureTime; QDateTime m_arrivalTime; QString m_aircraftType; int m_totalSeats; int m_availableSeats; // 关联的座位列表 QVector<Seat> m_seats; };

FlightManager类(业务逻辑)

class FlightManager : public QObject { Q_OBJECT public: static FlightManager* instance(); // 单例模式 QVector<Flight> searchFlights(const QString &dep, const QString &arr, const QDate &date); bool bookFlight(int flightId, const QString &seatNum, int userId, QString &outOrderNo); signals: void flightStatusChanged(int flightId); // 航班状态变化信号 private: FlightManager(QObject *parent = nullptr); QVector<Flight> fetchFlightsFromDB(const QString &dep, const QString &arr, const QDate &date); // ... };

FlightManager作为单例,统一管理所有航班相关的业务操作。bookFlight方法封装了前述的并发订票逻辑,返回布尔值表示成功与否,并通过outOrderNo参数返回生成的订单号。

4.3 主界面与多窗口管理

主窗口(MainWindow)通常包含一个菜单栏、工具栏和中心部件。中心部件可以使用QTabWidget来切换“航班查询”、“我的订单”、“系统管理”等不同功能视图。

多窗口跳转: 不要在每个按钮点击事件里new一个对话框然后exec()。更好的做法是提前创建好这些对话框(或使用懒加载),并管理它们的生命周期。例如,在MainWindow中:

// 在头文件中声明 private: SearchDialog *m_searchDlg = nullptr; OrderDetailDialog *m_orderDetailDlg = nullptr; // 在.cpp文件中懒加载和显示 void MainWindow::on_actionSearch_triggered() { if (!m_searchDlg) { m_searchDlg = new SearchDialog(this); // 指定父对象,主窗口关闭时自动销毁 connect(m_searchDlg, &SearchDialog::flightSelected, this, &MainWindow::onFlightSelected); } m_searchDlg->show(); m_searchDlg->raise(); m_searchDlg->activateWindow(); }

这样避免了反复创建和销毁窗口的开销,也便于在窗口间传递数据(通过信号槽或公有方法)。

5. 开发中的常见陷阱与调试技巧

5.1 数据库连接与线程安全

陷阱1:数据库连接在多线程中被共享。Qt的QSqlDatabase连接默认不能跨线程使用。如果你在子线程中执行数据库操作,必须在那个线程内创建新的数据库连接(使用不同的连接名)。

解决方案:为每个需要数据库的工作线程,在线程的run()函数开始时,创建独立的连接。

void WorkerThread::run() { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "my_thread_connection"); // 指定唯一连接名 db.setDatabaseName("database.db"); if (!db.open()) { emit error(db.lastError().text()); return; } // ... 执行数据库操作 // 线程结束时,连接会自动随线程销毁而关闭 }

陷阱2:忘记检查SQL查询的执行结果。盲目相信query.exec()一定会成功。

解决方案:养成习惯,每次执行后都检查。

QSqlQuery query; if (!query.exec("SELECT ...")) { qDebug() << "Query failed:" << query.lastError().text(); // 进行错误处理,如回滚事务、通知用户 return false; }

5.2 界面卡顿与异步处理

当执行一个耗时的数据库查询(如全表扫描)或复杂的业务计算时,如果直接在按钮点击的槽函数中执行,界面会“冻住”,直到操作完成。

解决方案:使用QtConcurrent或QThread

// 在槽函数中,使用QtConcurrent在后台线程运行耗时函数 void MainWindow::onHeavySearchClicked() { QFutureWatcher<QVector<Flight>> *watcher = new QFutureWatcher<QVector<Flight>>(this); connect(watcher, &QFutureWatcher<QVector<Flight>>::finished, this, [this, watcher]() { QVector<Flight> results = watcher->result(); // 在主线程中更新UI updateFlightTable(results); watcher->deleteLater(); }); // 显示一个等待提示 QFuture<QVector<Flight>> future = QtConcurrent::run(&FlightManager::instance(), &FlightManager::heavySearch, searchCriteria); watcher->setFuture(future); }

同时,在耗时操作开始前,可以禁用相关按钮,并显示一个加载动画或提示,防止用户重复点击。

5.3 内存管理与对象生命周期

陷阱:在槽函数中deleteLater了正在使用的对象。由于Qt的事件循环,deleteLater并不是立即删除,但如果后续代码还在访问该对象,就会导致崩溃。

实操心得:对于有明确父子关系的对象(如对话框、控件),依赖Qt的父子内存管理机制是最省心的。对于那些在多个类间共享的数据模型(如航班列表),使用QSharedPointerstd::shared_ptr进行引用计数管理。在复杂的信号槽连接中,如果接收者可能先于发送者被销毁,记得使用QObject::connect的五参数形式,并指定Qt::ConnectionType,或者及时在接收者的析构函数中断开连接。

5.4 部署与发布

开发完成后,如何将程序打包分发给没有Qt环境的用户?

  1. 使用windeployqt工具(Windows):这是Qt官方提供的部署工具。在Release模式下编译好你的程序后,在命令行进入可执行文件所在目录,执行windeployqt your_app.exe。它会自动扫描并复制程序运行所需的所有Qt动态库(DLL)到该目录。
  2. 处理数据库文件:你的SQLite数据库文件(.db)也需要一并发布。可以将它放在可执行文件同级目录,或者在程序首次运行时,检查如果不存在则从一个内置的资源文件中解压出来。
  3. 创建安装包:使用如Inno Setup、NSIS等工具,将可执行文件、所有DLL、数据库文件、图标等打包成一个专业的安装程序。

踩坑记录:发布后最常见的运行时错误是缺少特定的VC++ Redistributable运行库或某些Qt插件(如图像格式插件qjpeg.dll、数据库插件qsqlite.dll)。windeployqt通常能解决大部分Qt库的问题,但VC++运行库需要用户自行安装,或者在你的安装包中附带并静默安装。务必在干净的虚拟机中测试你的发布包。

6. 功能扩展与优化方向

一个基础的订票系统完成后,你可以考虑以下方向进行深化,这会让你的项目简历更加出彩:

  1. 网络通信与数据同步:将SQLite本地数据库改为连接远程MySQL/PostgreSQL数据库。使用Qt Network模块实现HTTP/WebSocket通信,模拟与服务器端的API交互。这可以让你学习JSON/XML数据解析、网络请求封装、错误重试等知识。
  2. 更复杂的UI/UX:使用Qt Quick(QML)重写前端,实现更炫酷、动画更流畅的现代界面。或者,在现有Qt Widgets基础上,使用样式表(QSS)深度定制界面皮肤,使其看起来不像一个“标准”的桌面程序。
  3. 引入设计模式:观察者模式(信号槽本身就是)、单例模式(Manager类)、工厂模式(创建不同的支付策略)等都可以在项目中找到用武之地。有意识地使用它们,能让代码结构更清晰。
  4. 支付流程模拟:集成一个模拟的支付网关。设计一个“支付状态机”,处理待支付、支付中、支付成功、支付失败、超时取消等状态,并合理更新订单状态。
  5. 日志与监控:集成一个简单的日志系统(如使用qInstallMessageHandler重定向Qt日志,或使用spdlog等库),记录用户操作、系统错误和性能数据,便于后期调试和问题追踪。

实现这个系统的过程,就像在搭建一个精致的微缩城市。从地基(数据库)到骨架(业务逻辑类),再到外观(UI)和交通规则(信号槽),每一步都需要精心设计。它带给你的,绝不仅仅是几行C++和Qt代码,而是一整套解决复杂工程问题的思维方式和实践能力。当你看到自己编写的程序能够流畅地处理一张虚拟机票的完整生命周期时,那种成就感,是任何理论教程都无法给予的。

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

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

立即咨询