简介:这份资源是面向计算机相关专业在校学生与教师的Qt教务系统完整项目源码,采用C++与Qt框架开发,后端使用MySQL数据库,支持学生、教师、管理员三种身份登录与权限区分,可作为毕业设计、课程设计或大作业的参考实现。压缩包共34个文件,约95KB,包含9个cpp源文件、8个h头文件与7个ui界面文件,另有sql建库脚本、qrc资源文件、pro工程文件及README说明,覆盖登录、主窗口、学生页面、教师页面、管理员页面等核心模块,结构清晰便于二次开发。目前已有87人学习下载。项目代码经过实际运行测试,答辩评审平均分达96分,读者可据此快速理解Qt界面与MySQL交互的完整实现思路,并在此基础上修改扩展功能,适合小白进阶学习,也可用于项目初期立项演示。
1. 从一份毕设题目说起:Qt + MySQL 的教务系统到底难在哪
每年毕业季,基于 Qt 的教务系统设计与实现都是高频选题,配套 MySQL 数据库、学生/教师/管理员三种身份,看起来需求清晰、技术栈成熟,真动手才发现坑全在细节里。我带过几届本科毕设,也帮同事做过课程设计评审,最常见的翻车不是界面画不出来,而是权限模型没设计好、数据库连接在打包后失效、事务边界模糊导致选课超卖。这篇笔记就按一线做法,把 Qt 客户端 + MySQL 服务端这套组合从建库、连库、分角色登录到选课事务、打包部署完整走一遍,给出可直接抄的 SQL、C++ 代码和参数配置。适合正在做同类毕设的学生,也适合需要快速搭一个桌面端管理系统的开发者。读完你能拿到一套可复现的最小骨架,知道哪些参数必须改、哪些坑提前绕。
2. 先把数据库立住:教务系统的表结构与 MySQL 8.0 建库实操
2.1 三张核心表怎么切分身份与权限
教务系统的数据模型不复杂,但表切分方式直接决定后面代码好不好写。我一般把身份信息、角色权限、业务数据分开:user表只存账号密码和身份枚举,student、teacher表存各自扩展字段,course、sc(选课关系)承载业务。这样学生和教师共用一个登录入口,登录后按role字段跳不同主界面,避免三套登录逻辑。
角色枚举用 tinyint:1 学生、2 教师、3 管理员。不要用字符串存角色,后面 WHERE 查询和索引效率差很多。密码字段至少 varchar(64),存 SHA-256 加盐后的值,别存明文——这是评审必查项。
-- 用户基础表:只放登录相关,三种身份共用 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL UNIQUE COMMENT '学号/工号/管理员账号', `password` VARCHAR(64) NOT NULL COMMENT 'SHA256加盐后的值', `role` TINYINT NOT NULL COMMENT '1学生 2教师 3管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生扩展表 CREATE TABLE `student` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `name` VARCHAR(32) NOT NULL, `major` VARCHAR(64), `class_name` VARCHAR(32), FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE `course` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `code` VARCHAR(16) NOT NULL UNIQUE, `name` VARCHAR(64) NOT NULL, `teacher_id` INT NOT NULL, `credit` DECIMAL(3,1) DEFAULT 2.0, `capacity` INT DEFAULT 60 COMMENT '容量上限', `selected` INT DEFAULT 0 COMMENT '已选人数', FOREIGN KEY (`teacher_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课关系表:联合唯一约束防重复选课 CREATE TABLE `sc` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_id` INT NOT NULL, `course_id` INT NOT NULL, `score` DECIMAL(4,1) DEFAULT NULL, `select_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_stu_course` (`student_id`, `course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:user表用role区分身份,student和teacher通过user_id外键关联,避免字段冗余。sc表的联合唯一索引uk_stu_course是防重复选课的第一道防线,比在应用层查一遍再插入可靠得多。course表里的selected字段是冗余计数,配合事务使用,后面会讲为什么不能只靠COUNT(*)。
参数说明:字符集统一utf8mb4,否则学生姓名里的生僻字会乱码;存储引擎必须 InnoDB,MyISAM 不支持事务和外键,选课场景直接废掉。capacity和selected用 INT 就够,别用 VARCHAR 存数字。
2.2 MySQL 8.0 在 Windows 上的安装与建库命令
热词里 mysql 安装教程、mysql 安装配置教程 出现频率极高,说明这一步确实卡人。我推荐用 MySQL 8.0 的 MSI 安装包,安装时勾选 MySQL Server 和 MySQL Workbench,服务名保持默认MySQL80。安装完第一件事是改认证插件,8.0 默认caching_sha2_password,老版本 Qt 驱动可能连不上。
# 登录 MySQL(安装时设的 root 密码) mysql -u root -p # 建库,字符集必须指定 CREATE DATABASE edu_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 建一个专用账号,别用 root 连应用 CREATE USER 'edu_app'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourStrongPass123'; GRANT ALL PRIVILEGES ON edu_system.* TO 'edu_app'@'localhost'; FLUSH PRIVILEGES;逻辑说明:单独建edu_app账号而不是用 root,一是安全,二是打包后配置文件里写 root 密码是评审扣分点。IDENTIFIED WITH mysql_native_password显式指定认证插件,兼容性最好。GRANT只给edu_system库的权限,最小权限原则。
参数说明:utf8mb4_general_ci排序规则对中文够用,追求精确可用utf8mb4_unicode_ci,性能略低。密码策略如果报错,是 8.0 默认的validate_password插件要求大小写数字符号齐全,改复杂点即可。
3. Qt 端连库与分角色登录:QSqlDatabase 配置和界面跳转
3.1 Qt 5.15 连接 MySQL 的驱动编译与连接参数
Qt 默认不带 MySQL 驱动,这是新手第一个大坑。Qt 5.15.2 MSVC 版本自带的qsqlmysql.dll往往缺失或版本不匹配,报错QMYSQL driver not loaded。解决办法是用 Qt 自带的qmake重新编译驱动,源码在Qt\5.15.2\Src\qtbase\src\plugins\sqldrivers\mysql。
# 进入驱动源码目录,用对应版本的 qmake 生成 Makefile cd C:\Qt\5.15.2\Src\qtbase\src\plugins\sqldrivers\mysql C:\Qt\5.15.2\msvc2019_64\bin\qmake.exe "INCLUDEPATH+=C:\mysql-8.0\include" "LIBS+=C:\mysql-8.0\lib\libmysql.lib" mysql.pro nmake nmake install逻辑说明:INCLUDEPATH指向 MySQL 的 include 目录,LIBS指向libmysql.lib,编译产物qsqlmysql.dll会装到 Qt 的 plugins/sqldrivers 目录。编译前确认 MySQL 装的是完整版而非仅 Connector,否则找不到头文件。
参数说明:msvc2019_64要换成你实际的编译器目录;MinGW 版本用mingw32-make。编译成功后把libmysql.dll拷到 exe 同目录,否则运行时报找不到动态库。
连接代码封装成一个单例,避免多处重复打开连接:
// dbmanager.h #include <QSqlDatabase> #include <QSqlError> #include <QDebug> class DbManager { public: static bool init() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("edu_system"); db.setUserName("edu_app"); db.setPassword("YourStrongPass123"); if (!db.open()) { qDebug() << "DB open failed:" << db.lastError().text(); return false; } return true; } };逻辑说明:addDatabase("QMYSQL")指定驱动名,必须和编译出的驱动一致。连接失败时打印lastError().text(),常见错误Access denied是账号密码错,Can't connect是服务没起或端口错。
参数说明:setHostName用127.0.0.1而非localhost,避免某些环境下走 socket 连接失败。端口默认 3306,改过要同步。生产环境密码不要硬编码,放配置文件读取。
3.2 登录校验与三种身份的主界面跳转
登录逻辑核心是一条带role的查询,验证成功后按角色实例化不同窗口。密码校验在数据库端做,用SHA2(CONCAT(password, salt), 256)比对,避免把明文密码传到应用层。
// 登录按钮槽函数 void LoginDialog::onLoginClicked() { QString sql = "SELECT id, role FROM user " "WHERE username = ? AND password = SHA2(CONCAT(?, 'edu_salt'), 256)"; QSqlQuery query; query.prepare(sql); query.addBindValue(ui->editUser->text()); query.addBindValue(ui->editPwd->text()); if (!query.exec() || !query.next()) { QMessageBox::warning(this, "登录失败", "账号或密码错误"); return; } int uid = query.value("id").toInt(); int role = query.value("role").toInt(); QWidget *mainWin = nullptr; if (role == 1) mainWin = new StudentWindow(uid); else if (role == 2) mainWin = new TeacherWindow(uid); else mainWin = new AdminWindow(uid); mainWin->show(); this->close(); }逻辑说明:prepare+addBindValue是防 SQL 注入的标准写法,别用字符串拼接。SHA2(CONCAT(?, 'edu_salt'), 256)在数据库端加盐哈希,盐值edu_salt要和注册时一致。query.next()返回 false 说明没查到匹配记录。
参数说明:role用value("role")按列名取值比按索引稳,列顺序变了也不怕。三个主窗口构造函数传uid,后续查询该用户的数据直接用它,避免再查一次。
4. 选课事务与并发控制:把超卖和重复选课挡在数据库层
4.1 用事务 + 行锁实现不超卖的选课
选课是教务系统最容易翻车的功能。两个学生同时点选同一门只剩 1 个名额的课,如果先SELECT判断再UPDATE,中间有时间窗,必然超卖。正确做法是把判断和更新放进一个事务,用SELECT ... FOR UPDATE锁住课程行。
bool CourseService::selectCourse(int studentId, int courseId) { QSqlDatabase::database().transaction(); QSqlQuery q; // 锁定课程行,防止并发修改 q.prepare("SELECT capacity, selected FROM course WHERE id = ? FOR UPDATE"); q.addBindValue(courseId); if (!q.exec() || !q.next()) { QSqlDatabase::database().rollback(); return false; } int capacity = q.value("capacity").toInt(); int selected = q.value("selected").toInt(); if (selected >= capacity) { QSqlDatabase::database().rollback(); return false; // 名额已满 } // 插入选课记录,唯一索引兜底防重复 q.prepare("INSERT INTO sc(student_id, course_id) VALUES(?, ?)"); q.addBindValue(studentId); q.addBindValue(courseId); if (!q.exec()) { QSqlDatabase::database().rollback(); return false; // 重复选课触发唯一约束 } // 更新已选人数 q.prepare("UPDATE course SET selected = selected + 1 WHERE id = ?"); q.addBindValue(courseId); q.exec(); return QSqlDatabase::database().commit(); }逻辑说明:transaction()开启事务,FOR UPDATE对课程行加排他锁,其他事务在这一行上会阻塞直到本事务提交,彻底消除时间窗。插入sc表时如果重复,唯一索引uk_stu_course会报错,exec()返回 false,回滚即可。最后commit()提交,任何一步失败都rollback()。
参数说明:FOR UPDATE只在事务内有效,别在自动提交模式下用。锁的粒度是行级,InnoDB 才有,MyISAM 会锁表。selected字段的更新必须在同一事务内,否则计数和实际记录不一致。
4.2 退课与成绩录入的事务边界
退课逻辑和选课对称:删sc记录、selected减 1,同样放事务里。成绩录入是教师端功能,UPDATE sc SET score = ? WHERE student_id = ? AND course_id = ?,单条更新不需要显式事务,但批量录入时建议包一层,避免录一半失败。
-- 退课:先删记录再减计数,顺序不能反 START TRANSACTION; DELETE FROM sc WHERE student_id = 1001 AND course_id = 5; UPDATE course SET selected = selected - 1 WHERE id = 5 AND selected > 0; COMMIT;逻辑说明:selected > 0是防御条件,防止计数被减成负数。先删sc再减计数,如果中途失败回滚,两边都还原。批量成绩录入用START TRANSACTION包住多条 UPDATE,全部成功再 COMMIT。
参数说明:selected > 0这个条件在正常流程下永远成立,但数据异常时能兜底。成绩字段允许 NULL 表示未录入,查询时用IS NULL判断。
5. 避坑与排查:Qt + MySQL 教务系统最常见的 5 个翻车点
5.1 驱动加载失败:QMYSQL driver not loaded
现象:程序启动连库时报QMYSQL driver not loaded,QSqlDatabase::drivers()里没有 QMYSQL。原因:Qt 没编译 MySQL 驱动,或libmysql.dll不在 exe 同目录。解决:按 3.1 节重新编译驱动,把qsqlmysql.dll放进plugins/sqldrivers,libmysql.dll放进 exe 目录。用windeployqt打包时它不会自动带这两个 dll,要手动拷。
5.2 中文乱码:数据库、连接、界面三处字符集不一致
现象:学生姓名显示成问号或方块。原因:建库时没指定utf8mb4,或连接没设字符集。解决:建库语句带DEFAULT CHARACTER SET utf8mb4;连接后执行SET NAMES utf8mb4;Qt 源码文件保存为 UTF-8 带 BOM,QString转QByteArray时用toUtf8()。三处都对齐才不乱码。
5.3 选课超卖:先查后改的时间窗
现象:课程容量 60,实际选了 62 人。原因:应用层先SELECT判断再UPDATE,并发下判断失效。解决:用 4.1 节的事务 +FOR UPDATE,把判断和更新锁在同一事务里。这是血泪经验,评审老师最爱问这个点。
5.4 打包后连不上库:配置文件路径和相对路径
现象:开发机能跑,拷到别人电脑报连接失败。原因:数据库地址写死localhost,或配置文件用了相对路径找不到。解决:数据库地址放config.ini,用QCoreApplication::applicationDirPath()拼绝对路径读取;目标机器要装 MySQL 或改成连远程库。打包用windeployqt后手动补sqldrivers目录和libmysql.dll。
5.5 密码明文存储:评审必查的安全项
现象:数据库里password字段是明文。原因:图省事直接存了输入值。解决:注册时SHA2(CONCAT(密码, 盐), 256)存哈希,登录时同样计算比对。盐值固定字符串即可,毕设级别够用。别用 MD5,已被淘汰,评审看到会扣分。
6. 进阶技巧:用视图和存储过程把统计查询从 Qt 端下沉到数据库
做到基本功能后,成绩统计、选课名单导出这类查询会让 Qt 端代码越来越臃肿。我的习惯是把复杂统计下沉到数据库,用视图和存储过程封装,Qt 端只调一条语句。比如教师端要查某门课的选课名单和平均分,建一个视图:
CREATE VIEW v_course_stat AS SELECT c.id AS course_id, c.name AS course_name, COUNT(sc.student_id) AS stu_count, AVG(sc.score) AS avg_score FROM course c LEFT JOIN sc ON c.id = sc.course_id GROUP BY c.id, c.name;逻辑说明:视图把聚合逻辑固化,Qt 端SELECT * FROM v_course_stat WHERE course_id = ?即可,不用在 C++ 里拼 GROUP BY。LEFT JOIN保证没人选的课也显示,AVG自动忽略 NULL 成绩。
参数说明:视图不存数据,每次查询实时计算,数据量大时考虑物化到统计表。AVG(sc.score)对未录入成绩的记录返回 NULL,界面显示时判断一下。
再进一步,把选课和退课写成存储过程,Qt 端用CALL调用,事务逻辑全在数据库层,客户端代码更薄:
DELIMITER // CREATE PROCEDURE sp_select_course(IN p_stu INT, IN p_course INT, OUT p_result INT) BEGIN DECLARE v_cap INT; DECLARE v_sel INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result = -1; END; START TRANSACTION; SELECT capacity, selected INTO v_cap, v_sel FROM course WHERE id = p_course FOR UPDATE; IF v_sel >= v_cap THEN SET p_result = 0; ROLLBACK; ELSE INSERT INTO sc(student_id, course_id) VALUES(p_stu, p_course); UPDATE course SET selected = selected + 1 WHERE id = p_course; COMMIT; SET p_result = 1; END IF; END // DELIMITER ;逻辑说明:EXIT HANDLER捕获任何 SQL 异常统一回滚,p_result返回 1 成功、0 满员、-1 异常。Qt 端CALL sp_select_course(?, ?, @r); SELECT @r;拿结果。这样并发控制和业务规则都在数据库,客户端换语言也不影响。
参数说明:DELIMITER //是 MySQL 客户端语法,改分隔符让存储过程内的分号不被提前解析。OUT参数用会话变量@r接收。存储过程调试麻烦,建议先在 Workbench 里跑通再集成。
我自己的习惯是:凡是涉及多表写入和并发判断的逻辑,一律下沉到存储过程或至少包事务,Qt 端只负责界面和参数收集。这样出问题时排查范围小,数据库层有日志可查,比在 C++ 里追事务状态轻松得多。毕设答辩时能讲清楚事务隔离级别和锁机制,基本就稳了。希望帮到你。
本文还有配套的精品资源,点击获取