
简介一套基于LinuxC、QT与MySQL实现的机票网络售票模拟系统完整源码及配套文档面向毕业设计、课程设计与项目开发场景适合具备一定C/C和Linux基础、希望实践数据库与GUI综合开发的学习者。系统采用客户端/服务端架构包含服务端、客户端、售票端三大模块航班信息存储于MySQL前端界面由QT构建并采用LAMP架构完成数据库展示覆盖从数据库配置到界面交互的完整链路。资源共86个文件压缩包约9.86MB主要包含C/C源码与头文件、QT的UI界面与qrc资源文件、工程配置文件、Shell部署脚本、PDF与Markdown说明文档以及大量PNG截图方便对照界面理解实现逻辑。文档中既提供自动部署脚本也给出手动配置指南并附有数据库建表与初始化指导能有效降低环境搭建门槛。目前已有155人学习下载适合作为课程设计或毕业设计的参考模板也可用于练习QT多线程、MySQL编程和简单网络通信。1. 这个机票网络售票模拟系统到底在模拟什么如果你翻过几份课程设计和毕业设计题目会发现“XX管理系统”占了多半而“机票网络售票模拟系统”属于少有的能把三层技术栈一次性练完的题目Linux C 负责底层业务逻辑Qt 负责图形界面和交互MySQL 负责持久化存储。表面上它只是一个“模拟系统”不需要真的连上航信或中航信但数据表设计、并发扣库存、事务回滚、界面状态同步这些环节和真实售票系统的核心难点是一致的。很多人卡在第一步先查 Qt 还是先建表实际上是先想清楚“谁来调谁”——界面层不能直接堆 SQL业务层不能写死 UI最后你会得到一条清晰的调用链Qt 按钮事件 → Linux C 函数 → MySQL 预编译语句 → 结果回填 QTableView。这篇文章直接按这条链路展开每一步给出可复现的命令、代码和参数表最后落到如何把这个项目打包成“能答辩、能演示、能写进简历”的完整工程。2. 建表与连接把 Linux C 和 Qt 黏在 MySQL 上2.1 先定表结构航班、订单、用户三张核心表机票售票模拟系统不管界面多华丽底层永远围绕三张表转用户表、航班表、订单表。不要一开始就加会员等级、优惠券、日志表那些是后期“凑功能”时再加的。第一版把三张表建好索引建对后续所有功能都能复用来。-- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password_hash CHAR(64) NOT NULL, salt CHAR(16) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 航班表 CREATE TABLE flight ( id INT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(10) NOT NULL, origin VARCHAR(20) NOT NULL, destination VARCHAR(20) NOT NULL, depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, total_seats INT NOT NULL, remaining_seats INT NOT NULL, price DECIMAL(10,2) NOT NULL, INDEX idx_route (origin, destination), INDEX idx_depart (depart_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE ticket_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, flight_id INT NOT NULL, seats INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-已支付 1-已退票, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), INDEX idx_user (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_order_flight FOREIGN KEY (flight_id) REFERENCES flight(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里remaining_seats是核心字段所有并发控制都围绕它。索引只建了查询频率最高的三处用户名唯一、航线联合索引、出发时间。不要给price建索引这种模拟系统的价格没有范围查询需求建了反而拖慢写入。外键在真实高并发系统里经常被去掉但这是课程设计保留外键可以在文档里写“保证数据完整性”答辩时能多讲两句。2.2 Qt 连接 MySQL 的两种方式驱动直连与 ODBCQt 访问 MySQL 最常用的是QMYSQL驱动。先在命令行验证驱动是否可用ldconfig -p | grep mysql # 确认 libmysqlclient.so 存在然后在 Qt 工程文件里加入QT sql连接代码如下#include QSqlDatabase #include QSqlError #include QDebug QSqlDatabase initDb() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(airline_db); db.setUserName(root); db.setPassword(your_password); if (!db.open()) { qCritical() DB open failed: db.lastError().text(); } return db; }注意addDatabase的第二个参数是连接名默认是 Qt 默认连接。如果系统里同时开了查询和事务建议用不同的连接名避免QSqlDatabase的线程亲和问题。我一般会在每个线程里用QSqlDatabase::addDatabase(QMYSQL, thread_conn_ QString::number((quintptr)QThread::currentThreadId()))创建独立连接。如果QMYSQL驱动加载失败常见原因是 Qt 编译时没带 MySQL 插件或者libmysqlclient.so版本和驱动不匹配。备选方案是走 ODBC但需要额外配置/etc/odbc.ini不推荐因为课程设计答辩时换一台电脑就很难复现。后文第 5 章会讲静态编译规避这个问题。2.3 用 Linux C 封装业务逻辑业务层分离的关键很多学生的项目直接在 Qt 槽函数里写 SQL代码全堆在一个on_pushButton_clicked()里最后仓库看起来像一坨不能维护的面条。既然标题强调了 Linux C就应当把业务逻辑抽成纯 C 函数通过extern C暴露给 Qt 调用。先定义头文件business.h#ifndef BUSINESS_H #define BUSINESS_H #include mysql/mysql.h typedef struct { int flight_id; char flight_no[16]; char origin[32]; char destination[32]; char depart_time[32]; int remaining_seats; double price; } FlightInfo; // 连接 MySQL返回 MYSQL 句柄 MYSQL* biz_connect(const char* host, const char* user, const char* pass, const char* db); // 根据起点终点查航班结果写入 out_arr返回条数 int biz_search_flights(MYSQL* conn, const char* origin, const char* dest, FlightInfo* out_arr, int max); // 订票事务扣余票、插订单、改状态全部成功才返回 0 int biz_book_ticket(MYSQL* conn, int user_id, int flight_id, int seats); #endif对应的business.c里biz_search_flights用mysql_stmt_prepare做预编译查询避免拼接 SQL。这里只列核心片段int biz_search_flights(MYSQL* conn, const char* origin, const char* dest, FlightInfo* out_arr, int max) { const char* sql SELECT id, flight_no, origin, destination, DATE_FORMAT(depart_time, %Y-%m-%d %H:%i), remaining_seats, price FROM flight WHERE origin ? AND destination ?; MYSQL_STMT* stmt mysql_stmt_init(conn); mysql_stmt_prepare(stmt, sql, strlen(sql)); MYSQL_BIND params[2]; memset(params, 0, sizeof(params)); params[0].buffer_type MYSQL_TYPE_STRING; params[0].buffer (char*)origin; params[0].buffer_length strlen(origin); params[1].buffer (char*)dest; params[1].buffer_length strlen(dest); mysql_stmt_bind_param(stmt, params); mysql_stmt_execute(stmt); // 绑定结果集循环 mysql_stmt_fetch 填入 out_arr // 略 mysql_stmt_close(stmt); return count; }参数说明MYSQL_BIND里的buffer_type要和表字段类型对应否则会出现“0 rows returned”但 MySQL 命令行能查出来的怪问题。DATE_FORMAT是为了把DATETIME直接转成字符串省去在 C 端解析MYSQL_TIME的麻烦。这是我自己常用的折中——业务层是 C但没必要在模拟系统里和 C 的日期结构体硬磕。用extern C包一层Qt 侧才能正常链接extern C { #include business.h }这样做的直接收益是Qt 界面里只出现biz_search_flights(db, 北京, 上海, arr, 100)不出现任何 SQL 字符串。后续把系统改成命令行版本或者换成 GTK 界面业务层一个字符都不用动。3. 航班查询、订票与事务把模拟系统做“真”3.1 航班查询QTableView QSqlQueryModel 的实时刷新查询界面是系统的门面。最省事的做法是QSqlQueryModel直接把结果集模型绑定到QTableView但这样就把 SQL 写死在了界面层和 2.3 的设计矛盾。更好的做法是让 C 业务函数返回结果再手动填充QStandardItemModel。核心代码如下void BookingWindow::onSearchClicked() { QString origin ui-originEdit-text(); QString dest ui-destEdit-text(); // 调用 C 业务层 FlightInfo infos[128]; int count biz_search_flights(db, origin.toUtf8().constData(), dest.toUtf8().constData(), infos, 128); // 把结果装配到 model QStandardItemModel* model new QStandardItemModel(this); model-setHorizontalHeaderLabels( {航班号, 起点, 终点, 起飞时间, 余票, 价格}); for (int i 0; i count; i) { QListQStandardItem* row; row new QStandardItem(infos[i].flight_no) new QStandardItem(infos[i].origin) new QStandardItem(infos[i].destination) new QStandardItem(infos[i].depart_time) new QStandardItem(QString::number(infos[i].remaining_seats)) new QStandardItem(QString::number(infos[i].price, f, 2)); model-appendRow(row); } ui-flightTable-setModel(model); ui-flightTable-horizontalHeader()-setStretchLastSection(true); }这段代码的关键点是toUtf8().constData()。MySQL 表用utf8mb4Qt 的QString内部是 UTF-16如果不转 UTF-8 直接传给 C 函数中文航线名会变成乱码甚至导致mysql_stmt_prepare都正常但查询结果为空。我在调试时遇到过一次用 mysql 命令行查有 5 条记录程序里查出来 0 条最后发现是gbk编码的origin传进了utf8mb4表。所以记住Qt 侧进入 C 层的字符串一律toUtf8()。3.2 订票与退票事务与行锁保证余票不错乱订票的核心动作是检查余票 → 扣减余票 → 插入订单。这三个操作必须在一个事务里执行否则高并发演示时一旦两个窗口同时订同一航班的最后一张票就会卖出两张。MySQL InnoDB 下用SELECT ... FOR UPDATE加行锁。C 函数biz_book_ticket完整实现int biz_book_ticket(MYSQL* conn, int user_id, int flight_id, int seats) { if (seats 0) return -1; mysql_autocommit(conn, 0); // 1. 锁定航班行读取当前余票 const char* lock_sql SELECT remaining_seats FROM flight WHERE id ? FOR UPDATE; MYSQL_STMT* lock_stmt mysql_stmt_init(conn); mysql_stmt_prepare(lock_stmt, lock_sql, strlen(lock_sql)); int lock_flight_id flight_id; MYSQL_BIND lock_param {0}; lock_param.buffer_type MYSQL_TYPE_LONG; lock_param.buffer lock_flight_id; mysql_stmt_bind_param(lock_stmt, lock_param); int remaining -1; MYSQL_BIND lock_result {0}; lock_result.buffer_type MYSQL_TYPE_LONG; lock_result.buffer remaining; mysql_stmt_bind_result(lock_stmt, lock_result); mysql_stmt_execute(lock_stmt); mysql_stmt_fetch(lock_stmt); mysql_stmt_close(lock_stmt); if (remaining seats) { mysql_rollback(conn); mysql_autocommit(conn, 1); return -2; // 余票不足 } // 2. 扣减余票 const char* update_sql UPDATE flight SET remaining_seats remaining_seats - ? WHERE id ?; // ... 绑定参数并执行略 // 3. 插入订单 const char* insert_sql INSERT INTO ticket_order(order_no, user_id, flight_id, seats) VALUES(?, ?, ?, ?); // 生成 order_no: 时间戳 随机数略 // 所有成功则 commit mysql_commit(conn); mysql_autocommit(conn, 1); return 0; }提示FOR UPDATE必须放在事务里才生效且必须是 InnoDB 引擎。MyISAM 不支持行锁FOR UPDATE会被忽略导致余票超卖。另外注意事务的隔离级别REPEATABLE READMySQL 默认下如果先SELECT后UPDATE用FOR UPDATE是安全的但如果是先普通SELECT再在业务层判断余票然后UPDATE就会出现“幻读”导致负数余票。最简单的方法是判断和扣减都放到同一条UPDATE的条件里。比如将第 2 步改成UPDATE flight SET remaining_seats remaining_seats - ? WHERE id ? AND remaining_seats ?如果affected_rows为 0说明余票不足直接回滚。这比先查询再更新更不容易出问题也是我在这个项目里最终采用的方式。保留FOR UPDATE纯粹是为了在文档里写“使用了行锁”但真正兜底的是WHERE条件。3.3 多线程模拟并发用 QThread 压测订票接口写完订票逻辑后需要验证它在并发下是否靠谱。可以在 Qt 里开几个线程同时订票class BookWorker : public QThread { public: int userId, flightId, seats; void run() override { MYSQL* conn biz_connect(127.0.0.1, root, pwd, airline_db); int ret biz_book_ticket(conn, userId, flightId, seats); qDebug() book result: ret; mysql_close(conn); } };然后启动 10 个线程抢最后 5 张票QListBookWorker* workers; for (int i 0; i 10; i) { BookWorker* w new BookWorker(); w-userId 1; w-flightId 7; w-seats 1; workers w; w-start(); } for (auto* w : workers) w-wait();每个线程必须使用独立的MYSQL*连接。虽然 Qt 的QSqlDatabase默认连接在多线程下是分线程自动复制但裸的 C API 没有这个保护。运行结束后查询remaining_seats应该正好剩 0 或负数如果没有WHERE条件兜底。这一步是答辩时最有说服力的功能演示比截图管用。4. 登录鉴权、定时任务与界面细节把课程设计打磨成“产品”4.1 密码存储与登录会话别再用明文了很多课程设计源码里密码直接明文存 MySQL导师一问“如何防止拖库”就哑火。这里做一个最基础的加盐哈希用 Qt 的QCryptographicHash生成 SHA-256 摘要配合每用户随机 salt。QByteArray generateSalt() { return QByteArray::number(QRandomGenerator::global()-generate(), 16); } QByteArray hashPassword(const QString password, const QByteArray salt) { QByteArray raw password.toUtf8() salt; return QCryptographicHash::hash(raw, QCryptographicHash::Sha256).toHex(); }注册流程简化为三步生成 salt → 算 hash → 插入user表。登录时按username查出 salt 和 hash用同样算法算一次比对即可。更安全的是用PBKDF2或bcrypt但课程设计的演示环境里拿 SHA-256 加盐已经足够在答辩时解释“不能被反查”。登录会话不需要引入 Redis用 Qt 内存里维护一个全局的currentUserId即可。如果需要增加“记住登录”功能可以写一个 token 表存user_id token expire_time本地用QSettings保存 token每次启动比对 MySQL 里的有效期。这是比较完整的做法文档写作时能多出一节。4.2 用 QTimer 模拟后台刷新任务机票数据不是固定的真实系统里有航班时刻调整、价格变动。模拟系统里可以加一个QTimer每 30 秒把某个航班的余票随机调整一下或者将“未来 24 小时起飞的航班”的剩余票数归零模拟售罄。QTimer* refreshTimer new QTimer(this); connect(refreshTimer, QTimer::timeout, this, [this](){ // 将出发时间在两小时内的航班置为售罄 QSqlQuery query(db); query.exec(UPDATE flight SET remaining_seats 0 WHERE depart_time BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 2 HOUR)); ui-statusBar-showMessage( QString(已更新 %1 个临近航班).arg(query.numRowsAffected()), 3000); onSearchClicked(); // 自动刷新当前列表 }); refreshTimer-start(30000);注意query.numRowsAffected()在 MySQL 驱动下返回的是受影响行数但如果UPDATE没有改变任何值比如本来已经是 0某些情况下返回 0。如果发现总是显示 0检查连接是否开启了CLIENT_FOUND_ROWS标志这个标志会改变行为。在模拟系统里其实不必关注这个数字改成执行完直接刷列表更直观。QTimer的最小间隔受系统定时器分辨率限制Linux 下一般不会高于 1 毫秒精度但这里我们需要的是 30 秒级完全没问题。线程模型上QTimer回调运行在主线程所以直接操作 UI 是安全的不要在里面执行耗时 SQL否则界面卡顿。4.3 界面体验Qt 国际化与自定义进度条课程设计最容易被扣分的是“界面太丑”。不要求做出 Material Design但至少要做到两点所有用户可见的字符串都能通过tr()提取支持中英文切换操作时有反馈比如订票过程中展示进度动画。国际化先做代码侧ui-searchBtn-setText(tr(Search)); ui-bookBtn-setText(tr(Book Ticket));然后在.pro文件里启用TRANSLATIONS airline_zh_CN.ts airline_en.ts用lupdate生成.ts文件翻译后lrelease导出.qm。在main.cpp里根据系统语言加载QTranslator translator; if (translator.load(airline_en.qm, :/translations/)) { qApp-installTranslator(translator); }自定义进度条可以用在“批量生成订单”或“初始化模拟数据”场景。比如启动时往数据库插入 10000 条航班记录用 QProgressDialog 显示进度QProgressDialog progress(生成模拟数据..., 取消, 0, 1000, this); progress.setWindowModality(Qt::WindowModal); for (int i 0; i 1000; i) { if (progress.wasCanceled()) break; insertOneFlight(i); progress.setValue(i 1); QCoreApplication::processEvents(); }这里processEvents()必须调用否则界面会进入假死状态。严格说批量插入应该放进QtConcurrent::run或QThread但考虑到课程设计的数据量万条级别在本地 MySQL 上不到一秒为了演示进度条可以手动在每次插入里加QThread::msleep(1)模拟耗时——这个技巧写进文档里也不丢人重点是展示了“异步进度反馈”的思路。5. 编译部署与文档整理把源码变成可演示的完整工程5.1 用 CMake 组织 C Qt MySQL 混合构建课程设计最忌讳的是“在我电脑上能跑”。用 CMake 管理依赖并留出清晰的 README。最小 CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(airline_system LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 COMPONENTS Widgets Sql REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(MYSQLPP REQUIRED IMPORTED_TARGET mysqlclient) add_executable(airline_system src/main.cpp src/booking_window.cpp src/business.c src/business.h ) target_link_libraries(airline_system PRIVATE Qt6::Widgets Qt6::Sql PkgConfig::MYSQLPP )这里把business.c直接编进 Qt 可执行文件。注意set(CMAKE_AUTOMOC ON)只处理 Qt 元对象纯 C 文件不影响。编译时如果提示mysql/mysql.h找不到先执行sudo apt install libmysqlclient-devUbuntu 20.04 下Qt 包可以用apt install qt6-base-dev安装但 Qt Creator 自带的版本可能更顺手。如果是 Ubuntu 20.04 交叉编译环境请参考系统自带 Qt 的 mkspec这属于另一篇教程的范畴。5.2 MySQL 驱动缺失的坑与 Qt 静态编译程序放到另一台机器上直接“Driver not loaded”是最高频的翻车现场。原因很简单目标机器没有 Qt 的 MySQL 驱动插件。解决办法有两个层次。第一如果目标机器上安装了相同版本 Qt可以把启动目录下的sqldrivers整个复制过去cp -r /path/to/qt/plugins/sqldrivers ./plugins/然后在代码里手动指定插件路径QApplication::addLibraryPath(QCoreApplication::applicationDirPath() /plugins);第二静态编译 Qt 时需要在 configure 时指定./configure -static -plugin-sql-mysql -I/usr/include/mysql这样QMYSQL驱动会编进 libqxcb 和 libqsqlmysql 静态库但 MySQL 客户端库本身需要动态链接。如果嫌静态编译太耗时另一个思路是直接要求目标环境安装libqt5sql-mysql和libmysqlclient-dev并把这一条写进部署文档。答辩演示机通常由学校实验室提供提前装好依赖是每个学生的基本礼仪这比什么技术都管用。5.3 详细文档里必须出现的十个关键点文档不是把代码注释抄一遍而是让评审在 10 分钟内看懂系统怎么跑起来。运行环境操作系统版本、Qt 版本、MySQL 版本、CMake 版本。数据库初始化给出schema.sql命令行执行mysql -u root -p schema.sql。配置文件数据库连接字符串独立成db_config.ini用QSettings读取避免改源码。前台操作流程注册 → 登录 → 查询 → 订票 → 退票 → 退出每一步配截图。ER 图用 MySQL Workbench 反向生成比手画图快且准确。核心代码讲解事务、行锁、预编译语句三块各列一个代码片段标注关键行。并发演示步骤开两个终端跑两次程序同时订同一张票记录结果。异常处理MySQL 连接失败界面提示、订票失败回滚后的反馈。已知缺陷比如没有处理支付超时、没有做实名认证诚实写出留给扩展。后期扩展思路加 Redis 缓存、用 QWebEngine 做 H5 前端表现出“可继续深耕”的潜力。最后一章收在“如何把这份文档变成 30 分钟可复现的演示流程”上先启动 MySQL运行 schema 脚本再编译程序进入后先演示查询再演示订票最后展示数据库中的ticket_order表变化。这套流程你练上两三遍答辩时就能闭着眼走完导师问哪个环节都能随手指出源码位置——这才是这款模拟系统从“能跑”到“能讲”的关键一步。本文还有配套的精品资源点击获取