简介这是一份面向计算机相关专业毕业设计场景的教务系统完整源码包采用Qt框架开发、MySQL作为后台数据库围绕学生、教师、管理员三类身份构建了登录、选课、成绩与信息管理等核心业务模块适合正在准备毕设或需要Qt桌面项目练手的同学参考与二次开发。压缩包共34个文件约97KB以cpp源文件与h头文件为主体配合ui界面文件、qrc资源文件、pro工程文件及sql建库脚本另附README说明与少量图片素材结构清晰、便于按模块阅读。项目源码均经本地编译验证按文档配置好Qt与MySQL环境即可运行难度适中内容经过助教老师审定能够满足学习与使用需求。目前已有258人学习关注下载后遇到问题也可私信作者获取解答对需要完整毕设方案与排错思路的读者具有一定参考价值。1. 从一张课程表说起QtMySQL 教务系统到底要解决什么选课季的机房总有人盯着屏幕骂人课程余量显示还有 3 个点进去提交却提示已满教师那边成绩录到一半客户端直接闪退重开发现录了半截的数据全没了。这类问题的根子往往不在界面而在「界面和数据库之间那层逻辑」没设计好。基于 Qt 的教务系统设计与实现配上 MySQL 数据库身份分学生、教师、管理员三种本质就是做一个带权限控制的桌面端增删改查系统把选课、成绩、排课、用户管理这些事务用一套可靠的数据流串起来。它适合谁正在做数据库课程设计或毕设的本科生需要一份能跑起来、能答辩、能讲清楚架构的项目也适合刚接触 Qt 桌面开发、想找一个完整业务场景练手的工程师。难点从来不是「怎么连数据库」而是并发选课怎么不超卖、三种角色的权限怎么在代码层收口、Qt 的信号槽怎么和 SQL 事务配合。下面按「先立住架构、再动手复现、最后讲坑」的顺序拆开讲代码和参数都能直接抄。2. 架构先立住三张核心表怎么撑起三种身份2.1 为什么用 Qt 客户端 MySQL 服务端而不是单机 SQLite很多人第一反应是「毕设而已用 SQLite 单文件多省事」。但教务系统的核心矛盾是多客户端并发写同一份数据几十个学生同时抢一门课教师同时提交成绩管理员同时改培养方案。SQLite 的写锁是库级的一个写事务会阻塞其他所有写操作选课高峰期直接卡死。MySQL 支持行级锁和真正的事务隔离配合 InnoDB 引擎能把「查余量—扣余量—写选课记录」放进一个事务里这是 SQLite 给不了的。Qt 这边用QSqlDatabaseQSqlQuery走 MySQL 驱动界面层用QTableView绑QSqlTableModel或自定义QAbstractTableModel。选型上如果只是展示和简单编辑QSqlTableModel省事但选课这种带业务校验的操作必须绕开模型直接写QSqlQuery并手动transaction()否则模型会自动提交事务控制不住。提示Qt 官方发行版默认不带 MySQL 驱动需要自己编译qsqlmysql插件或者用已编译好的libqsqlmysql.so/qsqlmysql.dll。这是新手第一个翻车点后面避坑章细说。2.2 三张核心表 两张关联表的最小 schema身份分三种但不要建三张用户表。正确做法是一张user表存公共字段用role字段区分再各自挂扩展表。核心表设计如下表名作用关键字段说明user所有账号id, username, password_hash, role, real_namerole 取值 student/teacher/admincourse课程id, course_code, name, teacher_id, capacity, enrolled, creditenrolled 是已选人数冗余字段course_selection选课记录id, student_id, course_id, select_time, statusstatus 区分正常/退选score成绩id, student_id, course_id, score, term与选课记录分离便于补考schedule排课id, course_id, weekday, start_slot, end_slot, classroom冲突检测靠它course.enrolled是冗余字段有人会问「为什么不每次COUNT(*)算」。因为选课事务里要SELECT ... FOR UPDATE锁住课程行再判断余量如果每次都去 count 选课表锁的范围和性能都不可控。冗余一个计数字段在同一个事务里UPDATE course SET enrolled enrolled 1 WHERE id ? AND enrolled capacity靠这条带条件的 UPDATE 的 affected rows 是否为 1 来判断选课成功是最稳的防超卖写法。2.3 权限控制放在哪一层别只藏按钮新手最容易犯的错是「学生登录后把管理按钮setVisible(false)」就以为做了权限。用户拿个 Qt Designer 改不了二进制但随便一个 SQL 注入或者直接调接口就绕过去了。权限必须落在数据访问层每个角色的数据库操作封装成独立的 DAO 类学生 DAO 里根本没有「删除用户」这个方法从代码结构上就杜绝越权。// 权限收口示例不同角色拿到不同的 DAO 接口 class IUserDao { public: virtual ~IUserDao() default; virtual QListUser listUsers() 0; }; class AdminUserDao : public IUserDao { public: QListUser listUsers() override { QSqlQuery q; q.exec(SELECT id, username, role, real_name FROM user); // 管理员才能看到全部用户 ... } bool deleteUser(int id); // 只有管理员 DAO 有删除 }; class StudentUserDao : public IUserDao { public: QListUser listUsers() override { // 学生只能看到自己 QSqlQuery q; q.prepare(SELECT id, username, role, real_name FROM user WHERE id ?); q.addBindValue(currentUserId_); ... } // 没有 deleteUser编译期就挡住了 };逻辑说明用抽象接口 角色实现类把「能做什么」固化在类型系统里而不是运行时 if 判断。参数上currentUserId_在登录成功后注入所有查询强制带这个条件。这样即使界面层出 bug 把按钮露出来点下去也只会查到自己。注意密码绝对不要明文存。用QCryptographicHash::hash(pwd salt, QCryptographicHash::Sha256)加盐哈希salt 每个用户随机生成存库。毕设答辩时老师十有八九会问这个。3. 动手复现从建库到选课事务跑通3.1 MySQL 建库建表与初始数据脚本先确保 MySQL 8.x 装好、服务起来。命令行登录后执行下面这段建库、建表、插一条管理员账号。字符集用utf8mb4否则学生姓名里的生僻字会变问号。CREATE DATABASE edu_sys DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE edu_sys; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL, salt CHAR(16) NOT NULL, role ENUM(student,teacher,admin) NOT NULL, real_name VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, teacher_id INT, capacity INT NOT NULL DEFAULT 50, enrolled INT NOT NULL DEFAULT 0, credit DECIMAL(3,1) DEFAULT 2.0, FOREIGN KEY (teacher_id) REFERENCES user(id) ) ENGINEInnoDB; CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1, -- 1 正常 0 已退选 UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES user(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB; -- 初始管理员密码是 123456 的 sha256 加盐示例实际由程序生成 INSERT INTO user (username, password_hash, salt, role, real_name) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, abcdef1234567890, admin, 系统管理员);参数说明uk_stu_course唯一索引是关键它保证同一个学生不能对同一门课插两条记录即使程序逻辑漏了判断数据库层也会拒绝。enrolled默认 0capacity默认 50这两个值在选课事务里配合使用。status用 TINYINT 而不是布尔方便以后扩展「待审核」等状态。3.2 Qt 连接 MySQL 与驱动加载的完整代码连接代码要处理三件事加载驱动、设置连接参数、检查错误。很多人卡在「QMYSQL driver not loaded」先看下面代码里的错误打印。#include QSqlDatabase #include QSqlError #include QDebug bool initDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(edu_sys); db.setUserName(root); db.setPassword(your_password); // 关键设置连接选项避免中文乱码和超时 db.setConnectOptions(MYSQL_OPT_CONNECT_TIMEOUT10;MYSQL_OPT_RECONNECT1); if (!db.open()) { qCritical() DB open failed: db.lastError().text(); qCritical() Available drivers: QSqlDatabase::drivers(); return false; } qDebug() MySQL connected, driver: db.driverName(); return true; }逻辑说明addDatabase(QMYSQL)里的字符串必须和已安装的驱动名完全一致用QSqlDatabase::drivers()打印出来核对。setConnectOptions里MYSQL_OPT_RECONNECT1让连接断开后自动重连避免长时间空闲后第一次操作失败。如果open()失败lastError().text()会给出具体原因比如密码错、库不存在、socket 路径不对。参数说明127.0.0.1比localhost更可靠因为localhost在 MySQL 里可能走 Unix socket 而不是 TCP报错信息会变成Cant connect to local MySQL server through socket。端口默认 3306如果改过要同步。生产环境别用 root建一个只有edu_sys权限的专用账号。3.3 选课事务防超卖的核心 SQL 与 Qt 事务封装这是整个系统最值得写进论文的一节。选课要同时做三件事判断余量、扣减余量、插入选课记录。三步必须原子。bool selectCourse(int studentId, int courseId) { QSqlDatabase db QSqlDatabase::database(); if (!db.transaction()) { // 开启事务 qWarning() begin transaction failed; return false; } QSqlQuery q(db); // 第一步带条件的原子扣减affected rows 为 1 才算抢到 q.prepare(UPDATE course SET enrolled enrolled 1 WHERE id ? AND enrolled capacity); q.addBindValue(courseId); if (!q.exec() || q.numRowsAffected() ! 1) { db.rollback(); qWarning() course full or not exist; return false; // 余量为 0 或课程不存在 } // 第二步插入选课记录唯一索引兜底防重复选 q.prepare(INSERT INTO course_selection (student_id, course_id) VALUES (?, ?)); q.addBindValue(studentId); q.addBindValue(courseId); if (!q.exec()) { db.rollback(); // 重复选课会触发唯一索引冲突 qWarning() insert selection failed: q.lastError().text(); return false; } if (!db.commit()) { // 提交 db.rollback(); return false; } return true; }逻辑说明核心是第一条 UPDATE 的WHERE enrolled capacity。数据库在执行这条语句时会锁住该课程行判断和自增在一条语句里完成天然原子。如果两个学生同时抢最后一个名额InnoDB 行锁会让它们串行执行第二个的enrolled capacity不成立numRowsAffected()返回 0直接回滚。这比「先 SELECT 查余量再 UPDATE」可靠得多后者在两步之间有竞态窗口。参数说明numRowsAffected()在 UPDATE 里返回实际修改的行数条件不满足时是 0。db.transaction()和db.commit()必须成对任何一步失败都要rollback()否则连接会一直挂着未提交事务后续操作全被阻塞。退选就是反向操作UPDATE course SET enrolled enrolled - 1 WHERE id ? AND enrolled 0再把选课记录status置 0。3.4 用 QSqlTableModel 快速搭出成绩录入界面成绩录入是教师端高频操作用QSqlTableModel能省掉大量样板代码但要注意它默认自动提交。void TeacherScoreWidget::setupModel() { model_ new QSqlTableModel(this, QSqlDatabase::database()); model_-setTable(score); // 只显示本教师所授课程的成绩用 filter 收口 model_-setFilter(QString(course_id IN (SELECT id FROM course WHERE teacher_id %1)).arg(currentTeacherId_)); model_-setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交 model_-select(); ui-tableView-setModel(model_); ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); // 隐藏主键列 ui-tableView-hideColumn(0); } void TeacherScoreWidget::onSaveClicked() { if (model_-submitAll()) { model_-database().commit(); } else { QMessageBox::warning(this, 保存失败, model_-lastError().text()); model_-revertAll(); // 回滚未提交的修改 } }逻辑说明setEditStrategy(OnManualSubmit)让所有编辑先缓存在模型里点保存才一次性提交避免改一格就写一次库。setFilter用子查询把教师权限收口到「自己教的课」这是数据层权限的又一体现。submitAll()失败时revertAll()把界面恢复成数据库里的值防止用户以为改成功了。参数说明currentTeacherId_登录时注入。hideColumn(0)隐藏自增主键防止用户误改。如果成绩表数据量大setFilter的子查询会拖慢可以改成先查出课程 id 列表再拼IN (...)。4. 避坑与排查那些让答辩当场卡壳的问题4.1 QMYSQL driver not loaded驱动缺失或版本不匹配现象程序启动连库时报QMYSQL driver not loadedQSqlDatabase::drivers()里根本没有 QMYSQL。原因Qt 官方安装包出于许可考虑不带 MySQL 驱动需要自己编译qsqlmysql插件或者驱动版本和 Qt 版本、MySQL 客户端库版本对不上。热词里那个cannot mix incompatible qt library也是同类问题混用了不同版本编译的库。解决先确认sqldrivers目录下有没有qsqlmysql.dllWindows或libqsqlmysql.soLinux。没有就用qmake编译 Qt 源码里qtbase/src/plugins/sqldrivers/mysql编译时指定 MySQL 的 include 和 lib 路径。编译出来的插件必须放到 Qt 安装目录的plugins/sqldrivers下且依赖的libmysqlclient要在系统库路径里。Linux 下用ldd libqsqlmysql.so检查依赖是否齐全。4.2 中文乱码连接字符集没设对现象学生姓名、课程名在界面显示成问号或方块。原因MySQL 服务端、库、表、连接四层字符集不一致。常见是库建成了latin1或者 Qt 连接没声明utf8mb4。解决建库时DEFAULT CHARACTER SET utf8mb4连接后立刻执行SET NAMES utf8mb4。Qt 侧在setConnectOptions里加MYSQL_OPT_CONNECT_TIMEOUT之外可以在open()成功后跑一条QSqlQuery(SET NAMES utf8mb4)。源文件保存为 UTF-8QString到QByteArray转换用toUtf8()而不是toLatin1()。4.3 选课超卖先查后改的经典竞态现象课程容量 50最后显示选了 51 个人。原因代码写成「先SELECT enrolled, capacity判断再UPDATE enrolled enrolled 1」两步之间其他事务插进来都读到 49 然后各自加 1。解决用 3.3 节那种带条件的原子 UPDATE把判断和修改合并成一条语句靠numRowsAffected()判断结果。或者用SELECT ... FOR UPDATE显式加行锁但要注意锁的范围和死锁风险不如条件 UPDATE 简洁。4.4 事务没提交导致后续操作全阻塞现象某次选课失败后其他所有数据库操作都卡住界面无响应。原因db.transaction()开启后某条分支return false前忘了rollback()未提交事务一直占着行锁其他连接等锁等到超时。解决用 RAII 封装事务或者严格保证每个return前都有rollback()。更稳的写法是写一个TransactionGuard类析构时如果没提交就自动回滚。排查时在 MySQL 里执行SHOW ENGINE INNODB STATUS看有没有长时间运行的事务或者SELECT * FROM information_schema.innodb_trx。4.5 密码明文与 SQL 注入答辩必问的两个安全点现象老师翻代码看到WHERE username name 这种拼接直接问「这能不能注入」。原因用字符串拼接构造 SQL用户输入 OR 11就能绕过登录。解决所有带用户输入的查询一律用QSqlQuery::prepareaddBindValue参数绑定绝不拼接。密码存QCryptographicHash的 SHA256 加盐哈希登录时把用户输入同样哈希后比对。这两点做到安全相关的追问基本能接住。5. 让系统更像「真教务」两个进阶技巧与验证方法基础功能跑通后答辩想拿高分得有点超出预期的设计。第一个技巧是排课冲突检测。教师排课时同一教室同一时间段不能有两门课同一班级同一时间段也不能冲突。用一条 SQL 就能查出来-- 检测某教室在某时间段是否已被占用 SELECT COUNT(*) FROM schedule WHERE classroom ? AND weekday ? AND start_slot ? AND end_slot ?; -- 区间重叠判断区间重叠的判断条件是已有.start 新.end AND 已有.end 新.start这是经典写法比逐个 slot 比对高效。插入排课前先跑这条返回大于 0 就提示冲突。班级冲突同理把classroom换成class_id即可。第二个技巧是用视图给管理员做统计看板。与其在 Qt 里写一堆循环算选课率不如在数据库建视图CREATE VIEW v_course_stat AS SELECT c.id, c.name, c.capacity, c.enrolled, ROUND(c.enrolled / c.capacity * 100, 1) AS fill_rate, u.real_name AS teacher_name FROM course c LEFT JOIN user u ON c.teacher_id u.id;管理员界面直接SELECT * FROM v_course_stat ORDER BY fill_rate DESC选课率最高的课一目了然。视图把计算逻辑放在数据库层Qt 端只负责展示代码干净且统计口径统一。验证方法上我一般会做三件事一是用 MySQL 的EXPLAIN看选课事务那条 UPDATE 有没有走主键索引避免全表扫描二是写个简单的并发测试脚本用多线程同时调selectCourse看最终enrolled是否超过capacity三是把course_selection的唯一索引故意去掉再跑并发确认会出现重复选课以此证明唯一索引的必要性——这个对比实验写进论文很有说服力。血泪经验是别等到答辩前一天才在老师电脑上装环境。Qt 驱动、MySQL 版本、字符集这些环境问题换台机器就可能复现。提前把建库脚本、驱动文件、依赖库打包成一个可复现的清单比多写两个界面功能有用得多。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
用AI Agent接管FairyGUI UI开发:从拖拽到代码生成 1. 为什么“让 Agent 写 UI”这件事能成立先聊个现象。我自己做客户端开发这几年,见过太多团队在 UI 上死磕:策划改个文案,前端改个位置,都要打开 FairyGUI 编辑器手动拖一遍,然后重新发布、重新导包。一套流程走下来&… · 2026/9/26 14:25:37
WorkBuddy+Flask+SQLite:快速搭建日更内容站实战 1. 为什么我选择 WorkBuddy Flask SQLite 这套组合
1.1 从零建站的真实需求拆解 先说清楚背景。我手上有一个内容型站点,需要每天更新文章,内容以农产品价格数据整理和简单分析为主。之前用过现成的博客系统,也试过纯静态页面手动改 HTML&… · 2026/9/26 14:25:37
谷歌图片搜索 API:字段口径、外链防盗链与成本纪律 图片端点是六个端点里最贵的:每次成功请求 2 credits,是搜索端点的两倍;它的返回结构也最容易踩坑——你以为有 title,它经常没有;你以为数组一定在,它可能整个缺席。这篇把字段口径、请求路径和成本纪律一次讲清,给一个能直接跑的采集脚本。
先说两个最容易栽的点:端点路径是… · 2026/9/26 14:59:02
Zephyr与FreeRTOS线程优先级差异:从数值反转到调度机制全解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:59:02
Trash-ICRA19数据集实战:从标注到YOLOv8训练全流程 简介:这套YOLO目标检测配套数据包取自Trash-ICRA19海洋垃圾检测数据集,已经过统一整理,形成可被现有训练框架直接读取的标注格式,面向需要海洋漂浮物与水下废弃物检测样本的学生和研究人员,也适合计算机、电子信息、数… · 2026/9/26 14:59:02
中通顶象验证码逆向全解析:从抓包到风控加固 这两年做Web安全与反爬对抗,我绕不开一个东西——验证码。尤其是中通快递这类体量巨大的业务系统,页面上的顶象验证码几乎成了所有自动化脚本的第一道拦路虎。很多朋友在群里问“中通顶象验证码逆向怎么搞”,我今天不打算给一个所谓的“秒过方… · 2026/9/26 14:58:43
Atlas 300V部署YOLO实战:从驱动到调优的完整指南 有人问“Atlas 300V 24G到底是不是一张正常的运算加速卡”,这问题我一年前也问过自己。后来在昇腾环境上把YOLOv5、YOLOv8的推理部署完整跑通,才意识到大多数人对它的误解都来自用GPU的思维惯性。这篇文章就把Atlas上部署YOLO的完整流程、参数设置、调优… · 2026/9/26 14:58:43
Kimi 智能助手核心能力全景拆解:从配置到效果验证的 TaoToken 实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:58:25
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46