简介这是一套面向计算机相关专业毕业设计场景的教务系统完整源码采用Qt框架开发、MySQL作为后台数据库区分学生、教师和管理员三种身份适合正在准备毕设或需要Qt桌面项目练手的学习者参考。压缩包共34个文件约97KB以cpp与h源码文件为主体配合ui界面文件、qrc资源文件、pro工程文件及sql建库脚本另附README说明与少量图片素材结构清晰、便于按模块阅读。项目已通过本地编译验证按文档配置好环境即可运行难度适中内容经助教老师审定能够满足学习与使用需求。目前已有258人学习下载。通过这份资源读者可以完整了解Qt多角色权限界面的组织方式、登录与主窗口的跳转逻辑以及MySQL建表与数据交互的基本流程并借助现成的工程文件快速搭建可运行环境为毕设选题、功能扩展和答辩演示提供直接参考。1. 从一张毕业设计题目说起QtMySQL 的教务系统到底要做什么每年到了毕设季总有一批同学拿着「基于 Qt 的教务系统设计与实现MySQL 做数据库分学生、教师、管理员三种身份」这个题目来找我。这个题目的典型场景是你需要做一个桌面客户端学生能查成绩、选课教师能录成绩、看课表管理员能管用户、排课、发通知所有数据落在 MySQL 里。它看起来是个「烂大街」的题目但真正动手时翻车点集中在三件事Qt 的数据库驱动没编好、三种角色的权限边界理不清、以及 MySQL 连接在长时间运行后莫名断开。这篇文章面向的是正在做这个题目、或者想拿它练手 Qt 桌面开发 MySQL 的从业者和学生。我会把身份权限模型、数据库表设计、Qt 侧的核心代码、以及那些只有踩过才知道的坑按能复现的顺序讲清楚。你不需要先看完一本 Qt 教材跟着走就能把最小可运行版本跑起来。热词里高频出现的 qt creator、mysql 安装配置、数据库增删改查、qt designer 界面设计都会在对应环节落到具体操作上。2. 三种身份怎么切权限模型与数据库表设计2.1 为什么不做三套界面而做一套界面加角色路由新手最容易犯的错是给学生、教师、管理员各写一套 MainWindow结果代码复制三份改一个 bug 要改三处。常见做法是登录成功后只拿到一个role字段主窗口用QStackedWidget装多个页面根据 role 决定哪些页面可见、哪些按钮禁用。这样界面只有一套权限判断集中在一个地方。角色权限的边界建议这样切角色可读可写典型操作学生自己的成绩、课表、可选课程选课、退课、改密码查成绩、选课教师自己任教课程的学生名单录入/修改成绩、提交课表录成绩管理员全部表用户增删改、排课、通知管用户、排课这个表不是给你抄的是让你在写if role ...之前先想清楚哪些操作是「读自己的」哪些是「读全部的」。数据库层面学生查成绩的 SQL 必须带WHERE student_id ?不能只靠界面隐藏按钮否则改个参数就能查到别人成绩。2.2 核心表结构与建表 SQL教务系统的表不用多六张就能撑起最小闭环用户表、学生表、教师表、课程表、选课表、成绩表。下面是我一般会用的建表语句字段类型和约束都按 MySQL 8 的常见配置来。-- 用户表登录用role 区分身份 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, -- 存哈希别存明文 role ENUM(student,teacher,admin) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表关联 users.id CREATE TABLE students ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, major VARCHAR(50), grade_year INT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE courses ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, teacher_id INT, credit DECIMAL(3,1) DEFAULT 0.0, capacity INT DEFAULT 0, FOREIGN KEY (teacher_id) REFERENCES teachers(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课表学生和课程的多对多 CREATE TABLE enrollments ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, enroll_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES students(id), FOREIGN KEY (course_id) REFERENCES courses(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表和选课表可以合并但分开更清晰 CREATE TABLE scores ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个参数说明utf8mb4是为了支持中文和 emoji别用utf8ENUM在 MySQL 里改起来麻烦如果角色可能扩展换成VARCHAR(20)加应用层校验UNIQUE KEY uk_stu_course是防止同一个学生重复选同一门课这个约束比在代码里查一遍再插入可靠得多。ON DELETE CASCADE用在学生表关联用户表上删用户时自动清学生记录但选课表我一般不加级联避免误删。2.3 用 Qt Designer 快速搭出登录和主界面界面这块不用手写布局代码qt designer 拖控件更快。登录窗口放QLineEdit用户名、QLineEdit密码设echoMode为 Password、QComboBox角色可选、两个QPushButton。主窗口用QStackedWidget加左侧QListWidget做导航点不同项切换setCurrentIndex。拖完之后控件命名要有意义比如lineEditUser、lineEditPwd、comboBoxRole、btnLogin。后面写代码时ui-lineEditUser-text()才看得懂。界面文件保存为.ui在.pro里加QT widgets sql编译时 uic 会自动生成ui_xxx.h。3. Qt 连 MySQL驱动、连接池与增删改查落地3.1 先确认 QSqlDatabase 能不能加载 MySQL 驱动Qt 默认不带 MySQL 驱动QSqlDatabase::drivers()里如果没有QMYSQL后面全白搭。这是新手第一个大坑热词里qt.qpa.plugin: could not find the qt platform plugin和cannot mix incompatible qt library都是环境层面的翻车。先跑一段检测代码#include QSqlDatabase #include QDebug int main() { qDebug() Available drivers: QSqlDatabase::drivers(); QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(edu_system); db.setUserName(root); db.setPassword(your_password); if (!db.open()) { qDebug() Open failed: db.lastError().text(); return -1; } qDebug() Connected.; return 0; }如果输出里没有QMYSQL说明驱动没编。Windows 下常见做法是找到 Qt 安装目录里的plugins/sqldrivers看有没有qsqlmysql.dll没有就用mysql-connector-c的库自己编一个或者直接用QMYSQL的替代方案。Linux 下用包管理器装libqt5sql5-mysql通常能解决。db.lastError().text()是排查连接问题的黑匣子任何 open 失败都先打印它。3.2 连接池别每次查询都 open 一次教务系统里查询频繁如果每个函数都addDatabaseopenclose连接开销大还容易在并发时出问题。常见做法是写一个单例连接池或者至少复用一个QSqlDatabase对象。下面是一个简化版连接管理class DbManager { public: static QSqlDatabase instance() { static QSqlDatabase db; if (!db.isValid()) { db QSqlDatabase::addDatabase(QMYSQL, main_conn); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(edu_system); db.setUserName(root); db.setPassword(your_password); db.setConnectOptions(MYSQL_OPT_RECONNECT1); // 自动重连 if (!db.open()) { qCritical() DB open failed: db.lastError().text(); } } return db; } };MYSQL_OPT_RECONNECT1是应对 MySQL 8 小时空闲断连的后悔药但要注意它只在连接失效后下次查询时重连不是实时保活。更稳的做法是加一个定时器每 4 小时执行一次SELECT 1。连接名main_conn要唯一重复addDatabase同名会警告。3.3 增删改查的四个标准写法以学生查成绩为例用QSqlQuery加预处理语句别拼字符串防注入。// 查学生查自己的成绩 QSqlQuery q(DbManager::instance()); q.prepare(SELECT c.course_name, s.score FROM scores s JOIN courses c ON s.course_id c.id WHERE s.student_id ?); q.addBindValue(currentStudentId); if (!q.exec()) { qDebug() Query failed: q.lastError().text(); } while (q.next()) { QString name q.value(0).toString(); double score q.value(1).toDouble(); // 填充到 QTableWidget } // 增学生选课 QSqlQuery ins(DbManager::instance()); ins.prepare(INSERT INTO enrollments(student_id, course_id) VALUES(?, ?)); ins.addBindValue(currentStudentId); ins.addBindValue(courseId); if (!ins.exec()) { // 唯一约束冲突会在这里报错提示已选过 qDebug() Enroll failed: ins.lastError().text(); } // 改教师录成绩 QSqlQuery upd(DbManager::instance()); upd.prepare(UPDATE scores SET score ? WHERE student_id ? AND course_id ?); upd.addBindValue(scoreValue); upd.addBindValue(stuId); upd.addBindValue(courseId); upd.exec(); // 删管理员删用户 QSqlQuery del(DbManager::instance()); del.prepare(DELETE FROM users WHERE id ?); del.addBindValue(userId); del.exec();参数说明addBindValue的顺序必须和 SQL 里的?一一对应错位是常见 bugq.value(1).toDouble()对 NULL 会返回 0如果成绩可能为空先用q.value(1).isNull()判断。选课插入失败时lastError().text()里会包含Duplicate entry据此提示用户「已选过这门课」比先查再插更可靠。4. 避坑与排查那些让毕设卡三天的真实问题4.1 现象编译报 cannot find -lpublic 或驱动加载失败原因通常是.pro里链接了不存在的库或者 Qt 版本和 MySQL 客户端库位数不匹配32 位 Qt 配 64 位 MySQL。解决检查LIBS -L... -lmysql路径是否存在用file命令确认.so或.dll的位数和 Qt 一致。热词里qt 编译时候 cannot find -lpublic多半是第三方库没放对位置。4.2 现象程序运行几分钟后查询全部失败原因MySQL 默认wait_timeout是 28800 秒但有些环境被调到 60 秒空闲连接被服务端断开Qt 侧还以为连接有效。解决连接字符串加MYSQL_OPT_RECONNECT1再加一个QTimer每 60 秒执行SELECT 1保活。排查时看lastError().text()是否含MySQL server has gone away。4.3 现象中文姓名存进去变成问号原因数据库、表、连接三处字符集不一致。解决建库时CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci连接后立即执行SET NAMES utf8mb4。Qt 侧QString本身是 Unicode问题基本都在 MySQL 服务端配置。4.4 现象学生能查到别人成绩原因SQL 没带student_id过滤只靠界面隐藏。解决所有涉及「自己的数据」的查询SQL 里必须带当前登录用户的 id 条件这是权限的最后一道防线不能省。4.5 现象Qt 程序在别人电脑上闪退报 0000005原因缺 DLL 或 MySQL 客户端库版本不对。解决用windeployqt打包 Qt 依赖MySQL 的libmysql.dll手动放到 exe 同目录。热词里qt写的关于can通讯的软件很容易闪退报0000005也是同类问题本质是运行时找不到依赖。5. 进阶技巧用存储过程和事务把选课做成原子操作选课这个动作表面是插一条enrollments实际要同时判断课程容量、是否已选、然后扣减名额。如果分三步在应用层做并发下会超卖。我一般把逻辑压进一个存储过程用事务包住。DELIMITER // CREATE PROCEDURE enroll_course(IN p_student_id INT, IN p_course_id INT, OUT p_result VARCHAR(50)) BEGIN DECLARE v_capacity INT; DECLARE v_enrolled INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result ERROR; END; START TRANSACTION; SELECT capacity INTO v_capacity FROM courses WHERE id p_course_id FOR UPDATE; SELECT COUNT(*) INTO v_enrolled FROM enrollments WHERE course_id p_course_id; IF v_enrolled v_capacity THEN SET p_result FULL; ROLLBACK; ELSE INSERT INTO enrollments(student_id, course_id) VALUES(p_student_id, p_course_id); SET p_result OK; COMMIT; END IF; END // DELIMITER ;Qt 侧调用QSqlQuery q(DbManager::instance()); q.prepare(CALL enroll_course(?, ?, result)); q.addBindValue(studentId); q.addBindValue(courseId); q.exec(); QSqlQuery r(DbManager::instance()); r.exec(SELECT result); r.next(); QString result r.value(0).toString(); // OK / FULL / ERRORFOR UPDATE锁住课程行防止两个学生同时读到相同名额。EXIT HANDLER保证任何异常都回滚。这个写法比在 Qt 里写一堆 if-else 可靠也是数据库课程设计里能加分的点。验证方法开两个客户端同时调同一个存储过程看是否只有一个返回 OK。我自己的习惯是凡是涉及「先查再改」的操作一律下沉到存储过程或至少用事务包住Qt 层只负责展示结果。这个题目做完你会对 Qt 的 SQL 模块和 MySQL 的事务有实打实的理解比背八股有用。希望帮到你。本文还有配套的精品资源点击获取