简介:在管理系统类课程设计与毕业设计中,如何让项目兼具技术深度与工程实用性,是许多开发者思考的起点。数据库设计决定了数据一致性的根基,而C语言则适合编写贴近底层、逻辑清晰的业务核心。通过合理划分界面层、逻辑层与存储层,利用QT的信号槽机制实现交互响应,同时将MySQL事务与条件更新用于余票扣减和订单创建,可以避免超卖、保证数据原子性。这种组合不仅覆盖了查航班、锁余票、下订单等真实业务流程,也为并发场景下的库存类应用提供了可复用的解决方案。从建表索引、连接池管理,到工作线程与界面线程的协作,再到中文乱码、Qt平台插件等常见问题的排查,完整呈现了LinuxC+QT+MySQL三者协同开发机票售票模拟系统的落地路径。
1. 毕业设计选题里最“能打”的组合:LinuxC+QT+MySQL做机票售票模拟系统
每年到了做课程设计和毕业设计的季节,总有人抱着“XXX管理系统”的题目找我,清一色增删改查,答辩老师问两句就问穿了。相比之下,基于LinuxC+QT+MySQL开发的机票网络售票模拟系统是个难得的“全栈”题目:界面归QT管,核心票务逻辑归LinuxC管,数据落库归MySQL管,三块缺一不可,正好把大学四年最常考的三板斧串起来。它解决的是“怎么把一个真实业务流程——查航班、锁余票、下订单——用C语言和数据库协作模拟出来”的问题,适合C语言、数据结构和数据库原理都有一定底子的人;如果是一张白纸的新手,跟着这篇把表结构和业务函数先抄明白,也比从零憋强得多。这篇笔记我会按自己的落地顺序来写:先搭数据库,再写C业务逻辑,然后接QT界面,最后把编译运行中真正折腾过人的几个坑一次性讲透。
2. 先立框架:数据库表设计和三层结构怎么搭才不翻车
2.1 把机票业务拆成三层:C逻辑层、QT界面层、MySQL存储层
模拟系统最容易犯的第一个错,是打开Qt Creator就直接拖控件,写着写着一个按钮的槽函数里塞了三百行SQL。我习惯先拿一张纸把边界画清楚:
- 界面层(QT Widgets):负责显示航班列表、接收用户输入、弹提示框,不直接拼SQL语句。
- 业务逻辑层(LinuxC,也就是在Linux环境下用C写的库或源文件):负责处理查航班、扣余票、创建订单这些规则的“判断”。比如余票够不够扣、下单后返回什么错误码,都是这一层说了算。
- 存储层(MySQL):负责数据持久化和并发控制。航班表、订单表、用户表都在这边。
这么分层最直接的好处是替答辩备好了话术:老师问“你代码怎么组织的”,你回答“界面和业务分离,业务层拿着数据库连接去操作,界面只认结果回调”,这一句话就能把“管理系统”和“模拟系统”拉开档次。另一个好处是调试友好,C语言里可以用fprintf打日志、printf输出错误码,QT界面崩了不用跟UI死磕,先怀疑业务层接口没返回正确值。
在LinuxC这个环节,我指的不是纯C++,而是用C语言(gcc直接编译)来写数据库访问和业务逻辑模块,再由QT的C++层通过extern "C"或者直接include头文件调用。很多课程设计项目都是这种混编路线——核心逻辑全用结构化C,界面交给QT,代码量可控,也足够讲清楚“系统编程”的味道。
2.2 建库建表:航班、订单、用户三张核心表的字段与索引设计
数据库设计是这个题目的地基。航班表管“有哪些票可卖”,订单表管“谁买了哪张票”,用户表管“谁在登录”。我的建议是至少建这三张,别图省事只建一张表塞所有字段,后面写SQL联查时会非常痛苦。
先看航班表的核心建表语句:
CREATE TABLE IF NOT EXISTS `flight` ( `flight_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '航班内部ID', `flight_no` VARCHAR(10) NOT NULL COMMENT '航班号,如CZ3101', `dep_city` VARCHAR(20) NOT NULL COMMENT '出发城市', `arr_city` VARCHAR(20) NOT NULL COMMENT '到达城市', `dep_time` DATETIME NOT NULL COMMENT '计划起飞时间', `arr_time` DATETIME NOT NULL COMMENT '计划到达时间', `price` DECIMAL(10,2) NOT NULL COMMENT '经济舱票价', `remain_cnt` INT UNSIGNED NOT NULL DEFAULT 100 COMMENT '剩余票数', PRIMARY KEY (`flight_id`), KEY `idx_route` (`dep_city`, `arr_city`, `dep_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航班表';这里有三个参数选择要解释清楚。第一,dep_time用DATETIME而不是用DATE加一个单独的dep_time_str字符串列,因为日期范围查询走DATETIME才能命中索引,字符串比较虽然也能出结果但性能差、而且格式一乱就排序错。第二,remain_cnt用INT UNSIGNED并设置默认100,是防止SQL里出现负余票——如果某次扣减写错了,字段本身的无符号约束马上报错,比业务代码里if判断更早暴露问题。第三,ENGINE=InnoDB必须写,因为后面要依赖事务来保证扣余票和创建订单的一致性,MyISAM不支持行级事务,千万别默认用旧习惯建表。
订单表我保留了相对精简的字段:
CREATE TABLE IF NOT EXISTS `orders` ( `order_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `flight_id` INT UNSIGNED NOT NULL COMMENT '关联航班ID', `user_id` INT UNSIGNED NOT NULL COMMENT '下单用户ID', `order_sn` VARCHAR(32) NOT NULL COMMENT '订单号,程序生成', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已出票 2已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`order_id`), UNIQUE KEY `uk_sn` (`order_sn`), KEY `idx_user` (`user_id`), KEY `idx_flight` (`flight_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';order_sn加上唯一索引用处很大,因为模拟系统里可能同时有两个模拟客户端下单,时间戳加随机数生成的订单号万一重复,唯一索引能兜底;否则后面逻辑层还得再查一次有没有重复。create_time用DEFAULT CURRENT_TIMESTAMP,C代码里不用自己拼时间,省一顿格式化的麻烦。建完表之后可以用mysql workbench可视化确认表结构,也可以直接命令行source导入,二选一都行,但表名、字段名建议全小写加下划线,避免Linux环境下大小写敏感导致的“表不存在”。
2.3 连接池思路:LinuxC下托管MySQL连接,避免每次查询都握手
模拟系统的数据量不大,但既然标题里写了LinuxC+MySQL,连接池这块值得做出来,它是答辩时一个很加分的“非增删改查”亮点。每次调用mysql_real_connect去连一次MySQL服务,握手加认证的开销虽然单看不大,可在循环下单演示里体感明显,而且频繁短连接会让MySQL端TIME_WAIT涨得很难看。
常见做法是自己维护一个固定大小的连接数组,初始化时建好,使用时取一个空闲连接,用归还机制放回去。我一般用互斥锁保护池子:
/* src/db_pool.c —— 最小可用连接池 */ #define MAX_POOL 8 static MYSQL *pool[MAX_POOL]; static int pool_used[MAX_POOL]; static pthread_mutex_t pool_mutex = PTHREAD_MUTEX_INITIALIZER; int db_pool_init(const char *host, const char *user, const char *passwd, const char *dbname) { int i; pthread_mutex_lock(&pool_mutex); for (i = 0; i < MAX_POOL; i++) { pool[i] = mysql_init(NULL); if (mysql_real_connect(pool[i], host, user, passwd, dbname, 0, NULL, 0) == NULL) { fprintf(stderr, "pool init fail: %s\n", mysql_error(pool[i])); pthread_mutex_unlock(&pool_mutex); return -1; } pool_used[i] = 0; } pthread_mutex_unlock(&pool_mutex); return 0; } MYSQL *db_pool_get(void) { MYSQL *conn = NULL; int i; pthread_mutex_lock(&pool_mutex); for (i = 0; i < MAX_POOL; i++) { if (!pool_used[i]) { pool_used[i] = 1; conn = pool[i]; break; } } pthread_mutex_unlock(&pool_mutex); return conn; /* 返回NULL时上层自行决定等待或直接报错 */ }这段代码的逻辑是:初始化阶段一次性建立8条TCP连接并认证好,之后业务每次想操作数据库,只需要在池子里取一个“没被占用”的连接,用完归还,避免重复建连和销毁。两个参数值得注意:MAX_POOL设成8是照顾并发不高但可能要开线程轮询的模拟场景,太大浪费内存,太小会阻塞;mysql_init(NULL)在MySQL 8.0里也可以用,它为连接对象分配内存,之后再交给mysql_real_connect填充连接信息。
当然,真正的生产连接池还要处理连接失效后的重建、空闲超时回收,这里模拟系统做到“取连接、用连接、还连接”就够了。关键是让看代码的人明白你懂“连接复用”这回事。
3. 用 LinuxC 把核心票务逻辑跑通:余票扣减与订单事务
3.1 条件UPDATE代替“先查再扣”,让并发不超卖
“查余票、判断大于0、再UPDATE减一”是很多新手的第一直觉,但这是错的,坑就坑在“查”和“改”之间隔着时间窗口,两个客户端同时查到余票为1,然后各自去扣,最后超卖到负数。正确做法是把判断写进UPDATE的条件里,让数据库一行行锁帮你做原子操作:
/* src/ticket_order.c —— 扣减余票的核心函数 */ int deduct_remain(MYSQL *conn, int flight_id) { char sql[256] = {0}; int ret; snprintf(sql, sizeof(sql), "UPDATE flight SET remain_cnt = remain_cnt - 1 " "WHERE flight_id = %d AND remain_cnt > 0", flight_id); if (mysql_real_query(conn, sql, strlen(sql)) != 0) { fprintf(stderr, "update remain fail: %s\n", mysql_error(conn)); return -1; } ret = (int)mysql_affected_rows(conn); if (ret == 1) { return 0; /* 扣减成功,影响了一行 */ } return -2; /* 影响0行,说明余票已经没了 */ }这段代码的关键在于WHERE里带上了remain_cnt > 0。MySQL执行UPDATE时会对命中的行加锁,判断条件不满足就直接跳过,所以“判断余票大于0”和“减一”在数据库层面是一次完成的。调用mysql_affected_rows()看结果:返回1说明确实扣掉了一张票,返回0说明航班余票已经为0,此时上层可以直接提示“余票不足”,不用再查一遍。
这里有个经验:别在UPDATE之后再SELECT一次“剩余多少”再决定成不成功,多余的网络往返和数据不一致窗口完全没必要。如果确实要把最新余票展示给界面,在UPDATE成功后紧接着做一次SELECT读取即可——此时读到的就是扣减后的值。
3.2 事务边界:出票失败时订单和余票怎样一起回滚
余票扣了,但订单插入时外键失败、或者模拟支付环节返回失败,这单票不能变成“扣了票却没订单”,也不能变成“有订单但余票没扣”。所以下单过程必须包在一个事务里,先关掉自动提交,全部成功再COMMIT:
/* src/ticket_order.c —— 创建订单 + 扣余票,同一个事务完成 */ int create_order(MYSQL *conn, int flight_id, int user_id, const char *order_sn) { char sql[512] = {0}; mysql_autocommit(conn, 0); /* 第一步:扣余票 */ if (deduct_remain(conn, flight_id) != 0) { mysql_rollback(conn); mysql_autocommit(conn, 1); return -1; } /* 第二步:插入订单 */ snprintf(sql, sizeof(sql), "INSERT INTO orders (flight_id, user_id, order_sn, status) " "VALUES (%d, %d, '%s', 0)", flight_id, user_id, order_sn); if (mysql_real_query(conn, sql, strlen(sql)) != 0) { mysql_rollback(conn); mysql_autocommit(conn, 1); fprintf(stderr, "insert order fail: %s\n", mysql_error(conn)); return -2; } mysql_commit(conn); mysql_autocommit(conn, 1); return 0; }逻辑不复杂但边界要交代清楚:mysql_autocommit(conn, 0)之后,同一条连接里所有SQL都进入同一个事务,只有mysql_commit(conn)才会真正落盘。任何一个步骤出错就mysql_rollback(conn),把这一步的余票扣减或订单插入全部撤销,最后记得把autocommit恢复成1,否则这条连接后续的查询全都在隐式事务里,锅很难查。
事务隔离级别用默认的REPEATABLE READ就够,不需要动。真正要小心的是:事务里的SELECT、UPDATE一定要走主键或者索引,否则会锁更多的行,模拟系统里明明只想锁一行,结果把整张flight表都锁了,另一个窗口查航班直接卡住——这个问题我第一次跑并发被测出来时非常懵,后来习惯性地给WHERE条件字段都建了索引,再没复发。
3.3 查询航班列表:多表联查与日期边界
航班查询在界面上通常表现为“输入出发城市、到达城市、出发日期,点查询”。SQL层面最直接是查flight表按航线过滤:
/* src/flight_query.c —— 查询可用航班列表 */ int query_flights(MYSQL *conn, const char *dep_city, const char *arr_city, const char *dep_date) { char sql[512] = {0}; char dep_begin[32] = {0}, dep_end[32] = {0}; /* 把用户选的日期转成:当天00:00:00 到 次日00:00:00 */ snprintf(dep_begin, sizeof(dep_begin), "%s 00:00:00", dep_date); snprintf(dep_end, sizeof(dep_end), "%s 23:59:59", dep_date); snprintf(sql, sizeof(sql), "SELECT flight_id, flight_no, dep_city, arr_city, " " dep_time, arr_time, price, remain_cnt " "FROM flight " "WHERE dep_city = '%s' AND arr_city = '%s' " " AND dep_time BETWEEN '%s' AND '%s' " "ORDER BY dep_time ASC", dep_city, arr_city, dep_begin, dep_end); ... }这里为什么不用DATE(dep_time) = '2025-06-01'?因为一旦在WHERE里对dep_time列使用了DATE函数,MySQL的索引就失效了,全表扫描虽然模拟数据量小感觉不出来,但属于习惯性问题。我的做法是把用户传进来的日期拼成“当天零点”和“当天23:59:59”两个边界。更严谨的写法是左闭右开(BETWEEN dep_begin AND 次日00:00:00),但模拟系统里用23:59:59可读性更好,前提是数据量小、没有刚好压在边界上的航班。
另外值得提一句:有同学喜欢把扣余票的SQL写进MySQL存储过程,让应用层只CALL一下。这种做法能让事务完全待在数据库里,确实少很多C代码,但调试时存储过程里printf不出来,错误信息要靠SELECT封装,课程设计还是建议把逻辑写在C里,界面层可以直接拿到错误码和错误描述。
4. 用 QT 把模拟系统画出界面:Qt Designer、信号槽与线程
4.1 在 Qt Creator 里搭主窗口:查询区、结果表、下单按钮
界面部分依赖QT,我用的是QT 5.15.2这个常规版本,下载安装后直接用Qt Creator打开工程。创建工程时选“Qt Widgets Application”,基类选QMainWindow,会生成一个mainwindow.cpp和mainwindow.h,后面在mainwindow.cpp里通过ui指针访问控件。
打开工程后,用Qt Designer编辑mainwindow.ui,我习惯把控件命名成有业务含义的前缀,方便后面信号槽代码阅读,否则默认的pushButton_3写多了自己都分不清谁是谁。常用控件和命名如下:
| 控件类型 | 对象名 | 用途 |
|---|---|---|
| QComboBox | cb_from | 下拉选择出发城市 |
| QComboBox | cb_to | 下拉选择到达城市 |
| QDateEdit | de_depart | 选择出发日期,默认今天 |
| QPushButton | btn_query | 触发航班查询 |
| QTableWidget | tbl_flights | 展示航班列表 |
| QPushButton | btn_order | 选中航班后下单 |
| QStatusBar | statusBar | 显示操作结果信息 |
QDateEdit要设置setCalendarPopup(true),这样弹日历选日期,体验更接近真实售票系统;同时设置最小日期为今天,防止用户选过去日期。两个QComboBox的条目,可以直接在Designer里手工填几个城市,也可以在mainwindow构造函数中用ui->cb_from->addItem("北京")批量加。我推荐后者,因为城市列表应该和数据库里的航线数据保持一致,手工填很容易出现“界面有上海,数据库却没有上海出发的航班”这种低级BUG。
4.2 信号槽写法:界面按钮怎么把请求交给C业务线程
QT里最核心的机制是信号槽。按钮点击、表格双击都会发出信号,槽函数负责响应。模拟系统里我建议让槽函数只干“取界面参数、调C业务函数、把结果填回去”三件事,不要在槽函数里写复杂SQL。
// mainwindow.cpp —— 查询按钮槽函数 void MainWindow::on_btn_query_clicked() { QString fromCity = ui->cb_from->currentText().trimmed(); QString toCity = ui->cb_to->currentText().trimmed(); QDate departDate = ui->de_depart->date(); if (fromCity.isEmpty() || toCity.isEmpty()) { QMessageBox::warning(this, "提示", "请选择出发和到达城市"); return; } if (fromCity == toCity) { QMessageBox::warning(this, "提示", "出发城市不能等于到达城市"); return; } // 调用C函数查询航班列表,fmtDate转成数据库需要的 "yyyy-MM-dd" QString dateStr = departDate.toString("yyyy-MM-dd"); ... }槽函数开头的两个if是模拟系统很需要的“防呆设计”,真实系统里空城市、同城出发都是非法请求,提前拦下比等SQL返回错误再弹窗友好得多。currentText()从下拉框拿字符串,date().toString("yyyy-MM-dd")是QT里标准的日期格式化方式,注意这句不会受系统locale影响,格式永远是四位年份加两位月日,正好喂给MySQL的DATETIME字段。
连接方式可以直接在Designer里点“转到槽”自动生成on_btn_query_clicked(),也可以手动connect。我习惯手动connect,因为代码里能顺手把业务线程的调用方式写清楚:
// mainwindow.cpp 构造函数中 connect(ui->btn_query, &QPushButton::clicked, this, &MainWindow::on_btn_query_clicked);4.3 用 QTableWidget 展示航班结果,列表双击触发下单
查询结果返回后,用一个表格把所有航班列出来。QTableWidget适合这种“固定列数、行数动态”的小数据量场景,不用自己写Model,代码量最少:
// 用查询到的航班数据填充QTableWidget void MainWindow::fillFlightTable(const QList<FlightInfo>& flights) { ui->tbl_flights->clearContents(); ui->tbl_flights->setRowCount(flights.size()); ui->tbl_flights->setColumnCount(7); QStringList headers = {"航班号", "出发", "到达", "起飞时间", "到达时间", "票价", "余票"}; ui->tbl_flights->setHorizontalHeaderLabels(headers); for (int row = 0; row < flights.size(); ++row) { const FlightInfo& f = flights.at(row); ui->tbl_flights->setItem(row, 0, new QTableWidgetItem(f.flightNo)); ui->tbl_flights->setItem(row, 1, new QTableWidgetItem(f.depCity)); ui->tbl_flights->setItem(row, 2, new QTableWidgetItem(f.arrCity)); ... // 把航班ID藏进单元格的自定义数据里,下单时取出来用 QTableWidgetItem* idItem = new QTableWidgetItem(f.flightNo); idItem->setData(Qt::UserRole, f.flightId); } }Qt::UserRole是这里的关键,它是QT给开发者预留的“自定义数据槽”,把数据库里的主键flight_id以数字形式挂到界面项上,用户点这行时就不用通过解析字符串拿ID,直接从item里取:
void MainWindow::on_tbl_flights_cellDoubleClicked(int row, int column) { QTableWidgetItem* item = ui->tbl_flights->item(row, 0); if (!item) return; int flightId = item->data(Qt::UserRole).toInt(); // 继续调下单接口 ... }QTableWidget默认双击进入编辑状态,要顺手setEditTriggers(QAbstractItemView::NoEditTriggers)关掉,否则用户双击本来想下单,结果表格先变编辑框,体验很怪。
4.4 把数据库操作挪进QT工作线程,别卡死界面
查询和下单都涉及MySQL网络交互,如果直接在槽函数里同步调用,MySQL响应慢时整个界面会“假死”。模拟系统里数据量不大,但演示时如果数据库服务刚重启,连接池还没热起来,第一次点击查询就可能卡一两秒。更好的做法是开一个QThread子类跑数据库操作。
// flightworker.h —— 工作线程 class FlightWorker : public QThread { Q_OBJECT public: explicit FlightWorker(MYSQL* conn, QObject* parent = nullptr); void run() override; signals: void queryFinished(const QList<FlightInfo>& flights); void queryError(const QString& err); private: MYSQL* m_conn; QString m_depCity; QString m_arrCity; QString m_date; };run()函数里执行真正阻塞的C查询函数,查询完通过emit queryFinished(flights)把结果传回主线程。这里强调一下:QT里跨线程传自定义结构体,需要在类里用Q_DECLARE_METATYPE(FlightInfo)声明一下,否则排队信号时Qt不知道如何帮你拷贝这个参数。我用的是QThread::run这种直接写法,简单直观;进阶的moveToThread方案更标准,但对模拟系统学习成本偏高,属于锦上添花的优化项。
5. 踩坑记录:模拟系统编译和运行时的 5 个高频翻车点
5.1 cannot mix incompatible Qt library:qmake路径串了
现象:编译时报fatal: cannot mix incompatible Qt library (version ex50601) with this library,或者运行时直接崩溃,提示Qt版本不匹配。我当时用的是QT 5.15.2,系统里还残留着一个老版本QT,qmake指向了老版本,但链接器找到了新版本库。
原因:环境变量里的PATH和QTDIR没对应上,Qt Creator用的是自带套件时没问题,但在终端里手动qmake、make时,系统PATH里先找到了旧版qmake,生成Makefile后按旧版路径去头文件和库文件,于是新旧头文件、库文件混在一起。解决:先把PATH里QT相关路径检查清楚,which qmake确认用的是哪个版本,然后统一用Qt Creator自带的qmake路径;同时把之前生成的Makefile、.pro.user缓存删掉重新qmake。血泪经验:不要在同一个环境里装两套大版本QT,老项目指定版本前先备份环境。
5.2 could not find the qt platform plugin "linuxfb":无显示环境跑QT
现象:在纯命令行Linux服务器上运行编译好的QT程序,报qt.qpa.plugin: Could not find the qt platform plugin "linuxfb",然后程序退出。
原因:QT默认走xcb插件,需要X Window显示环境;服务器或远程终端没有X服务,QT找不到合适的“平台插件”。解决分两种场景。如果是本地有桌面的Linux,装libxcb相关库,或者用export QT_QPA_PLATFORM=offscreen让QT在“虚拟显示”下运行,适合跑自动化测试;如果是在嵌入式开发板上跑,需要保留linuxfb插件。最简单快速验证程序能起来的办法是QT_QPA_PLATFORM=offscreen ./main,界面不显示但逻辑照样跑,日志能出来。
5.3 ERROR 2002:MySQL服务没起或socket路径不对
现象:程序连数据库时报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。
原因:看到“localhost”时MySQL客户端默认走Unix socket文件而不走TCP,如果mysqld没启动、或者socket文件不在默认路径,就会报这个错。解决:先systemctl status mysql确认服务在跑;再查/etc/mysql/my.cnf里的socket路径是否一致。最简单稳妥的验证方式是把程序里的host从localhost改成127.0.0.1,强制走TCP连接,绕开socket路径问题,代价是多一点网络开销,对模拟系统完全无所谓。这是我认为“模拟系统最值得背下来的一句经验”——先排查服务,再排查socket,再排查防火墙。
5.4 超卖问题:同一时段两个客户端抢到最后一张票
现象:并发测试时两个客户端都显示“下单成功”,但航班余票变成负数;或者显示成功后查订单,发现两张订单指向同一张票。原因:没有使用条件UPDATE,而是“先SELECT余票数、再UPDATE扣减”两步走,两个请求在SELECT阶段都看到余票为1,然后各自执行UPDATE,自然超卖。解决:回到3.1节的条件UPDATE写法,把remain_cnt > 0放进WHERE,并检查mysql_affected_rows是否为1。这个坑可以说覆盖了所有“模拟库存/票务”类系统的核心,答辩时主动说出来,老师会知道你理解并发下的原子性问题。
5.5 中文乱码和日期格式化:utf8mb4与连接字符集
现象:QT界面上显示的城市名变成了???,或者MySQL里存进去的中文读出来乱码。原因:建表时没用utf8mb4、连接建立后没设置客户端字符集。解决分两步:建表统一带DEFAULT CHARSET=utf8mb4;C代码在连接建立后立刻执行一次SET NAMES utf8mb4。QT方面,从MySQL取出的字符串是UTF-8,直接用QString::fromUtf8包一层再放进界面控件。遇到过最隐蔽的情况是:命令行下mysql查表中文正常,但程序读出来乱码——这就是连接字符集没设,各管各的,程序客户端和数据库服务器用了不同字符集。
6. 把模拟系统做成“能答辩的毕业设计”:日志、状态机与最后的验证
前面五章讲的是“跑得通”,第六章我想说说“怎么让它更像一个真正能交差的成品”。第一个建议是给订单加状态机。不要只有一张订单表没有状态流转,至少要有“待支付、已出票、已取消”三个状态,界面上下单后弹一个“模拟支付”确认框,确认后把status从0改成1;如果C业务函数返回扣票失败,则把状态置为2。状态机让系统有了“业务过程”,而不是一个静态列表。
第二个建议是加上日志模块。模拟系统的黑匣子是所有人都头疼的事,我在业务层每个关键函数入口用fprintf写一行带时间戳的记录到logs/ticket.log,比如2025-06-01 10:23:11 flight_id=5 order_sn=T202506011023 cancel。答辩现场无论演示什么功能,出了问题都可以打开日志说明“这里发生了什么”,这比你在台上口述“我刚才点了下单”有说服力得多。
交付前我会做三件事:第一,用valgrind跑一遍程序,valgrind --leak-check=full ./main,把“definitely lost”清零,这是给老师看程序质量的狠招;第二,写一个demo.sql或reset.sql,把三张表清空并插入几组固定航班数据,每次答辩演示前重置一遍,防止演示现场数据被自己改乱;第三,在QT里把编译模式从Debug切到Release,Release版体积小、运行快、不像Debug版那样依赖一堆调试库,在别人电脑上演示时少翻一个“缺DLL”的坑。
我自己的习惯是每完成一个模块就完整跑一遍“查询→下单→看余票”的闭环,哪怕只改了一个变量名。这比最后统一测试时一次性面对十几个报错要舒服得多,也是我这么多年踩坑踩出来的止血办法。希望这些内容能帮到你,把你自己的机票售票模拟系统从“能编译”做成“能讲清楚、能扛住追问、能拿出来演示”的完整毕业设计。
本文还有配套的精品资源,点击获取