简介:基于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 1=1 --,拼接版SQL会直接把所有用户查出来,prepare方式则把整个输入当作一个参数值。第二,查询失败时只打印日志并返回空角色,不要崩掉整个程序;登录窗口可以根据返回值做“用户名或密码错误”的统一提示,不暴露具体是用户名错了还是密码错了,这也是答辩时能讲的安全习惯。
2.2 数据库表设计:五张核心表与字段取舍
一个教务系统的MySQL库,我通常建议不少于五张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 所有登录账号 | user_id, username, password, role, real_name, class_id |
| class | 班级 | class_id, class_name, major_name, grade |
| course | 课程 | course_id, course_name, teacher_id, credit, capacity |
| select_course | 学生选课与成绩 | id, student_id, course_id, score |
| system_log | 操作日志 | log_id, user_id, action, log_time |
user表里用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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个字段取舍值得多说一句。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 ¶ms = {}); // 通用的更新入口,insert/update/delete 用 bool executeUpdate(const QString &sql, const QVariantList ¶ms = {}); 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 ¶ms) { QSqlQuery query(m_db); query.prepare(sql); for (const QVariant ¶m : 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 ¶ms) { QSqlQuery query(m_db); query.prepare(sql); for (const QVariant ¶m : 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. Qt+MySQL避坑指南:乱码、驱动、事务与翻车现场
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(注意不是utf8,utf8在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默认autocommit=1,单条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里被废弃了,要用QRegularExpression;QTextCodec相关代码要改用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 'root'@'localhost' 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] host=127.0.0.1 port=3306 database=edu_system username=root password=123456程序启动时用QSettings读配置,这样演示时换数据库只需要改文件。这一步在答辩现场非常实用:老师换了一台电脑,或者数据库密码不同,你不用现场改代码重新编译,改两行配置就能跑起来。
6.2 自测清单与演示路径设计
答辩演示最怕的是临场翻车,翻车点多集中在:登录密码忘了、数据库没启动、演示到一半选了重复课程直接报错。按自测习惯给出一个最小清单:
| 操作 | 预期结果 | 自测状态 |
|---|---|---|
| 启动程序,不连数据库 | 提示连接失败而不是闪退 | 建议做 |
| 学生账号登录 | 进入学生窗口,看不到教师功能 | 必测 |
| 教师账号登录 | 只能看到自己教的课程 | 必测 |
| 管理员登录 | 可查看/编辑所有用户 | 必测 |
| 学生选同一门课两次 | 第二条被拒绝,提示已选过 | 必测 |
| 教师录入成绩后学生端查看 | 分数立即可见,未录入显示“未录入” | 必测 |
| 关闭程序再重开 | 登录窗口正常,不残留连接错误 | 建议做 |
演示路径的设计上,我倾向于一条线走完:管理员建账号、教师登录录入两门课的成绩、学生登录查看成绩并选新课、管理员看日志验证操作可追溯。这条路径把三种身份全部串起来,对应到数据库里就是几张表的数据都动了一遍,比零散地演示每个窗口更有说服力。
6.3 三个低成本加分项
第一个是密码哈希存储。用QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256)把密码摘要存入数据库,登录时先哈希再比较。这个改动只有两处:注册/改密时的写入逻辑、登录时的查询逻辑,工作量极小,但答辩时被问到“系统有哪些安全设计”回答起来很有底气。
第二个是操作日志的可视化。管理员端加一个日志查询Tab,展示system_log表里最近50条记录。演示时先做一次“修改学生资料”的操作,然后切到日志页面,指给老师看那里多了一条记录。这个细节答辩时非常加分,因为它证明你不是只写了增删改查,而是考虑了审计。
第三个是config.ini加一个remember_me字段,登录窗口记录上次登录的用户名,下次打开自动填充。这个功能代码量小、容易被注意到,属于典型的“低投入、高观感”改动。
打包测完一遍后还有一个容易被忽视的环节:把项目迁移到一台干净机器上跑一遍完整流程。很多坑——驱动缺失、MySQL版本不一致、插件目录不对——在这一步才会暴露。等真在答辩现场才翻车就晚了。按这套流程走下来,从选题到验收基本能在两周内完成主体功能,剩余时间都留给调试和写论文。希望帮到你。
本文还有配套的精品资源,点击获取