ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机票网络售票模拟系统:LinuxC+QT+MySQL三层架构防超卖实战

机票网络售票模拟系统:LinuxC+QT+MySQL三层架构防超卖实战 简介一套基于Linux C、Qt与MySQL的机票网络售票模拟系统源码面向毕业设计、课程设计与项目开发适合需要掌握C/S架构、数据库交互和Qt界面编程的开发者。压缩包共104个文件、约10.01MB主体为8个cpp源文件、13个C/C头文件、5个ui界面文件、3个pro工程文件及3个PDF说明另配30张png截图与17个zbak备份便于对照界面和恢复编译状态。系统以MySQL存储航班信息、LAMP架构承担前端展示压缩包内既有自动部署shell脚本也有手动配置文档配置完成后可启动Server、Client与Sell三个程序构成完整的售票模拟闭环。目前已有31人浏览学习资源来自网络分享且经严格测试既可直接作为毕业设计或课程设计的参考项目也可用来学习Linux下Qt与MySQL的集成开发方法。1. 机票网络售票模拟系统这套 LinuxCQTMySQL 源码能让你少熬几个通宵每年的课程设计和毕业设计选题里“网络售票”都排在前列。真动手才发现QT 界面三天能画完LinuxC 的 socket 通信一周能调通真正让人熬夜的是 MySQL 里那张订单表在多人同时出票时怎么保证不超卖、不出脏数据。这套基于 LinuxC QT MySQL 的机票网络售票模拟系统正好把这条链路完整走了一遍QT 客户端负责登录、查票、下单LinuxC 服务端处理业务和库存MySQL 负责落库源码加详细文档都齐。拿它做课程设计、毕业设计正合适也适合想搞明白“桌面客户端 C 服务端 数据库”三层结构怎么落地的人。下面按我拆项目的顺序从表结构、QT 界面、C 服务端到避坑记录逐个说清楚。2. MySQL 库表设计与三层架构数据边界定清楚后端少改一半2.1 为什么要走“QT 客户端—LinuxC 服务端—MySQL”三层先把架构选型说透。网络售票模拟系统要模拟的核心场景不是点按钮下单而是“很多人同时抢同一航班的余票”。如果让 QT 客户端直连 MySQL出票时的检查逻辑分散在各客户端进程里事务边界不统一余票校验没地方收敛。挂一台 LinuxC 服务端中转之后业务规则全收进服务端一个函数客户端只负责发协议、收结果这才叫“网络售票”而不是“带数据库的单机程序”。从课程设计评分角度看三层结构也更好讲客户端 QT 讲界面与信号槽服务端 LinuxC 讲 socket、线程与事务MySQL 讲表设计与并发控制三个课程点都落在实处。通信协议我建议用自定义二进制协议而不是 HTTP JSON二进制协议解析逻辑简单、贴近 LinuxC 的系统编程课程要求还不用引入第三方库源码包自包含换环境编译省事得多。方案事务边界并发控制演示说服力QT 直接连 MySQL分散在客户端靠数据库隔离难统一弱像单机系统QT LinuxC 服务端 MySQL收敛在服务端UPDATE 条件扣减 行锁强贴合网络售票数据流向也很直白QT 客户端把“登录 / 查航班 / 下单”转化成一条 TCP 协议包发出服务端监听固定端口比如 8848收包LinuxC 服务端解析命令后调用 MySQL C API 操作库结果再封装成回包发回客户端。后面三章全部围绕这条链路展开先把库表定死后面代码才不用反复返工。2.2 三张核心表user、flight、orders 的字段取舍表结构是这套系统里最不该省思考的地方。我按常规课程设计规模直接给一版可落地的字段其中几个容易想当然的点单独说明。表关键字段类型说明useruser_id / username / password_hash / phoneINT / VARCHAR(32) / VARCHAR(64) / VARCHAR(20)密码存 hash课程设计够用flightflight_no / origin / destination / depart_time / arrive_time / price / seat_total / seat_leftVARCHAR(8) / VARCHAR(32) / DATETIME / DECIMAL(10,2) / INT余票单独一列出票逻辑只盯 seat_leftordersorder_id / user_id / flight_id / seat_count / status / create_timeINT / TINYINT / DATETIMEstatus 用数字标状态机三个细节值得展开。第一金额不用 FLOAT 用 DECIMAL(10,2)。FLOAT 是近似存储价格这种必须精确的值出现 980 存进去、读出来变成 979.999999 的问题演示时很尴尬。第二orders 表留一个 status 字段我习惯用 0 待支付、1 已出票、2 已退票、3 已取消。退票模拟、支付模拟全靠这一个字段翻状态没有它后面做二次开发等于推倒重来。第三flight_no 用业务号展示给用户内部关联用自增 id两者分开。实际项目里订单还会冗余一个航班快照价格、时间那是为了规避航班改价带来的历史订单争议模拟系统规模小不需要冗余写进文档说明“为简化模型未做快照”反而是加分项。索引不要贪多。演示规模不过几十条航班、几百条订单多建索引除了拖慢写入没有实际收益。我只在 orders 表建了 (flight_id, status) 联合索引因为演示时“按航班统计已售 / 退票”是最频繁的操作flight 表在 origin、destination、depart_time 上建一个普通索引够用。2.3 初始化脚本建库建表加演示数据一步到位整个数据库脚本我一般整理成一个 init.sql从头执行到尾不分开建这样课程设计报告里直接引用一份脚本就行。下面这段是核心贴出来可直接改库名复用。CREATE DATABASE IF NOT EXISTS airticket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE airticket; CREATE TABLE user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE flight ( id INT AUTO_INCREMENT PRIMARY KEY, flight_no VARCHAR(8) NOT NULL UNIQUE, origin VARCHAR(32) NOT NULL, destination VARCHAR(32) NOT NULL, depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL, seat_total INT NOT NULL DEFAULT 100, seat_left INT NOT NULL DEFAULT 100, KEY idx_route (origin, destination, depart_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, flight_id INT NOT NULL, seat_count INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, KEY idx_flight_status (flight_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO flight (flight_no, origin, destination, depart_time, arrive_time, price, seat_total, seat_left) VALUES (CA1234, 北京, 上海, 2025-06-01 08:00:00, 2025-06-01 10:30:00, 980.00, 100, 100), (MU5678, 上海, 广州, 2025-06-01 09:30:00, 2025-06-01 11:50:00, 760.00, 100, 100); INSERT INTO user (username, password_hash, phone) VALUES (student01, e10adc3949ba59abbe56e057f20f883e, 13800000001);参数说明utf8mb4 配合 utf8mb4_general_ci 是中文数据的基本盘建库时定了后面乱码少一半engine 必须 InnoDBMyISAM 不支持行级锁和事务超卖问题根本没法处理password_hash 里那串是 md5(123456)课程设计演示足够别拿到生产环境用。脚本导入走命令行最稳mysql -u root -p init.sql导入完验证一条 SQLSELECT COUNT(*) FROM flight;能看到两条演示航班落进去即可。这个脚本同时也是数据库设计说明的附录素材写进课程设计报告里当初始化脚本答辩老师基本不会在这块挑毛病。注意脚本里的 root 密码留空只是演示方便。如果这份工程要放到多人演示环境建议单独建一个 airticket 账号只授权 airticket 库别把 root 口令写进源码发给别人。3. QT 客户端实战信号槽切界面、TCP 协议封装与直连调试开关3.1 工程组织登录框、主窗口、查询列表的分工QT 端我按三个文件组组织LoginDialog 负责登录MainWindow 承载主界面NetClient 单独封装与服务端的收发。把网络收发拆成独立类而不是写在 MainWindow 里是这套源码里最值得抄的结构——后续你加“退票”界面只需要复用 NetClient不用动主窗口。入口逻辑是常见的“先登录后进主窗”// main.cpp #include QApplication #include login.h #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); LoginDialog dlg; if (dlg.exec() ! QDialog::Accepted) { return 0; // 用户点了取消直接退出 } MainWindow w; w.initSession(dlg.userId()); // 把登录用户的 id 带进主窗口下单要用 w.show(); return app.exec(); }这里的关键是 initSession 把用户信息从登录框传给主窗口而不是主窗口再去查一次数据库。exec() 是模态运行登录框不关闭后面代码不执行这是 QT 桌面程序最顺手的流程控制方式。主窗口里的信号槽接线在构造函数里完成典型的一句connect(queryBtn, QPushButton::clicked, this, MainWindow::doQuery); connect(orderBtn, QPushButton::clicked, this, MainWindow::doOrder);界面组件用 QTableWidget 展示航班列表就够演示规模别上 QTableView 加自定义 Model那是几万行数据才需要的复杂度真遇到表格大数据卡顿再换 QTableView 配 QAbstractTableModel这个坑第六章会提。3.2 把网络收发隔离成 NetClient定长包头协议客户端与服务端约好一个最简协议前 4 字节是整包长度后 4 字节是命令号后面跟 body。命令号 1 登录、2 下单、3 查航班、4 退票这张表写进文档两边照着实现即可。// netclient.cpp 发送一个请求包 bool NetClient::sendRequest(int cmd, const QByteArray body) { QByteArray pkt; QDataStream out(pkt, QIODevice::WriteOnly); out quint32(8 body.size()); // 总长度 包头 8 字节 body out quint32(cmd); // 命令号 pkt body; socket_-write(pkt); return socket_-waitForBytesWritten(1000); }对应地读回包要等完整响应// 读响应超时 3 秒 bool NetClient::recvResponse(QByteArray body, int timeoutMs) { if (!socket_-waitForReadyRead(timeoutMs)) return false; QDataStream in(socket_); quint32 len, resp_cmd; in len resp_cmd; // 先读 8 字节包头 body socket_-read(len - 8); // 再读 body return true; }参数说明包头固定 8 字节服务端先 recv 收满这 8 字节解析出长度再收 body这是从根上拦粘包问题的写法。QDataStream 默认大端序服务端是 C 语言手解包的话按大端读一致即可两边字节序不一致是最隐蔽的玄学 bug表现为“发出去的命令没反应”查半天发现是字节序倒了。waitForBytesWritten 是阻塞式接口发送量小无所谓真实项目要改成异步信号槽这里不做展开。3.3 开发期直连 MySQL 的调试开关为什么保留 QSqlQuery严格按三层架构QT 客户端不该直接碰数据库但开发界面时每次都先启动服务端太浪费时间。我一般会在工程里留一个DEBUG_SQL编译开关在 .pro 文件里用DEFINES DEBUG_SQL控制。调试界面时直连 MySQL把“界面问题”和“协议问题”分开定位#ifdef DEBUG_SQL QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(airticket); db.setUserName(root); db.setPassword(123456); if (!db.open()) { qDebug() open mysql failed: db.lastError().text(); return; } #endif// 查航班用预编译语句别拼字符串 QSqlQuery q; q.prepare(SELECT flight_no, origin, destination, depart_time, price, seat_left FROM flight WHERE origin :o AND destination :d AND depart_time :t); q.bindValue(:o, departCombo-currentText()); q.bindValue(:d, arriveCombo-currentText()); q.bindValue(:t, dateEdit-dateTime().toString(yyyy-MM-dd HH:mm:ss)); q.exec(); while (q.next()) { // 逐行取字段填充 QTableWidget }这里三件事要讲清楚。第一prepared statement 的 bindValue 不只防 SQL 注入还顺带解决了中文参数乱码这是手拼 SQL 最容易踩的地方。第二日期条件必须先格式化成yyyy-MM-dd HH:mm:ss字符串直接传 QDateTime 在某些驱动下会转出奇怪格式和 MySQL DATETIME 对不上。第三这个开关只用于开发期定位问题答辩演示时一定编译成走 TCP 的版本否则评委一句“你这不就是单机程序加了个数据库吗”整层架构就白讲了。提示直连调试的账号密码写死在源码里没有问题课程设计项目没有外网暴露风险如果要把这套工程改造成带服务器的实际演示记得把密码挪到配置文件。4. LinuxC 服务端实战多线程 socket、协议解析与事务出票4.1 每连接一线程模拟系统最直白的并发模型服务端选型上课程设计规模不用上 epoll每来一个连接就开一个 pthread 是最直白的模型代码量小、思路清楚答辩时也容易讲。主循环负责 accept每接到一个连接复制一份文件描述符传给新线程// server.c 主循环 int listen_fd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); // bind / listen 省略监听端口固定比如 8848 while (1) { struct sockaddr_in client_addr; socklen_t len sizeof(client_addr); int fd accept(listen_fd, (struct sockaddr *)client_addr, len); if (fd 0) continue; int *pfd malloc(sizeof(int)); // 拷贝一份别直接传 fd *pfd fd; pthread_t tid; pthread_create(tid, NULL, client_handler, pfd); pthread_detach(tid); // 分离线程结束自动回收资源 }这里最阴险的坑是把fd直接传给线程函数——主循环下一次 accept 会覆盖同一个 fd 变量线程里拿到的描述符可能已经变成新连接翻车率极高。用 malloc 拷贝一份再传线程函数里用完 free是标准写法。pthread_detach 让线程结束自动释放资源避免僵尸线程堆积如果哪一天你想统计在线人数再改成不 detach、用数组管理 tid。SO_REUSEADDR 一定要设否则服务端 CtrlC 重启时大概率报 Address already in use。4.2 拆包与解析recv 必须循环读满TCP 是流协议recv 一次返回多少字节完全看网络状况服务端第一次读到的可能只是半个包头。我一般先写一个 recv_full 把问题封死// 读满 n 字节才返回处理粘包和半包 int recv_full(int fd, void *buf, size_t n) { char *p (char *)buf; size_t left n; while (left 0) { int r recv(fd, p, left, 0); if (r 0) return -1; // 对端断开或出错 p r; left - r; } return 0; }读包的完整链路先recv_full(fd, header, 8)从 header 里解出总长度 len再recv_full(fd, body, len - 8)。body 按命令号分发给不同业务函数。解析 body 时不建议把字节流强转成 struct 直接访问——C 结构体有内存对齐编译器会在字段间插入填充字节客户端按紧凑排列发的包这边一强转全错位。要省事可以加__attribute__((packed))统一打包但必须两边一致。这种“结构体对齐导致的解析错位”很气人小数据对大数据错查到最后是填充字节在作怪。我习惯逐字段 memcpy 出来稳还方便加日志。4.3 出票事务条件 UPDATE 扣减affected_rows 判定成败这是整套源码的业务核心也是防超卖的真正关键。下单流程三件事扣余票、插订单、提交。正确顺序是先用带条件的 UPDATE 扣余票再判断mysql_affected_rows// 出票user_id 购 seat_count 张 flight_id 航班成功返回 0 int book_ticket(MYSQL *conn, int user_id, int flight_id, int seat_count) { mysql_autocommit(conn, 0); // 关掉自动提交开启事务 char sql[256]; snprintf(sql, sizeof(sql), UPDATE flight SET seat_left seat_left - %d WHERE id %d AND seat_left %d, seat_count, flight_id, seat_count); if (mysql_query(conn, sql) ! 0) { mysql_rollback(conn); return -1; // SQL 执行失败回滚 } if (mysql_affected_rows(conn) ! 1) { mysql_rollback(conn); return -2; // 余票不足或航班不存在 } snprintf(sql, sizeof(sql), INSERT INTO orders (user_id, flight_id, seat_count, status, create_time) VALUES (%d, %d, %d, 1, NOW()), user_id, flight_id, seat_count); if (mysql_query(conn, sql) ! 0) { mysql_rollback(conn); return -3; } mysql_commit(conn); mysql_autocommit(conn, 1); return 0; }逻辑说明把“余票够不够”的判断直接写进 UPDATE 的 WHERE 条件数据库会对这一行加行锁同一航班的两个并发请求串行执行后到的那条affected_rows为 0直接回滚。这是防超卖的标准姿势。反面典型是“先 SELECT seat_left 再判断再 UPDATE”两个线程同时读到余票 1都认为能买结果两张订单扣一次库存避坑章节会专门复盘这个现场。参数说明seat_count 在进函数前要校验比如单次下单不超过 5 张create_time 用库的 NOW() 而不是客户端时间保证所有订单时间来自同一时钟源。服务端每个线程持有独立 MySQL 连接进线程时mysql_real_connect结束时mysql_close。不要全局共用一个连接MySQL 驱动本身不是并发安全的共享连接会在高并发时崩出稀奇古怪的错误。连接断了也能自动重连一次再报错这个重试逻辑写在 client_handler 里比让客户端重发请求省事。5. 避坑手册MySQL 驱动、依赖路径、中文乱码与超卖现场5.1 现象QT 报 “QMYSQL driver not loaded”原因Qt 安装时默认不带 MySQL 驱动插件或者插件编译时依赖的 libmysqlclient 版本和本机装的不匹配。解决先在程序里打印QSqlDatabase::drivers()看有没有 QMYSQL。没有的话Linux 发行版一般有现成包装上重启即可这是最快路线源码编译路线要在 Qt 构建时勾选 MySQL 插件支持生成 libqsqlmysql.so 放到 Qt 安装目录的 plugins/sqldrivers 下。最省事的绕法是用 TCP 模式把数据库访问全放到 LinuxC 服务端QT 端根本不加载 QMYSQL驱动问题直接消失——这也是我推荐三层架构的另一个原因。5.2 现象Qt Creator 编译报 “:-1: error: dependent ‘...qt\5.15.2\msvc2019_64\include\qtwidgets’ does not exist”原因工程里 .pro 或 .pro.user 文件残留了上一台机器的绝对路径。我见过最多的形态是 Qt 5.15.2 配 MSVC 的工程include 路径写成..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets这种一长串换机器、换 Qt 版本后路径不存在编译直接失败。解决删除 .pro.user 文件让 Qt Creator 重新生成把 .pro 里写死的 INCLUDEPATH、LIBS 绝对路径改成相对路径或者干脆删掉让 qmake 自动推导。看到 include 路径里一串..\..\..\就要警觉路径越深越脆这也是这类源码换环境编译最容易翻车的一步。5.3 现象MySQL 表里中文全是问号原因建库时用了默认字符集 latin1或者程序连接后没设置连接字符集客户端和服务端字符集对不上。解决建库建表统一指定DEFAULT CHARACTER SET utf8mb4见 2.3 节脚本程序连接成功后立即执行SET NAMES utf8mb4LinuxC 服务端源码文件另存为 UTF-8 编码别让编辑器保存成 GBK。三处对齐后剩下就是终端显示问题了可以用echo $LANG确认 locale。乱码问题九成出在“库是 latin1、连接是 utf8”这种错配上改完两处基本根治。5.4 现象同一个航班爆出两张订单余票却只扣了一次原因出票逻辑写成了“先 SELECT 读余票满足条件再 UPDATE”。两个线程同时读都读到 seat_left1都认为可以买于是都走到 UPDATE库存只减一次订单却生成两张。这是并发编程里典型的 check-then-act 竞态。解决改成第 4 章 4.3 的条件 UPDATE 写法把判断塞进WHERE seat_left seat_count用mysql_affected_rows判定成功数据库行锁保证同一航班的扣减串行。代码写完之后用第六章的并发脚本压一轮能直接验证这个 bug 是否根除。这套源码里如果哪部分逻辑你改过复测这一步别省。5.5 现象服务端重启时报 bind: Address already in use原因上一个服务端进程还没退干净或者连接处于 TIME_WAIT 状态端口没释放。解决监听 socket 上设置 SO_REUSEADDR4.1 节的代码里已经有了重启前用lsof -i:8848查端口占用把残留进程 kill 干净。监听端口号建议集中定义在一个头文件里比如#define SERVER_PORT 8848客户端和服务端都 include 它改端口只改一处避免两边各写各的导致连不上。6. 验收与二次开发并发压测脚本和三分钟演示流程资源拿到手光能编译跑通不算完答辩和验收最怕现场翻车。我自己的做法是准备一个余票只剩 1 张的测试航班然后压一把用事实说话。先往 flight 表插一条演示用的极端数据seat_total 100、seat_left 1记下它的 id。然后写一个最简命令行压测客户端循环 20 次并发下单同一航班只允许成功一单for i in $(seq 1 20); do ./test_client 127.0.0.1 8848 3 1 # 参数依次是 命令号 / 航班id / 张数 done wait mysql -u root -p123456 airticket -e SELECT seat_left FROM flight WHERE id 3;验证逻辑很直接正常结果应该是 20 个进程里 19 个返回“余票不足”seat_left 从 1 变成 0orders 表只多一条 status1 的订单。能跑出这个结果超卖问题才算真正闭环。演示时动作顺序固定先开 MySQL 命令行展示测试航班的初始余票再开两个 QT 客户端实例同时登录下单一个成功一个被拒最后切回 MySQL 命令行看余额变化全程不用读文档数据自己会说话。二次开发的方向也顺手提几个。退票接口把 orders.status 改成 2同时把 seat_left 加回去同样要包事务这是和出票对称的完整闭环查航班加价格排序和模糊搜索SQL 里 ORDER BY price 和 LIKE 拼接记得用预编译QTableWidget 在数据量上来之后卡顿明显换成 QTableView 加 QAbstractTableModel 是成熟解法代码量多一点但值得。这三个点随便挑一个写进课程设计报告的“下一步工作”比空泛地写“优化系统性能”实在得多。这套源码我前后拆过两遍第一遍我自己也踩了 5.4 那个超卖坑用先 SELECT 后 UPDATE 写出了脏数据当时还没意识到问题。从那以后我每次拿到这类“模拟系统”的工程验收前都强制走一遍并发压测造一个只有 1 张余票的航班并发下单看崩溃、看超卖、看连接泄漏这一遍能暴露七成以上平时测不出来的问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表