Qt+MySQL教务系统实战:三身份九表与选课冲突处理 简介这是一套面向计算机相关专业在校学生的Qt教务系统毕业设计源码采用C与Qt框架开发后端使用MySQL数据库完整实现了学生、教师、管理员三种身份角色的登录与权限管理适合作为毕设、课程设计或大作业的参考项目。压缩包共34个文件约95KB其中包含9个cpp源文件与8个h头文件构成核心业务逻辑7个ui界面文件负责可视化布局另有qrc资源文件、pro工程文件、sql建库脚本及README说明文档结构清晰便于二次开发。项目代码均经过实际运行测试功能完整答辩评审平均分达到96分已有87人学习下载。读者可从中获取完整的教务系统实现方案涵盖登录验证、管理员增删改查、教师与学生页面交互等模块既能直接用于毕设答辩也可在此基础上修改扩展实现更多功能是学习Qt与MySQL结合开发的实用范例。1. 基于QtMySQL的教务系统三个身份、九张表怎么把课设做成能跑的原型每年毕业季教务系统这个题目被选中的频率高得离谱。原因很直接需求文档现成、业务逻辑不复杂、答辩时老师一听就懂。但真正动手时大部分人会卡在三个地方——Qt怎么连MySQL、三种身份怎么在同一个界面框架里切换、选课冲突怎么在数据库层面兜住。这篇笔记就按我实际做过的路径把这三个问题拆开讲清楚。标题里的关键词是Qt、MySQL、教务系统身份分学生、教师、管理员。适合的人群很明确正在做毕设或课程设计、已经装好Qt Creator和MySQL、但还没想清楚表结构和界面跳转逻辑的人。如果你还在纠结用不用Qt我的建议是——只要你的环境是WindowsMSVC或MinGWQt的QSqlDatabase模块能让你在半天内跑通数据库连接比从头写JDBC省事得多。下面从环境配置开始一步步推到选课冲突检测和成绩录入的完整链路。2. 环境搭建与数据库连接Qt Creator里跑通MySQL的第一步2.1 Qt版本与MySQL驱动的匹配关系Qt连接MySQL不是装完两边就自动能用的。Qt的QSqlDatabase依赖一个叫QMYSQL的驱动插件这个插件在官方安装包里默认不带需要自己编译或者找现成的。我一般会先确认三件事Qt的版本、编译器类型MSVC还是MinGW、MySQL的版本。Qt 5.15.2配MSVC2019_64是毕设里最常见的组合。MySQL用8.0或者5.7都行但要注意MySQL 8.0默认的认证插件是caching_sha2_password而Qt 5.15自带的MySQL驱动可能只支持mysql_native_password。如果连接时报“Authentication plugin caching_sha2_password cannot be loaded”就是这个问题。解决办法是在MySQL里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;提示改完认证插件后如果Qt还是连不上检查一下libmysql.dll和libmysql.lib是否放到了Qt的编译输出目录下。MSVC编译器需要这两个文件MinGW则需要libmysql.dll。2.2 在.pro文件里配置数据库模块Qt项目要使用SQL模块必须在.pro文件里加上一行QT core gui sql如果是CMake项目对应的是find_package(Qt5 COMPONENTS Core Gui Sql REQUIRED) target_link_libraries(你的项目名 PRIVATE Qt5::Core Qt5::Gui Qt5::Sql)加完之后重新执行qmake否则编译时会报“QSqlDatabase: No such file or directory”。2.3 写一个可复用的数据库连接函数我习惯把数据库连接封装成一个静态函数放在单独的dbhelper.h和dbhelper.cpp里。这样学生端、教师端、管理员端都能调用同一份代码改密码或换主机时只改一个地方。// dbhelper.h #ifndef DBHELPER_H #define DBHELPER_H #include QSqlDatabase #include QSqlError #include QDebug class DBHelper { public: static bool connectDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); // 数据库主机 db.setPort(3306); // MySQL默认端口 db.setDatabaseName(edu_system); // 数据库名 db.setUserName(root); // 用户名 db.setPassword(123456); // 密码 if (!db.open()) { qDebug() 连接失败: db.lastError().text(); return false; } qDebug() 数据库连接成功; return true; } }; #endif // DBHELPER_H逻辑说明addDatabase指定驱动为QMYSQLsetHostName和setPort指向本机MySQL服务setDatabaseName对应提前建好的库名。open()返回false时lastError().text()会给出具体原因常见的有“Access denied”“Unknown database”“Driver not loaded”。参数说明主机名如果是本机可以用localhost或127.0.0.1端口默认3306数据库名建议用英文小写加下划线。密码不要用特殊字符否则在代码里需要转义。2.4 建库建表的最小SQL脚本数据库名定为edu_system字符集用utf8mb4排序规则用utf8mb4_general_ci。下面这张表是学生信息表其他表按同样风格扩展。CREATE DATABASE IF NOT EXISTS edu_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_system; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, student_name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL DEFAULT 123456, class_name VARCHAR(50), major VARCHAR(50), enrollment_year INT ); CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY, teacher_name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL DEFAULT 123456, title VARCHAR(30), department VARCHAR(50) ); CREATE TABLE admin ( admin_id VARCHAR(20) PRIMARY KEY, admin_name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL DEFAULT admin );逻辑说明三张身份表分开建字段各自独立登录时根据选择的身份去查对应的表。密码字段先存明文方便调试后期可以换成MD5或SHA256。参数说明VARCHAR(20)足够存学号工号VARCHAR(100)给密码留余量。enrollment_year用INT存年份方便按年级筛选。3. 三种身份的界面切换用QStackedWidget做登录后的路由3.1 登录窗口的设计思路登录界面只需要三个东西身份下拉框、账号输入框、密码输入框外加一个登录按钮。身份下拉框用QComboBox选项是“学生”“教师”“管理员”。点击登录后根据当前选中的身份去查不同的表。void LoginDialog::onLoginClicked() { QString role ui-comboRole-currentText(); QString id ui-editAccount-text().trimmed(); QString pwd ui-editPassword-text().trimmed(); if (id.isEmpty() || pwd.isEmpty()) { QMessageBox::warning(this, 提示, 账号和密码不能为空); return; } QString tableName; if (role 学生) tableName student; else if (role 教师) tableName teacher; else tableName admin; QSqlQuery query; query.prepare(QString(SELECT * FROM %1 WHERE %2_id :id AND password :pwd) .arg(tableName).arg(tableName)); query.bindValue(:id, id); query.bindValue(:pwd, pwd); if (query.exec() query.next()) { // 登录成功根据身份打开对应主窗口 if (role 学生) { StudentWindow *w new StudentWindow(id); w-show(); } else if (role 教师) { TeacherWindow *w new TeacherWindow(id); w-show(); } else { AdminWindow *w new AdminWindow(id); w-show(); } this-close(); } else { QMessageBox::warning(this, 错误, 账号或密码不正确); } }逻辑说明用preparebindValue防止SQL注入表名和字段名通过arg拼接是因为它们来自程序内部而非用户输入不存在注入风险。登录成功后把学号或工号传给主窗口后续查询都带上这个ID。参数说明trimmed()去掉首尾空格避免用户复制粘贴时带入不可见字符。query.next()返回true表示至少查到一行。3.2 主窗口用QStackedWidget管理多个页面学生登录后能看到选课、查成绩、改密码三个功能。教师能看到录入成绩、查看选课名单。管理员能看到学生管理、教师管理、课程管理。如果每个功能都弹一个窗口任务栏会乱成一团。我一般用QStackedWidget把每个功能做成一个QWidget页面主窗口左侧放导航按钮右侧放QStackedWidget。// StudentWindow构造函数里 stackedWidget new QStackedWidget(this); coursePage new CourseSelectionPage(studentId); gradePage new GradeQueryPage(studentId); passwordPage new ChangePasswordPage(studentId); stackedWidget-addWidget(coursePage); // index 0 stackedWidget-addWidget(gradePage); // index 1 stackedWidget-addWidget(passwordPage); // index 2 // 左侧按钮点击时切换 connect(btnCourse, QPushButton::clicked, []() { stackedWidget-setCurrentIndex(0); }); connect(btnGrade, QPushButton::clicked, []() { stackedWidget-setCurrentIndex(1); });逻辑说明每个页面在构造时接收studentId页面内部自己负责查询和展示。主窗口只负责切换不掺和业务逻辑。参数说明addWidget的顺序决定了setCurrentIndex的参数值建议用枚举或常量代替裸数字。3.3 教师端和管理员端的差异处理教师端的核心页面是成绩录入。我一般做成一个表格列出该教师所授课程的学生名单每行一个QLineEdit或QDoubleSpinBox用来输入分数底部一个“保存”按钮批量提交。管理员端的核心是增删改查。学生管理页面用一个QTableWidget展示所有学生上方放搜索框和“新增”“删除”按钮。新增时弹一个QDialog收集信息删除时先弹确认框。注意教师端和管理员端虽然功能不同但数据库连接和登录逻辑是共用的。不要把DBHelper的代码复制三份否则改密码时要改三个地方。4. 选课冲突与成绩录入把业务规则写进SQL和Qt逻辑里4.1 选课冲突检测的两种实现位置选课冲突分两种时间冲突和容量冲突。时间冲突是指同一时间段已经选了另一门课容量冲突是指课程人数已满。这两个检查可以放在Qt代码里做也可以放在数据库层面做。我的做法是容量检查用SQL的UPDATE加条件时间检查在Qt里查完再判断。先看课程表结构CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_id VARCHAR(20), credit INT DEFAULT 3, capacity INT DEFAULT 50, selected_count INT DEFAULT 0, schedule VARCHAR(50), -- 例如 周一3-4节 FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ); CREATE TABLE student_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20), course_id VARCHAR(20), score DECIMAL(5,2) DEFAULT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id), UNIQUE KEY uk_student_course (student_id, course_id) );逻辑说明student_course表用UNIQUE约束防止重复选同一门课。score字段默认为NULL表示尚未录入成绩。参数说明schedule用字符串存时间段简单但够用。如果要精确到分钟可以拆成start_time和end_time两个字段。4.2 用事务处理选课操作选课涉及两个写操作往student_course插一条记录同时把course表的selected_count加一。这两个操作必须在一个事务里完成否则可能出现选了课但人数没加的情况。bool CourseSelectionPage::selectCourse(const QString studentId, const QString courseId) { QSqlDatabase::database().transaction(); QSqlQuery query; // 第一步检查容量 query.prepare(SELECT capacity, selected_count, schedule FROM course WHERE course_id :cid); query.bindValue(:cid, courseId); query.exec(); if (!query.next()) { QSqlDatabase::database().rollback(); return false; } int capacity query.value(capacity).toInt(); int selected query.value(selected_count).toInt(); QString newSchedule query.value(schedule).toString(); if (selected capacity) { QMessageBox::warning(this, 提示, 该课程已满); QSqlDatabase::database().rollback(); return false; } // 第二步检查时间冲突 query.prepare(SELECT c.schedule FROM student_course sc JOIN course c ON sc.course_id c.course_id WHERE sc.student_id :sid); query.bindValue(:sid, studentId); query.exec(); while (query.next()) { if (query.value(schedule).toString() newSchedule) { QMessageBox::warning(this, 提示, 与已选课程时间冲突); QSqlDatabase::database().rollback(); return false; } } // 第三步插入选课记录 query.prepare(INSERT INTO student_course (student_id, course_id) VALUES (:sid, :cid)); query.bindValue(:sid, studentId); query.bindValue(:cid, courseId); if (!query.exec()) { QSqlDatabase::database().rollback(); return false; } // 第四步更新已选人数 query.prepare(UPDATE course SET selected_count selected_count 1 WHERE course_id :cid); query.bindValue(:cid, courseId); if (!query.exec()) { QSqlDatabase::database().rollback(); return false; } QSqlDatabase::database().commit(); return true; }逻辑说明transaction()开启事务任何一步失败就rollback()全部成功才commit()。时间冲突检查用JOIN查出该学生已选课程的所有schedule逐一比对。参数说明schedule的字符串格式必须统一比如都用“周一3-4节”这种写法否则比对会失效。4.3 成绩录入的批量提交教师端录入成绩时我一般用QTableWidget展示学生名单每行最后一列放一个QDoubleSpinBox范围0到100保留一位小数。点击保存时遍历所有行逐条UPDATE。void GradeEntryPage::onSaveClicked() { QSqlDatabase::database().transaction(); QSqlQuery query; query.prepare(UPDATE student_course SET score :score WHERE student_id :sid AND course_id :cid); for (int i 0; i tableWidget-rowCount(); i) { QString sid tableWidget-item(i, 0)-text(); double score qobject_castQDoubleSpinBox*( tableWidget-cellWidget(i, 2))-value(); query.bindValue(:score, score); query.bindValue(:sid, sid); query.bindValue(:cid, currentCourseId); if (!query.exec()) { QSqlDatabase::database().rollback(); QMessageBox::warning(this, 错误, 保存失败 query.lastError().text()); return; } } QSqlDatabase::database().commit(); QMessageBox::information(this, 成功, 成绩已保存); }逻辑说明整个保存过程在一个事务里任何一条更新失败就全部回滚避免部分学生有成绩部分没有。参数说明QDoubleSpinBox的setDecimals(1)控制小数位数setRange(0, 100)控制范围。如果成绩允许为空可以在保存前判断是否所有行都填了值。5. 避坑与排查教务系统开发中最容易翻车的五个地方5.1 中文乱码数据库、连接、界面三处都要设UTF-8现象学生姓名显示成问号或方块。原因MySQL建库时没指定utf8mb4或者Qt连接时没设置编码。解决建库语句加上DEFAULT CHARACTER SET utf8mb4连接后执行query.exec(SET NAMES utf8mb4)Qt界面里QString默认是Unicode一般不需要额外处理。5.2 驱动加载失败QMYSQL not loaded现象程序启动后qDebug输出“QSqlDatabase: QMYSQL driver not loaded”。原因Qt安装包里没有编译好的QMYSQL驱动或者libmysql.dll不在可执行文件目录下。解决先确认Qt版本和编译器然后找对应的qsqlmysql.dll放到Qt的plugins/sqldrivers目录下。MSVC用户还需要把libmysql.dll复制到exe同级目录。5.3 选课人数超限并发下的计数错误现象两个学生同时选最后一门课结果两个人都选上了selected_count变成capacity1。原因检查和更新之间有时间窗口两个事务都读到了相同的selected_count。解决把容量检查写进UPDATE的条件里用UPDATE course SET selected_count selected_count 1 WHERE course_id ? AND selected_count capacity然后根据affectedRows判断是否成功。5.4 登录后窗口关不掉父窗口指针丢失现象登录窗口关闭后主窗口也跟着关了或者程序还在后台运行。原因登录窗口是主窗口的父对象父窗口关闭时子窗口被销毁。解决登录成功后先new主窗口再调用this-close()不要用exec()模态对话框。或者在main.cpp里把登录窗口和主窗口都作为独立对象管理。5.5 成绩录入后查询不到事务未提交现象教师端保存成绩提示成功但学生端查成绩还是空。原因QSqlDatabase::database()返回的默认连接和查询用的连接不是同一个或者事务没commit。解决确保所有查询都用QSqlDatabase::database()获取连接不要混用addDatabase创建的新连接。保存后确认commit()被调用。6. 从能跑到好用三个让答辩加分的细节6.1 用QSqlTableModel简化管理员端的增删改查管理员端的学生管理、教师管理、课程管理本质上都是对单表的增删改查。与其手写每个按钮的SQL不如用QSqlTableModel绑定QTableView。model new QSqlTableModel(this); model-setTable(student); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-select(); ui-tableView-setModel(model); ui-tableView-setSelectionMode(QAbstractItemView::SingleSelection);逻辑说明setEditStrategy设为OnManualSubmit后修改不会立即写回数据库需要调用model-submitAll()才生效。这样可以在提交前做校验。参数说明setTable的参数是数据库里实际的表名。select()执行查询把数据拉到模型里。6.2 给选课界面加一个实时人数显示学生选课时最关心的是“还剩多少名额”。可以在课程列表的每一行后面加一列显示capacity - selected_count。QString sql SELECT course_id, course_name, teacher_id, credit, capacity - selected_count AS remaining, schedule FROM course; QSqlQueryModel *model new QSqlQueryModel(this); model-setQuery(sql); ui-tableView-setModel(model);逻辑说明在SQL里直接算好剩余名额Qt端只负责展示。remaining为0时可以用setStyleSheet把该行标红。参数说明AS remaining是给计算结果起别名方便在Qt里通过model-record().indexOf(remaining)找到列号。6.3 密码存储从明文换成哈希毕设答辩时老师经常会问“密码怎么存的”。如果回答明文会被扣分。最简单的改法是存MD5虽然MD5也不够安全但比明文强。QString hashPassword(const QString input) { QByteArray data input.toUtf8(); QByteArray hash QCryptographicHash::hash(data, QCryptographicHash::Sha256); return hash.toHex(); }逻辑说明登录时把用户输入的密码做同样的哈希再和数据库里的值比对。数据库里存的是哈希后的十六进制字符串。参数说明QCryptographicHash支持Md5、Sha1、Sha256等Sha256比Md5更安全。哈希是不可逆的忘记密码只能重置不能找回。我做完这个系统后最大的习惯改变是任何涉及多步写操作的业务先想清楚事务边界在哪。选课、退课、成绩录入、批量导入这些操作要么全成功要么全失败中间状态在教务系统里是不可接受的。另一个习惯是把数据库连接和业务逻辑分开写DBHelper只负责连页面只负责查和展示改起来不牵连。希望帮到你。本文还有配套的精品资源点击获取