简介基于Qt与MySQL的教务系统毕业设计项目面向需要完成类似选题的高校学生与开发者。系统覆盖学生、教师、管理员三类身份包含登录、教务管理、页面交互等常见模块SQL脚本可直接用于建库适合用于课程设计、毕业设计或Qt数据库开发入门。压缩包共34个文件主要包含9个C源码、8个头文件、7个Qt界面文件以及数据库SQL、项目配置、README文档和少量图片素材整包仅97KB结构清楚便于按模块阅读和修改。目前已有259人学习下载。源码在本地编译通过按文档配置MySQL与Qt环境即可运行项目难度适中内容经助教老师审定能够帮助使用者快速理解Qt界面与数据库联动的基本思路。若遇到环境配置或运行问题还可联系作者获得解答。1. 基于Qt的教务系统图形界面加管理系统的组合值不值得做毕设选题选到“基于Qt的教务系统”的同学十有八九是被C课设磨过、又想避开算法深坑的人。这个题目最大的优点是它把界面编程、数据库设计、多角色权限和增删改查全占了难度中等工作量可见做完以后写论文、画架构图、做答辩都有实打实的东西可以讲。学生能查成绩能选课教师能录成绩能管课程管理员能管全部用户和数据——这套模型放在哪所学校都成立不需要你虚构业务场景。读者里有三种人最适合接着往下看一是已经选了Qt方向但不确定工作量的人二是想快速确认技术栈是否可行的人三是想在别人基础上把系统做得更完整、避开常见坑的人。这篇笔记从表结构讲到每个端口的核心代码再到打包发布按自己做这套方案的习惯完整写一遍。2. 系统拆解三种身份的权限模型与模块边界2.1 登录与身份路由权限判断放在哪一层教务系统所有功能的入口都是登录窗口。用户输入用户名和密码后程序查询数据库中的用户表拿到角色字段然后决定打开哪个主窗口。有一个常见的错误做法是只在前端用if(role student)来决定显示哪些按钮这样并不能真正保护接口——任何人只要能连上数据库就可以绕过界面直接执行SQL。我一般的做法是把权限判断分成两层第一层在界面层登录后根据角色打开对应的主窗口学生看不到教师的功能入口减少干扰。第二层在数据访问层执行增删改查的函数里校验调用者的角色例如改成绩的函数只允许role teacher或role admin的会话调用不满足直接返回错误码。界面层管体验数据访问层管安全。两层都做这个系统在答辩时被问到“如果学生修改界面代码怎么办”就能答得上来。登录路由的核心代码大约是这样的框架// login_handler.h #ifndef LOGIN_HANDLER_H #define LOGIN_HANDLER_H #include QString struct UserSession { int userId; QString username; QString role; // student / teacher / admin int classId; // 学生所属班级教师和管理员可为0 }; class LoginHandler { public: // 返回 UserSession失败时 role 为空字符串 static UserSession authenticate(const QString username, const QString password); }; #endif // LOGIN_HANDLER_H// login_handler.cpp #include login_handler.h #include QSqlQuery #include QSqlError #include QDebug UserSession LoginHandler::authenticate(const QString username, const QString password) { UserSession session; session.userId -1; session.role ; QSqlQuery query; query.prepare(SELECT user_id, username, role, class_id FROM user WHERE username ? AND password ?); query.addBindValue(username); query.addBindValue(password); if (!query.exec()) { qDebug() 登录查询失败: query.lastError().text(); return session; } // 用密码原文查询是简化做法 // 上线前应改为先对密码做哈希再比较。 if (query.next()) { session.userId query.value(0).toInt(); session.username query.value(1).toString(); session.role query.value(2).toString(); session.classId query.value(3).toInt(); } return session; }这段代码里有两点值得展开。第一SQL语句用prepare加占位符不用字符串拼接——这是防止SQL注入的最基本手段。学生名的输入框里如果输一个 OR 11 --拼接版SQL会直接把所有用户查出来prepare方式则把整个输入当作一个参数值。第二查询失败时只打印日志并返回空角色不要崩掉整个程序登录窗口可以根据返回值做“用户名或密码错误”的统一提示不暴露具体是用户名错了还是密码错了这也是答辩时能讲的安全习惯。2.2 数据库表设计五张核心表与字段取舍一个教务系统的MySQL库我通常建议不少于五张表表名用途关键字段user所有登录账号user_id, username, password, role, real_name, class_idclass班级class_id, class_name, major_name, gradecourse课程course_id, course_name, teacher_id, credit, capacityselect_course学生选课与成绩id, student_id, course_id, scoresystem_log操作日志log_id, user_id, action, log_timeuser表里用role字段区分三种身份而不是拆三张表原因是登录逻辑只需要一条SQL、一套账号体系代码更简单。class表单独拆出来是为了以后按班级查学生、按专业统计成绩时有统一入口。select_course是典型的关联表它承担了“学生选了什么课”和“这节课考了多少分”两个职责。system_log很多人不建但我建议建——答辩时演示“某次数据被修改后能追溯到谁操作的”比讲任何架构概念都有说服力。建表的SQL按常规做法给一个最小版CREATE DATABASE edu_system DEFAULT CHARACTER SET utf8mb4; USE edu_system; CREATE TABLE user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role ENUM(student, teacher, admin) NOT NULL, real_name VARCHAR(50) NOT NULL, class_id INT NULL, INDEX idx_class (class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE class ( class_id INT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(100) NOT NULL, major_name VARCHAR(100) NOT NULL, grade VARCHAR(10) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(3, 1) DEFAULT 2.0, capacity INT DEFAULT 60, INDEX idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE select_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5, 2) NULL, UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES user(user_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE system_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, action VARCHAR(200) NOT NULL, log_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个字段取舍值得多说一句。password字段在演示项目里可以存明文但最好从第一天起就养成做哈希的习惯Qt的QCryptographicHash类可以直接算MD5或SHA-256成本极低。score用DECIMAL(5,2)而不是INT是因为成绩可能带0.5这样的半分也方便将来存百分制的整数DECIMAL(5,2)最大能存999.99足够了。select_course表的联合唯一键(student_id, course_id)是防止同一个人重复选同一门课的最简单手段这一行建表语句能帮你挡掉不少程序里的重复逻辑。3. 用Qt连接MySQL从驱动加载到第一条查询3.1 Qt连MySQL的最小可运行代码Qt访问MySQL依赖QMYSQL驱动插件。很多同学第一次跑程序报QSqlDatabase: QMYSQL driver not loaded不是因为代码错了而是驱动库没找到。这里先给最小连接代码再解释驱动部署。// db_connection.cpp #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug bool initDatabaseConnection() { // 先取默认连接避免重复创建时报警告 QSqlDatabase db QSqlDatabase::contains(qt_sql_default_connection) ? QSqlDatabase::database(qt_sql_default_connection) : 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() 数据库连接失败: db.lastError().text(); return false; } QSqlQuery query(db); if (!query.exec(SELECT COUNT(*) FROM user)) { qDebug() 查询失败: query.lastError().text(); return false; } if (query.next()) { qDebug() user 表行数: query.value(0).toInt(); } return true; }这段代码的逻辑说明contains判断是为了防止同一个连接名被重复添加导致Qt打印“connection already exists”的警告。setHostName(127.0.0.1)表示本机MySQL如果你的数据库跑在别的机器上就改成对应IP。open()返回bool失败原因一定要用lastError().text()打印出来——最常见的两个失败原因后面避坑章节单独说。参数上建议把主机、端口、库名、用户名、密码全部提取成配置文件不要硬编码。常见做法是放进一个config.ini程序启动时用QSettings读取这样换一台电脑演示只需要改配置文件不用重新编译。3.2 把数据库操作封装成数据访问层连接代码放进main()里直接跑确实能通但每个窗口都写一遍连接逻辑就是灾难。更常见的做法是封装一个单例的DatabaseManager所有数据访问都通过它做。// database_manager.h #ifndef DATABASE_MANAGER_H #define DATABASE_MANAGER_H #include QSqlDatabase #include QSqlQuery #include QVariant #include QStringList class DatabaseManager { public: static DatabaseManager instance(); bool init(const QString host, int port, const QString dbName, const QString user, const QString password); QSqlDatabase connection() const; // 通用的查询执行入口select 用 QSqlQuery executeQuery(const QString sql, const QVariantList params {}); // 通用的更新入口insert/update/delete 用 bool executeUpdate(const QString sql, const QVariantList params {}); private: DatabaseManager() default; QSqlDatabase m_db; }; #endif // DATABASE_MANAGER_H// database_manager.cpp #include database_manager.h #include QSqlError #include QDebug DatabaseManager DatabaseManager::instance() { static DatabaseManager mgr; return mgr; } bool DatabaseManager::init(const QString host, int port, const QString dbName, const QString user, const QString password) { m_db QSqlDatabase::addDatabase(QMYSQL); m_db.setHostName(host); m_db.setPort(port); m_db.setDatabaseName(dbName); m_db.setUserName(user); m_db.setPassword(password); if (!m_db.open()) { qDebug() 数据库连接失败: m_db.lastError().text(); return false; } return true; } QSqlDatabase DatabaseManager::connection() const { return m_db; } QSqlQuery DatabaseManager::executeQuery(const QString sql, const QVariantList params) { QSqlQuery query(m_db); query.prepare(sql); for (const QVariant param : params) { query.addBindValue(param); } if (!query.exec()) { qDebug() SQL执行失败: query.lastError().text(); qDebug() SQL: sql; } return query; } bool DatabaseManager::executeUpdate(const QString sql, const QVariantList params) { QSqlQuery query(m_db); query.prepare(sql); for (const QVariant param : params) { query.addBindValue(param); } return query.exec(); }这个封装有几个实际好处。第一全项目只存在一个QSqlDatabase实例数据库连接在整个程序生命周期里保持打开不频繁开关连接。频繁开关MySQL连接非常耗时在本地连看不出来换到实验室服务器上就特别明显。第二executeQuery和executeUpdate统一处理了参数绑定和错误日志业务代码里不需要重复写prepare和addBindValue。第三后续想加连接池、改登录账号只需要改这一处。使用的时候业务窗口里只需要一行QSqlQuery query DatabaseManager::instance() .executeQuery(SELECT * FROM course WHERE teacher_id ?, {currentUserId});注意QVariantList里直接放数字和字符串都可以addBindValue会自动转换成MySQL需要的格式。最常见的错误是参数数量写错——SQL里两个?参数列表里只给了一个MySQL会报参数数量不匹配执行失败日志里能看到排查起来不费劲。4. 学生端、教师端、管理员端的核心业务代码4.1 学生端查成绩、选课与退课学生端的核心诉求基本是三块查自己的成绩、查看可选课程、选课退课。登录成功后UserSession.role是student程序打开StudentMainWindow窗口里三个Tab页对应三个功能。查成绩的SQL要关联三张表是学生端最典型的查询// student_score_query.cpp QSqlQuery query DatabaseManager::instance().executeQuery( SELECT c.course_name, c.credit, IFNULL(sc.score, 未录入) AS score FROM select_course sc JOIN course c ON sc.course_id c.course_id WHERE sc.student_id ?, {currentUserId}); QStandardItemModel *model new QStandardItemModel(this); model-setHorizontalHeaderLabels({课程名, 学分, 成绩}); int row 0; while (query.next()) { QString courseName query.value(course_name).toString(); QString credit query.value(credit).toString(); QString score query.value(score).toString(); model-setItem(row, 0, new QStandardItem(courseName)); model-setItem(row, 1, new QStandardItem(credit)); model-setItem(row, 2, new QStandardItem(score)); row; } ui-tableViewScore-setModel(model);这里IFNULL(sc.score, 未录入)的意思是如果成绩还是NULL——教师还没录入——就显示“未录入”而不是空白这个细节在演示时很讨喜。需要提一下成绩字段在数据库里是DECIMAL(5,2)如果直接查出来再转成toString()会带两位小数比如88.00如果想显示成整数可以在SQL里用CAST转换或者用QString::number(score.toDouble(), f, 0)处理这点看个人偏好。选课操作的典型逻辑里最容易漏掉的是容量检查。光靠界面上显示“剩余名额”不够多个学生同时选课时会产生超选。常见做法是用事务包裹两步先查course.capacity是否大于已选人数再插入select_course。事务的写法在避坑章节详说这里先看一个简化版本的选课函数// student_enroll.cpp bool enrollCourse(int studentId, int courseId) { QSqlDatabase db DatabaseManager::instance().connection(); db.transaction(); QSqlQuery query DatabaseManager::instance().executeQuery( SELECT COUNT(*) FROM select_course WHERE course_id ?, {courseId}); int selectedCount 0; if (query.next()) { selectedCount query.value(0).toInt(); } QSqlQuery courseQuery DatabaseManager::instance().executeQuery( SELECT capacity FROM course WHERE course_id ?, {courseId}); int capacity 0; if (courseQuery.next()) { capacity courseQuery.value(0).toInt(); } if (selectedCount capacity) { db.rollback(); return false; // 已满员 } QSqlQuery insertQuery DatabaseManager::instance().executeQuery( INSERT INTO select_course (student_id, course_id) VALUES (?, ?), {studentId, courseId}); if (insertQuery.lastError().isValid()) { db.rollback(); return false; } db.commit(); return true; }这个函数里注意两点第一容量检查和插入在同一个事务里如果只是单独执行两条SQL在并发场景下会超卖第二INSERT使用executeQuery而不是executeUpdate是因为需要检查lastError()是否有效这一点看个人封装方式有些人直接让executeUpdate返回bool就够。实际上对MySQL的INSERT来说只要SQL本身没问题失败多半是约束冲突——比如重复选课被联合唯一键挡住而这正是我们想要的行为。4.2 教师端录入成绩与课程管理教师端比学生端多一个“权限底色”一个教师只能管理自己负责的课程不能让老师随便改别人的成绩。所以教师端的核心查询几乎都带着WHERE teacher_id ?条件。教师端录入成绩的界面一般是一个表格左边列出该教师的所有课程右边显示选了这门课的学生列表和成绩输入框。保存成绩的代码是逐行更新// teacher_score_save.cpp void TeacherWindow::saveScores(int courseId) { QSqlDatabase db DatabaseManager::instance().connection(); db.transaction(); QSqlQuery updateQuery(db); updateQuery.prepare(UPDATE select_course SET score ? WHERE student_id ? AND course_id ?); // 假设 tableViewStudents 的每一行对应一个学生 for (int row 0; row model-rowCount(); row) { int studentId model-item(row, 0)-text().toInt(); QString scoreStr model-item(row, 2)-text(); bool ok false; double score scoreStr.toDouble(ok); updateQuery.addBindValue(score); updateQuery.addBindValue(studentId); updateQuery.addBindValue(courseId); if (!ok || !updateQuery.exec()) { db.rollback(); QMessageBox::warning(this, 保存失败, 第 QString::number(row 1) 行成绩格式错误); return; } } db.commit(); QMessageBox::information(this, 保存成功, 成绩已保存); }这个函数最值得说的是事务的范围覆盖了所有行。也就是说如果有20个学生第15个学生的成绩格式写错了前面14行的更新会全部回滚不会出现半个班有成绩、半个班没成绩的脏数据。对教师用户来说“要么全成功要么全失败”远比“能改一半”好理解。代价是如果数据量很大一个事务包住很多UPDATE会锁行时间变长但对于几百人的班级规模影响可以忽略。4.3 管理员端用户管理、课程审批与日志查看管理员端是最简单的因为大部分是单表增删改查没有业务联动。但有一个细节值得单独讲管理员修改用户资料时如果改动涉及password字段需要额外提示“是否重置密码”而不是直接覆盖。// admin_user_edit.cpp void AdminWindow::editUser(int userId) { QDialog dlg(this); // 表单里只有 real_name, role, class_id 三个字段 // 密码框留空表示不修改填了才更新 QSqlQuery query DatabaseManager::instance().executeQuery( SELECT username, real_name, role, class_id FROM user WHERE user_id ?, {userId}); if (query.next()) { // 把当前值填入表单 } if (dlg.exec() QDialog::Accepted) { QSqlQuery updateQuery DatabaseManager::instance().executeQuery( UPDATE user SET real_name ?, role ?, class_id ? WHERE user_id ?, {dlg.realName(), dlg.role(), dlg.classId(), userId}); // 如果密码框非空再执行一次密码更新 if (!dlg.password().isEmpty()) { QString hashed hashPassword(dlg.password()); DatabaseManager::instance().executeUpdate( UPDATE user SET password ? WHERE user_id ?, {hashed, userId}); } // 写入日志管理员修改了某用户信息 DatabaseManager::instance().executeUpdate( INSERT INTO system_log (user_id, action) VALUES (?, ?), {currentAdminId(), 修改用户 QString::number(userId) 的资料}); } }管理员端的日志写入是容易被忽略但又很重要的细节——答辩时如果能演示“数据库里任何一次关键修改都留有记录”整个系统的完整度会高一个档次。system_log表不需要做得很复杂记下操作人、干的事、时间就够了。这里也带出了管理员的隐藏职责他自己也要被日志记录不能有“管理员能改一切但不留痕”的设计这个理念在答辩时是加分项。5. QtMySQL避坑指南乱码、驱动、事务与翻车现场5.1 驱动加载失败QMYSQL driver not loaded现象程序编译通过运行时报QSqlDatabase: QMYSQL driver not loaded接着db.open()返回false。原因Qt安装时没有把qsqlmysql.dll插件编译出来或者编译了但程序找不到插件目录。Qt对数据库驱动的加载是运行时动态加载的路径不对就报这个错。尤其常见的是你的Qt是用MinGW编译的但MySQL的驱动是MSVC版两者不兼容或者反过来——这类驱动与编译器不匹配是纯玄学级别的坑稍不注意就在这一步耗半天。解决先确认自己的Qt套件是MinGW还是MSVC。常见的做法是打开Qt安装目录的plugins/sqldrivers看里面有没有qsqlmysql.dll或qsqlmysql.lib。如果没有就要用Qt自带的编译器重新编译MySQL驱动插件或者换一个带MySQL驱动的Qt版本。另外在代码里可以手动指定插件路径QCoreApplication::addLibraryPath(C:/Qt/6.5.0/mingw_64/plugins); QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL);这个做法能解决一部分“驱动明明在但找不到”的问题但治标不治本——真正原因还是插件目录没被自动扫描到。5.2 中文乱码显示成问号或繁体乱串现象Qt界面上显示的中文都是??????或者MySQL里存进去的中文变成乱码写出来的汉字读出来是乱的。原因有三个层级的字符集不统一任何一级出问题都会表现在界面上。第一层是数据库的默认字符集建库时如果没指定utf8mb4默认可能是latin1存中文必乱。第二层是Qt连接MySQL时没有指定字符集需要在连接建立后执行SET NAMES utf8mb4或通过连接参数指定。第三层是Qt源码文件本身的编码——如果源文件是GBK保存的而Qt编译器按UTF-8解析字符串字面量就乱了。解决建库时用utf8mb4注意不是utf8utf8在MySQL里是utf8mb3部分字符存不了代码里在打开连接后加上QSqlQuery query(db); query.exec(SET NAMES utf8mb4);源码文件统一用UTF-8编码保存并且在.pro文件里加一行QMAKE_CXXFLAGS /utf-8三条同时满足乱码基本绝迹。如果只改了数据库但Qt端没设SET NAMES最常见的现象是MySQL命令行里看中文正常但Qt界面上读出来全是问号。5.3 事务边界不清导致数据不一致现象上面教师端保存成绩的代码如果忘了写transaction()和commit()每执行一条UPDATE就自动提交了中途某一行失败前面的成绩已经落库——学生看到的就是“成绩录了一半”。原因MySQL默认autocommit1单条SQL自动提交。没有显式开启事务批量操作就没有原子性。解决像4.2节那样把循环和SQL包进db.transaction()和db.commit()之间。还要注意一个坑Qt的QSqlDatabase::transaction()对MySQL驱动是有效的但对某些驱动比如SQLite是无效操作。如果演示时换了数据库要确认对应驱动支持事务。同时事务里不要混入界面弹窗——不要把QMessageBox::warning写在事务中间等待用户点击因为MySQL连接在事务未提交时一直持有锁弹窗等用户点按钮其他会话就被锁住了。常见的做法是先检查数据合法性、再开事务执行、最后根据结果弹窗。5.4 Qt 5与Qt 6的API差异现象网上抄的代码在Qt 5能编译换到Qt 6报一堆错最常见的是QTextCodec头文件找不到、QRegExp被移除、某些QString::toStdString()的行为有变化。原因Qt 6做了大量清理旧版API被移除或不推荐使用。对教务系统这种场景核心问题集中在正则表达式和字符串处理上。Qt 5的QRegExp在Qt 6里被废弃了要用QRegularExpressionQTextCodec相关代码要改用QStringConverter。解决决定用哪个版本后整个项目统一按该版本的API写。如果是为了交作业我更倾向于Qt 5.15 LTS因为网上能找到的示例代码最多踩坑的答案也最全——写项目时最大的成本其实是查错而Qt 5的问题在中文社区几乎都有现成答案。Qt 6的新特性对教务系统来说用不上没必要为了新而新。5.5 连接被拒绝Host 127.0.0.1 is not allowed to connect现象代码完全没变数据库也从命令行能登录但Qt程序连不上报socket not open或Access denied。原因MySQL的root账号默认只允许localhost连接如果用IP地址连接可能被user表里的host字段限制。另一常见原因是你改了密码但MySQL缓存了认证插件老密码还在内存里。解决在MySQL命令行里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的新密码; FLUSH PRIVILEGES;如果确定要用IP连接则CREATE USER root% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON *.* TO root%; FLUSH PRIVILEGES;注意最后一条有安全边界root%允许任何IP连入只适用于开发机或完全可信的内网。在实验室环境里这么干问题不大但如果演示时网络环境不干净建议只对127.0.0.1开放或者新建一个专用账号只授权edu_system库的权限。6. 从能跑到答辩打包发布、自测清单与三个加分项6.1 打包发布windeployqt 与配置文件外置开发时在IDE里按F5能跑交到老师那里打不开是每届都有的事。Qt程序发布要带上动态库和插件目录。最常见的做法是用Qt自带的windeployqt工具# 先编译出 release 版 exe # 假设 build 目录下有教务系统.exe cd build C:/Qt/6.5.0/mingw_64/bin/windeployqt.exe 教务系统.exe这个命令会把Qt的DLL和插件自动复制到exe旁边MySQL驱动qsqlmysql.dll所在的sqldrivers目录也会被带上。执行完之后建议实测一次在有MySQL的机器上双击exe确认能弹登录窗口、能查数据。打包机上能跑只是第一步换台机器跑才见真章——最常见的问题是那台机器没装MySQL或没建数据库程序启动直接弹连接失败。我一般建议把数据库配置外置成一个config.ini[mysql] host127.0.0.1 port3306 databaseedu_system usernameroot password123456程序启动时用QSettings读配置这样演示时换数据库只需要改文件。这一步在答辩现场非常实用老师换了一台电脑或者数据库密码不同你不用现场改代码重新编译改两行配置就能跑起来。6.2 自测清单与演示路径设计答辩演示最怕的是临场翻车翻车点多集中在登录密码忘了、数据库没启动、演示到一半选了重复课程直接报错。按自测习惯给出一个最小清单操作预期结果自测状态启动程序不连数据库提示连接失败而不是闪退建议做学生账号登录进入学生窗口看不到教师功能必测教师账号登录只能看到自己教的课程必测管理员登录可查看/编辑所有用户必测学生选同一门课两次第二条被拒绝提示已选过必测教师录入成绩后学生端查看分数立即可见未录入显示“未录入”必测关闭程序再重开登录窗口正常不残留连接错误建议做演示路径的设计上我倾向于一条线走完管理员建账号、教师登录录入两门课的成绩、学生登录查看成绩并选新课、管理员看日志验证操作可追溯。这条路径把三种身份全部串起来对应到数据库里就是几张表的数据都动了一遍比零散地演示每个窗口更有说服力。6.3 三个低成本加分项第一个是密码哈希存储。用QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256)把密码摘要存入数据库登录时先哈希再比较。这个改动只有两处注册/改密时的写入逻辑、登录时的查询逻辑工作量极小但答辩时被问到“系统有哪些安全设计”回答起来很有底气。第二个是操作日志的可视化。管理员端加一个日志查询Tab展示system_log表里最近50条记录。演示时先做一次“修改学生资料”的操作然后切到日志页面指给老师看那里多了一条记录。这个细节答辩时非常加分因为它证明你不是只写了增删改查而是考虑了审计。第三个是config.ini加一个remember_me字段登录窗口记录上次登录的用户名下次打开自动填充。这个功能代码量小、容易被注意到属于典型的“低投入、高观感”改动。打包测完一遍后还有一个容易被忽视的环节把项目迁移到一台干净机器上跑一遍完整流程。很多坑——驱动缺失、MySQL版本不一致、插件目录不对——在这一步才会暴露。等真在答辩现场才翻车就晚了。按这套流程走下来从选题到验收基本能在两周内完成主体功能剩余时间都留给调试和写论文。希望帮到你。本文还有配套的精品资源点击获取