☰
Qt+MySQL教务系统实战:权限设计、连接池与避坑指南
2026/9/26 21:37:05 网站建设 项目流程

简介:这是一套面向计算机相关专业毕业设计场景的教务系统完整源码,采用Qt框架开发、MySQL作为后台数据库,区分学生、教师和管理员三种身份,适合正在准备毕设或需要Qt桌面项目练手的学习者参考。压缩包共34个文件,约97KB,以cpp与h源码文件为主体,配合ui界面文件、qrc资源文件、pro工程文件及sql建库脚本,另附README说明与少量图片素材,结构清晰、便于按模块阅读。项目已通过本地编译验证,按文档配置好环境即可运行,难度适中,内容经助教老师审定,能够满足学习与使用需求。目前已有258人学习下载。通过这份资源,读者可以完整了解Qt多角色权限界面的组织方式、登录与主窗口的跳转逻辑,以及MySQL建表与数据交互的基本流程,并借助现成的工程文件快速搭建可运行环境,为毕设选题、功能扩展和答辩演示提供直接参考。

1. 从一张毕业设计题目说起:Qt+MySQL 的教务系统到底要做什么

每年到了毕设季,总有一批同学拿着「基于 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生表:关联 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课表:学生和课程的多对多 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表:和选课表可以合并,但分开更清晰 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个参数说明:utf8mb4是为了支持中文和 emoji,别用utf8;ENUM在 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 一次

教务系统里查询频繁,如果每个函数都addDatabase+open+close,连接开销大,还容易在并发时出问题。常见做法是写一个单例连接池,或者至少复用一个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_RECONNECT=1"); // 自动重连 if (!db.open()) { qCritical() << "DB open failed:" << db.lastError().text(); } } return db; } };

MYSQL_OPT_RECONNECT=1是应对 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 里的?一一对应,错位是常见 bug;q.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_RECONNECT=1,再加一个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 / ERROR

FOR UPDATE锁住课程行,防止两个学生同时读到相同名额。EXIT HANDLER保证任何异常都回滚。这个写法比在 Qt 里写一堆 if-else 可靠,也是数据库课程设计里能加分的点。验证方法:开两个客户端同时调同一个存储过程,看是否只有一个返回 OK。

我自己的习惯是,凡是涉及「先查再改」的操作,一律下沉到存储过程或至少用事务包住,Qt 层只负责展示结果。这个题目做完,你会对 Qt 的 SQL 模块和 MySQL 的事务有实打实的理解,比背八股有用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询