简介:这是一套基于Qt框架与MySQL数据库的教务系统完整源码,包含学生、教师、管理员三种身份模块,覆盖课程管理、成绩录入与查询、用户权限区分等典型业务场景,面向计算机相关专业学生开展课程设计、毕业设计或项目初期演示使用,也适合有一定C++基础的开发者参考学习。压缩包共34个文件,以cpp源文件、h头文件、ui界面文件为主,辅以sql数据库脚本、qrc资源文件、README说明文档及少量图片素材,整体约95KB,结构清晰便于导入Qt Creator后直接运行与二次修改。已有86人学习下载。代码经测试运行成功,答辩评审平均分达96分,内附数据库初始化脚本与项目配置文件,配合README说明即可快速搭建演示环境,也可在此基础扩展其他教务功能。
1. 基于 Qt 的教务系统毕业设计:先想清楚它是个数据库项目,不是界面项目
“基于Qt的教务系统设计与实现,用的MySQL数据库,身份有学生、教师和管理员”,这个标题覆盖了一个典型桌面教务系统的全部要素。很多同学拿到题目先画界面,画完发现登录、权限、选课、成绩录入全在数据库层,界面只是壳。这个方向适合C++基础一般、想快速做出完整系统的毕设,核心不在于界面多好看,而在于三张核心表和角色路由能不能讲清楚。建议先设计数据库,再写Qt,整个系统可以在一周内跑通。
2. 先定技术栈再动手:Qt 版本、MySQL 驱动和项目骨架
2.1 为什么选 Qt 5.15 + Widgets,而不是 QML 或 Qt 6
常见做法是直接选 Qt 5.15.2,这是目前教程资源最多的版本。教务系统里最常见的界面是登录表单、数据表格、下拉框和按钮,Qt Widgets 提供了一套最直接的控件,不需要 QML 动画,也没有 JavaScript 逻辑分层的负担。选 Widgets 还有一个实际好处:报错信息可搜,模型/视图的例子多,答辩现场即使被问到底层也能解释。
Qt 6 不是不能用,但 QMYSQL 驱动在 Qt 6 里通常需要自己编译,MySQL 8.0 默认认证插件对 Qt 5 的驱动兼容性处理起来也要额外配置。对毕业设计来说,稳定比新版本更有价值。安装时要注意勾选 Sources 和 MySQL 组件;如果安装包是 MinGW 版本,之后用 MinGW 编译器,MSVC 版本用 MSVC 编译器,千万不要混用。
这里我建议先做一个小验证:安装好后打开 Qt Creator,新建一个带 QSqlDatabase 的测试工程,先跑通数据库连接,再开始写业务代码。后续所有报错都围绕这个最小连接进行排查,比写到一半再查驱动省时间。
2.2 QMYSQL 驱动:连接前先检查驱动列表
Qt 连 MySQL 靠的是 QMYSQL 驱动插件。很多“Driver not loaded”的报错,不是因为代码写错,而是驱动插件根本没安装或没被识别。先跑一条最简单的代码:
#include <QSqlDatabase> #include <QDebug> int main(int argc, char *argv[]) { qDebug() << QSqlDatabase::drivers(); return 0; }如果输出里有“QMYSQL”,说明驱动插件可用;如果没有,检查安装目录下的 plugins/sqldrivers 是否存在 qsqlmysql.dll(Windows)或 libqsqlmysql.so(Linux)。插件缺失的话,重新安装对应版本的 Qt 并勾选 MySQL 组件,或者用 Qt 源码自行编译。自己编译不是必要路径,直接在安装器里勾选 MySQL 组件是最快的。
驱动确认后,写连接代码:
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("edu_sys"); db.setUserName("edu_user"); db.setPassword("123456"); db.setConnectOptions("MYSQL_OPT_RECONNECT=1"); if (!db.open()) { qDebug() << db.lastError().text(); return 1; } QSqlQuery query(db); query.exec("SET NAMES utf8mb4");参数说明:MYSQL_OPT_RECONNECT=1让连接断开后自动重连,毕业设计单人操作够用;setHostName写成127.0.0.1,不要写localhost,可以避免一部分走 socket 的歧义。用SET NAMES utf8mb4强制指定当前连接的字符集,比只靠配置文件更可控。
2.3 封装一个 Database 单例,别在窗口里重复连库
如果每个窗口都写一遍 addDatabase 和 db.open,一旦改了密码就要全局搜索替换,还容易造成连接泄露。我用一个简单的单例封装,把连接参数集中管理:
// database.h #ifndef DATABASE_H #define DATABASE_H #include <QSqlDatabase> class Database { public: static Database &instance(); QSqlDatabase db(); private: Database(); private: QSqlDatabase m_db; }; #endif// database.cpp #include "database.h" #include <QDebug> #include <QSqlError> Database &Database::instance() { static Database sing; return sing; } Database::Database() { m_db = QSqlDatabase::addDatabase("QMYSQL"); m_db.setHostName("127.0.0.1"); m_db.setPort(3306); m_db.setDatabaseName("edu_sys"); m_db.setUserName("edu_user"); m_db.setPassword("123456"); if (!m_db.open()) { qCritical() << "db open failed:" << m_db.lastError().text(); } } QSqlDatabase Database::db() { return m_db; }这里用 static 局部变量初始化单例,C++11 之后是线程安全的。单人开发的教务系统可以接受一个固定连接;如果以后要改成一个连接池,只需要替换这个类的内部实现,窗口里的调用点基本不用改。这也是熟手常说的“把数据库访问收敛到一层”。
需要澄清一点:单例只在主线程执行 UI 操作时足够。如果你开了 QThread 做后台导入导出,子线程里访问同一个 QSqlDatabase 连接对象会有问题,建议每个线程独立连接,这个边界要在代码注释里写清楚。
3. 数据库设计:三张核心表与权限落点
3.1 用户表:一张表放三种角色,还是三张表
常见做法是用一张 user 表,role 字段区分身份。用三张表的问题是:登录查询要判断三张表,账号唯一性也要维护三次,而且管理员和教师的部分字段重合。一张表加role TINYINT后,登录只查一次,Qt 端拿到 role 就知道该显示哪些菜单,这就是标题里“身份有学生、教师和管理员”的最小实现。
USE edu_sys; CREATE TABLE user ( uid INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 3 COMMENT '1学生 2教师 3管理员', real_name VARCHAR(50) NOT NULL DEFAULT '', class_name VARCHAR(50) DEFAULT '' COMMENT '学生班级,教师和管理员可留空', title VARCHAR(30) DEFAULT '' COMMENT '教师职称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码字段我建议存 SHA2 摘要而不是明文,答辩时被问到安全性也能答上来。生成摘要可以在 Qt 端用 QCryptographicHash,也可以用 MySQL 的 SHA2 函数:
INSERT INTO user (username, password, role, real_name) VALUES ('admin', SHA2('123456', 256), 3, '系统管理员');登录时比对:SELECT uid, role, real_name FROM user WHERE username=? AND password=?。这里用查出来再比对更好调试,但直接在 where 里带上条件也可以。我倾向于将密码字段单独查出来在 Qt 里比对,方便以后改成加盐哈希时不改动 SQL。
role 用整型比字符串好:界面切换菜单用 switch 更直观,存储空间小,也不会因为拼写不一致产生脏数据。如果后续要扩展更多角色,新增枚举值即可。
3.2 课程表与选课表:外键、唯一约束和余量控制
课程和选课是教务系统的核心关系。课程属于教师,选课连接学生和课程,成绩放在选课表里。这样设计的好处是:一门教师上多门课、一个学生选多门课,都是多对多关系,而 enrollment 是典型的关系表。
CREATE TABLE course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, capacity INT NOT NULL DEFAULT 60, selected_count INT NOT NULL DEFAULT 0, schedule VARCHAR(100) DEFAULT '' COMMENT '上课时间,如 周一3-4节', location VARCHAR(100) DEFAULT '', FOREIGN KEY (teacher_id) REFERENCES user(uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE enrollment ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL COMMENT '如 2025-2026-1', score DECIMAL(5,1) DEFAULT NULL COMMENT 'NULL 表示未录入', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0退课', UNIQUE KEY uq_student_course_sem (student_id, course_id, semester), FOREIGN KEY (student_id) REFERENCES user(uid), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;selected_count是冗余字段,用来快速显示“剩余名额”。这个字段一定要在事务里维护,否则并发选课会出现超选。一个常用的存储过程如下:
DELIMITER // CREATE PROCEDURE sp_enroll(IN stu_id INT, IN cid INT, IN sem VARCHAR(20)) BEGIN START TRANSACTION; SELECT selected_count, capacity FROM course WHERE course_id = cid FOR UPDATE; IF selected_count >= capacity THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'course is full'; END IF; UPDATE course SET selected_count = selected_count + 1 WHERE course_id = cid; INSERT INTO enrollment (student_id, course_id, semester, status) VALUES (stu_id, cid, sem, 1); COMMIT; END// DELIMITER ;FOR UPDATE在事务内锁住这一行,防止两个并发选课同时读到同一个余量。SIGNAL是 MySQL 5.5 之后可以用的存储过程错误抛出方式,Qt 端捕获到 QSqlError 后提示“人数已满”。退课则反向更新selected_count,并将 enrollment 的status置为 0,而不是物理删除,保留选课历史。
成绩字段用DECIMAL(5,1),NULL 表示未录入。教师端后续只更新这个字段,不需要额外设计成绩表。如果你喜欢用 MySQL Workbench 建表和观察数据,上面的 DDL 可以直接粘贴执行。
3.3 认证插件和存储过程调用:MySQL 8.0 连不上的常见根因
如果安装的是 MySQL 8.0,直接用 root 登录会让 Qt 5 的驱动踩坑。MySQL 8.0 默认的认证插件是caching_sha2_password,老版本的客户端库可能不识别,Qt 连接时报Authentication plugin 'caching_sha2_password' cannot be loaded。
常见做法是给程序建一个专用账号,使用兼容的认证方式:
CREATE USER 'edu_user'@'127.0.0.1' IDENTIFIED WITH mysql_native_password BY '123456'; GRANT ALL PRIVILEGES ON edu_sys.* TO 'edu_user'@'127.0.0.1'; FLUSH PRIVILEGES;这样把认证问题隔离在程序账号上,root 账号保持数据库默认设置,不会影响系统整体安全。
Qt 调用存储过程也很直接:
QSqlQuery q(Database::instance().db()); q.prepare("CALL sp_enroll(:stu, :cid, :sem)"); q.bindValue(":stu", 2023001); q.bindValue(":cid", 101); q.bindValue(":sem", "2025-2026-1"); if (!q.exec()) { qDebug() << q.lastError().text(); }注意 prepare 的存储过程占位符不带@符号,和 MySQL 客户端的CALL @stu写法不一样。Qt 端用 bindValue 替换命名占位符即可。
4. 在 Qt 里把功能串起来:登录路由、数据展示与成绩录入
4.1 登录:一次查询完成身份判断和主窗口路由
教务系统不管角色多复杂,入口都是登录。登录窗口拿到用户名和密码后,从 user 表查出 uid、role、real_name,再按 role 决定主窗口显示哪些菜单、隐藏哪些按钮。最省事的做法是登录成功后再 new 主窗口,并把用户信息塞给主窗口构造函数。
struct UserInfo { int uid; int role; QString realName; };void LoginWidget::onLogin() { QString username = ui->editUser->text().trimmed(); QString password = ui->editPass->text(); QSqlQuery q(Database::instance().db()); q.prepare("SELECT uid, password, role, real_name FROM user WHERE username = :un"); q.bindValue(":un", username); if (!q.exec() || !q.next()) { QMessageBox::warning(this, "登录", "用户不存在"); return; } if (q.value("password").toString() != password) { QMessageBox::warning(this, "登录", "密码错误"); return; } UserInfo u; u.uid = q.value("uid").toInt(); u.role = q.value("role").toInt(); u.realName = q.value("real_name").toString(); MainWindow *w = new MainWindow(u); w->show(); this->hide(); }这里用prepare + bindValue而不是把字符串直接拼进 SQL,是为了避免 SQL 注入。毕业设计里很多人图方便用"SELECT ... WHERE username='" + username + "'",只要用户名里带单引号就会出现各种诡异错误。prepare 这种方式在答辩时也是一个加分点。
主窗口拿到UserInfo后,根据 role 控制入口。传统 Widgets 里用QStackedWidget最直接,不同角色放不同的页面;也可以用setVisible控制菜单项:
MainWindow::MainWindow(const UserInfo &u) : ui(new Ui::MainWindow), m_u(u) { ui->setupUi(this); ui->stackedWidget->setCurrentIndex(0); switch (u.role) { case 1: // 学生 ui->btnGradeManage->setVisible(false); ui->btnUserManage->setVisible(false); break; case 2: // 教师 ui->btnCourseSelect->setVisible(false); ui->btnUserManage->setVisible(false); break; case 3: // 管理员 break; } loadDefaultPage(); }如果界面是动态菜单,就把权限表建进数据库,做成“角色-菜单”两张关联表;但毕业设计用页内可见性控制就足够,不是所有系统都要上权限表。
4.2 学生端:已选课程列表与退课操作
学生登录后要看到已选课程和成绩。这适合用QSqlQueryModel做只读表格:查出的是 join 后的视图,字段多但不需要编辑。
void StudentWidget::loadCourses(int studentId) { QSqlQueryModel *model = new QSqlQueryModel(this); QSqlQuery q(Database::instance().db()); q.prepare( "SELECT c.course_id, c.course_name, c.credit, c.schedule, e.score " "FROM enrollment e " "JOIN course c ON e.course_id = c.course_id " "WHERE e.student_id = :sid AND e.status = 1"); q.bindValue(":sid", studentId); q.exec(); model->setQuery(q); model->setHeaderData(1, Qt::Horizontal, "课程名"); model->setHeaderData(4, Qt::Horizontal, "成绩"); ui->viewCourse->setModel(model); }QSqlQueryModel传一个已经执行完的QSqlQuery进去,模型会把结果存下来。这里不能用QSqlTableModel直接 bind,容易把 SQL 拼接需求绕复杂。如果想要表头用中文,在setHeaderData里逐列设置即可。
退课操作可以理解为把 enrollment 的status置 0,并回减 course 的selected_count。用事务包装住两步:
void StudentWidget::onDropCourse(int courseId, int studentId) { QSqlDatabase db = Database::instance().db(); db.transaction(); QSqlQuery q(db); q.prepare("UPDATE enrollment SET status = 0 " "WHERE student_id = :sid AND course_id = :cid AND status = 1"); q.bindValue(":sid", studentId); q.bindValue(":cid", courseId); if (!q.exec() || q.numRowsAffected() != 1) { db.rollback(); QMessageBox::information(this, "退课", "该课程不在可选课列表中"); return; } QSqlQuery q2(db); q2.prepare("UPDATE course SET selected_count = selected_count - 1 " "WHERE course_id = :cid"); q2.bindValue(":cid", courseId); if (!q2.exec()) { db.rollback(); return; } db.commit(); }注意第二个语句如果失败,第一个语句的更新也要一起回滚。教务系统里这种“两步写”的操作不要拆成两段独立 SQL 跑,数据会越跑越乱。同一连接上连续执行多条语句时,事务才能让它们共用同一份回滚上下文。
4.3 教师端:用 QSqlTableModel 直接编辑成绩
教师录入成绩时,数据来源是 enrollment 和 course 的关联结果,但修改的只是 score 字段。这种场景用QSqlTableModel最省代码:绑定表、设置筛选、选择提交通道。
QSqlTableModel *model = new QSqlTableModel(this, Database::instance().db()); model->setTable("enrollment"); model->setFilter("course_id = 101"); model->setEditStrategy(QSqlTableModel::OnManualSubmit); model->select(); model->setHeaderData(1, Qt::Horizontal, "学号"); model->setHeaderData(2, Qt::Horizontal, "课程"); model->setHeaderData(6, Qt::Horizontal, "成绩"); ui->viewScore->setModel(model);editStrategy用OnManualSubmit,用户改完表格后点击“保存”按钮才统一提交,方便教师在保存前检查整张表。保存按钮里调用model->submitAll();如果某几行不符合约束,比如成绩范围超了,再调用model->revertAll()回滚全部修改。这里不要设成OnFieldChange,否则每改一格就写一次数据库,出现误输就不好撤。
管理员端和教师端逻辑是同一个套路:管理员要维护 user 表、course 表字段,同样可以用QSqlTableModel建表模型。区别只是字段可见性,比如管理员编辑用户表时应隐藏 password 列,或者干脆用单独的用户新增/编辑表单,避免密码字段裸奔在表格里。
5. 避坑:Qt + MySQL 教务系统最常见的 5 个翻车现场
5.1 qt.qpa.plugin 报错:平台插件缺失,不是代码问题
现象:在自己电脑上正常,拷贝到另一台机器或交叉编译环境,启动直接崩,命令行输出qt.qpa.plugin: could not find the qt platform plugin "linuxfb"或类似的 platform plugin 提示。
原因:程序没找到platforms目录里的平台插件。常见于直接把 exe 拷走没带上 plugins,或者多个 Qt 版本导致加载到不匹配的位置。linuxfb是 LCD 屏场景的平台插件,桌面环境通常用xcb,报错名称会随目标环境变化。
解决:Windows 上用官方自带部署工具生成依赖目录,把plugins/platforms一起带到目标机器;Linux 检查可执行文件旁是否放了plugins/platforms,必要时设置QT_QPA_PLATFORM_PLUGIN_PATH环境变量;树莓派交叉编译场景,需要为目标机单独放 qmake 生成的平台插件。跑一次 windeployqt 再启动,比手工拷 dll 省很多时间。
5.2 cannot mix incompatible Qt library:多版本 Qt 串库
现象:程序编译通过,运行时先打印cannot mix incompatible Qt library (version 0x50601) with this library,然后崩溃。
原因:链接器或加载器把程序引到了另一个 Qt 版本的 DLL/SO。常见是 PATH 里既有 Qt 5.15 又有 Qt 6,或开发机装了两个版本后环境变量写错。版本号里的0x50601对应 Qt 5.6.1,但可能与当前使用的库不一致。
解决:开发机只保留一个 Qt 版本的 bin 目录在 PATH 里。发布时用部署工具部署后,再确认 exe 旁边的 Qt5Widgets.dll 来源路径与编译时一致。如果是 Linux 环境,打出来ldd 可执行文件看 Qt 库路径。我一般会写一个环境清理脚本,把 QtCreator 自带的编译器环境单独加载,避免和系统里其他 Qt 冲突。
5.3 error 2002 (HY000):MySQL 连不上不全是网络问题
现象:命令行mysql -uroot -p能进,Qt 程序却报error 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。
原因:QMYSQL 驱动在 host 为空或写成localhost时走 unix socket,而程序运行环境里摸不到那个 socket 文件;另一种可能是 MySQL 只监听在 127.0.0.1 的 TCP,socket 路径本身也不对。
解决:Qt 连接参数里显式写127.0.0.1,端口3306;检查 MySQL 的bind-address和port配置,服务启动后再测。另一个隐藏坑是驱动插件没加载,报错提示看起来像网络问题,实际是Driver not loaded。把db.lastError().text()完整打出来再判断。MySQL 8.0 默认认证插件导致旧驱动报caching_sha2_password cannot be loaded时,需要建一个mysql_native_password账号给程序专用。
5.4 中文乱码:源码、编译选项、连接字符集三条链一起查
现象:界面显示正常,写入数据库的中文变成??或乱码,查出来再显示也是乱码。
原因:字符集链条任一处断裂:源码文件编码不是 UTF-8、MSVC 编译器对窄字符串的编码假设不对、连接字符集没设置、表字符集不一致。这是一个很容易被忽略的黑匣子。
解决:源码统一 UTF-8;MSVC 编译时在 pro 文件加QMAKE_CXXFLAGS += /utf-8;数据库连接打开后立刻执行SET NAMES utf8mb4;建库建表用DEFAULT CHARSET=utf8mb4。如果还乱码,把 MySQL 的character_set_client、character_set_connection、character_set_results三条系统变量打出来看。记住一个原则:字符集每一步都要显式指定,不要依赖默认值。
5.5 发布后找不到 MySQL:qsqlmysql 和 libmysql 都得带上
现象:用部署工具处理后程序能启动,一打开数据库就报Driver not loaded,或者启动后直接崩。
原因:部署工具经常漏掉 MySQL 驱动插件和客户端库。qsqlmysql.dll依赖libmysql.dll或libmariadb.dll,这个客户端库不在 exe 目录,驱动就加载失败。
解决:手动把 Qt 安装目录下plugins/sqldrivers/qsqlmysql.dll复制到发布目录的sqldrivers下,再把 MySQL Connector/C 里的libmysql.dll复制到 exe 同级目录。发布前跑一次qDebug() << QSqlDatabase::drivers(),看到 QMYSQL 再继续。如果发布机没装 MySQL 服务,连接地址写成127.0.0.1:3306,确保目标机器服务已启动。
6. 答辩前的验证顺序:让评委跟着你的演示脚本走
代码和数据库都跑通之后,最后一步不是写文档,而是编排演示顺序。我一般会准备一个demo.sql,重置系统数据,保证每次演示状态一致。演示顺序按业务流程走:管理员创建账号,教师创建课程,学生选课,教师录成绩,学生查成绩。
| 顺序 | 操作 | 验证点 |
|---|---|---|
| 1 | 管理员登录 | 进入管理员页面,能看到用户管理和课程管理 |
| 2 | 教师账号创建课程 | 课程列表出现新课程,余量正确 |
| 3 | 学生选课 | 选课成功后已选列表出现课程,余量减少 |
| 4 | 教师录入成绩 | 学生端成绩列显示分数 |
| 5 | 数据统计 | 管理员能查到选课人数统计 |
再给一个自检查询,检查selected_count和实际选课记录是否一致:
SELECT c.course_name, c.selected_count, c.capacity, COUNT(e.id) AS actual_count FROM course c LEFT JOIN enrollment e ON c.course_id = e.course_id AND e.status = 1 GROUP BY c.course_id;如果selected_count和actual_count不一致,说明某个事务路径没回滚,优先查退课和选课两个操作。答辩前一天,我会把 MySQL 服务、Qt Creator、发布目录全部清一遍,避免现场碰到服务未启动或 dll 缺失。换机器演示前,先跑一遍QSqlDatabase::drivers(),看到 QMYSQL 再继续。发布目录里的依赖库问题,检查一遍胜过现场调试。希望帮到你。
本文还有配套的精品资源,点击获取