Qt+MySQL教务系统毕业设计:从数据库设计到驱动避坑全指南
发布时间:2026/9/25 7:15:38来源:尧图网络
简介这是一套基于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.dllWindows或 libqsqlmysql.soLinux。插件缺失的话重新安装对应版本的 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_RECONNECT1); if (!db.open()) { qDebug() db.lastError().text(); return 1; } QSqlQuery query(db); query.exec(SET NAMES utf8mb4);参数说明MYSQL_OPT_RECONNECT1让连接断开后自动重连毕业设计单人操作够用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 局部变量初始化单例C11 之后是线程安全的。单人开发的教务系统可以接受一个固定连接如果以后要改成一个连接池只需要替换这个类的内部实现窗口里的调用点基本不用改。这也是熟手常说的“把数据库访问收敛到一层”。需要澄清一点单例只在主线程执行 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 教师职称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段我建议存 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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_user127.0.0.1 IDENTIFIED WITH mysql_native_password BY 123456; GRANT ALL PRIVILEGES ON edu_sys.* TO edu_user127.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): Cant connect to local MySQL server through socket /tmp/mysql.sock。原因QMYSQL 驱动在 host 为空或写成localhost时走 unix socket而程序运行环境里摸不到那个 socket 文件另一种可能是 MySQL 只监听在 127.0.0.1 的 TCPsocket 路径本身也不对。解决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-8MSVC 编译时在 pro 文件加QMAKE_CXXFLAGS /utf-8数据库连接打开后立刻执行SET NAMES utf8mb4建库建表用DEFAULT CHARSETutf8mb4。如果还乱码把 MySQL 的character_set_client、character_set_connection、character_set_results三条系统变量打出来看。记住一个原则字符集每一步都要显式指定不要依赖默认值。5.5 发布后找不到 MySQLqsqlmysql 和 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 再继续。发布目录里的依赖库问题检查一遍胜过现场调试。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网