简介:基于Qt与MySQL构建的多角色教务管理系统,面向在校学生、授课教师与行政管理人员,适合作为毕业设计或数据库课程项目的完整参考。系统围绕权限分离设计,覆盖登录验证、选课、成绩查询与录入、课程安排、师生信息管理等核心流程,后端通过严谨表结构与事务机制保障数据一致性。压缩包共41个文件,大小约102KB,以9个cpp和8个h源码文件、7个ui界面定义、sql数据库脚本及pro工程文件为主,另含qrc资源文件、jpg/png展示截图、README说明及zbak备份文件,便于迅速梳理项目结构。目前已有54人浏览/学习。借助该资源可对照学习Qt界面组件与MySQL操作的整合方式,理解多角色菜单布局、管理员端增删改查以及完整教务业务逻辑,对准备同类系统设计与论文撰写的开发者有直接借鉴价值。
1. 为什么选了Qt与MySQL,这个组合在教务系统里还能再战十年
不少开发者一听到“管理系统”就默认走向B/S架构,但在机房断网、内网高并发、教师端需要高速批量录分的场景里,基于Qt与MySQL的多角色教务管理系统反而是更可靠的答案。我接过好几个校内这类项目的需求,它们共同的特点是:角色复杂(管理员、教师、学生)、权限边界清晰、数据量不大但录入操作频繁,而且对离线可用性有硬要求。这套系统解决的核心问题不是“功能多”,而是用C/S架构把权限隔离和录入效率做到位。适合谁?适合需要离线运行、批量导入成绩、在局域网内稳定跑几年的小型教务团队。本文按我实际落地这套系统的顺序展开,从数据库建模到Qt界面,从权限控制到排错,所有代码都可以直接照改。
2. 数据库建模与多角色权限设计:先定角色模型,再写建表语句
2.1 多角色权限模型选型:单一用户表还是“用户表+扩展表”?
很多第一次做教务系统的开发者会直接把学生、教师、管理员拆成三张互不相干的表,各自带一套登录字段。这个方案在登录时就要用“先查哪张表”来猜角色,后续加一个家长角色就要再建一套登录逻辑,属于典型的后续维护翻车现场。常见做法是采用RBAC思路:一张用户表存登录凭证和角色标识,再按角色拆扩展信息表。我的习惯是做成sys_user主表,用role_id区分角色,然后学生、教师各自的扩展字段放到独立表中,通过主键uid一一关联。
这样设计的好处很实际:登录时只需要一条SQL就能完成账号校验并拿到角色,不需要三次试探查询;学生和教师共用的姓名、性别、手机号等字段只维护一份;将来加新角色时,建一张扩展表,再插入一条角色记录就算完事,不需要动登录模块。代价是查询学生列表时要多一次JOIN,但教务系统单表数据量到不了几十万行,这个开销完全可以忽略。
2.2 核心表结构:DDL语句与字段类型选择说明
我这里的项目库名用edu_mgr,所有表都用utf8mb4字符集,排序规则用utf8mb4_unicode_ci,这样才能存生僻字和 emoji。成绩字段一定用DECIMAL,不要用FLOAT,这是多次踩坑换来的教训——浮点类型在累计平均分时会出现 89.5 变成 89.49999 的玄学误差。下面的SQL把这套系统的核心表建出来,包含角色表、用户表、学生扩展表、教师扩展表、课程表和选课成绩表。
CREATE DATABASE IF NOT EXISTS edu_mgr DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE edu_mgr; -- 角色表:角色编号和名称,权限判断时直接比对role_id CREATE TABLE role ( role_id TINYINT PRIMARY KEY, role_name VARCHAR(20) NOT NULL UNIQUE ) ENGINE=InnoDB COMMENT='角色表'; -- 用户主表:所有登录账号统一存放,密码存SHA-256摘要 CREATE TABLE sys_user ( uid INT PRIMARY KEY AUTO_INCREMENT, login_name VARCHAR(50) NOT NULL UNIQUE, pwd_hash CHAR(64) NOT NULL COMMENT 'SHA-256十六进制摘要', role_id TINYINT NOT NULL, real_name VARCHAR(50) NOT NULL, gender ENUM('M','F') DEFAULT 'M', phone VARCHAR(20) NULL, email VARCHAR(100) NULL, is_active TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES role(role_id) ) ENGINE=InnoDB COMMENT='用户主表'; -- 学生扩展表:与sys_user通过uid一对一关联 CREATE TABLE student ( uid INT PRIMARY KEY, student_no CHAR(12) NOT NULL UNIQUE COMMENT '学号', class_name VARCHAR(50) NOT NULL, grade_year SMALLINT NOT NULL COMMENT '入学年份', FOREIGN KEY (uid) REFERENCES sys_user(uid) ) ENGINE=InnoDB COMMENT='学生扩展信息'; -- 教师扩展表 CREATE TABLE teacher ( uid INT PRIMARY KEY, emp_no CHAR(10) NOT NULL UNIQUE COMMENT '工号', title VARCHAR(30) NULL COMMENT '职称', department VARCHAR(50) NULL, FOREIGN KEY (uid) REFERENCES sys_user(uid) ) ENGINE=InnoDB COMMENT='教师扩展信息'; -- 课程表:教师通过uid关联到sys_user CREATE TABLE course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, teacher_uid INT NOT NULL, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, max_students INT NOT NULL DEFAULT 0 COMMENT '0表示不限', FOREIGN KEY (teacher_uid) REFERENCES sys_user(uid) ) ENGINE=InnoDB COMMENT='课程表'; -- 选课成绩表:一个学生选一门课一条记录,成绩初始为空 CREATE TABLE enrollment ( enroll_id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, student_uid INT NOT NULL, semester VARCHAR(20) NOT NULL COMMENT '如2024-2025-1', score DECIMAL(5,2) NULL COMMENT '百分制,NULL表示未录入', status ENUM('selected','dropped') NOT NULL DEFAULT 'selected', UNIQUE KEY uk_course_student (course_id, student_uid, semester), FOREIGN KEY (course_id) REFERENCES course(course_id), FOREIGN KEY (student_uid) REFERENCES sys_user(uid) ) ENGINE=InnoDB COMMENT='选课与成绩表';这段DDL里有几个关键点需要说明。sys_user.pwd_hash设计成CHAR(64)是因为SHA-256摘要输出固定64位十六进制字符,比起直接存明文密码,即使数据库被拖走也无法反推出原密码。enrollment表加上uk_course_student联合唯一键,避免同一个学生在同一学期重复选同一门课——这是应用层代码很容易漏掉的约束。成绩字段允许NULL,因为选课和录入成绩发生在不同时间点,选课时创建记录、期末再回填分数,如果一开始就设置NOT NULL,那么选课接口就必须连带成绩一起提交,逻辑上说不通。
2.3 外键与索引的取舍:什么时候保留,什么时候去掉
MySQL的InnoDB引擎支持外键约束,它能保证enrollment表不会出现指向不存在课程的记录。但外键不是免费的,每次插入和更新时数据库都要做一致性检查,在高并发写入时会放大锁的竞争范围。教务系统这种每天几十次批量操作的量级,外键带来的额外开销基本无感,所以我建议保留。只有一种情况我会去掉外键——已经上线运行且需要做分库分表改造的老系统,外键在拆分时会变成巨大的障碍。
索引方面,除了唯一键之外,最需要加的是查询频率最高的字段。enrollment表的student_uid和semester要建普通索引,因为学生端成绩查询的过滤条件就是“当前登录的学生 + 某个学期”。course.teacher_uid也要建索引,教师端查看“我教的课程列表”是每次登录后的必发请求。索引不是越多越好,每个索引都会拖慢写入速度,对于这种规模的系统,每个表两到三个索引足够,切忌给所有字段无脑加索引。
3. Qt客户端骨架:数据库连接、登录验证与角色路由
3.1 Qt中连接MySQL的配置:驱动检查与连接参数
Qt通过QMYSQL驱动访问MySQL,但Qt安装包默认不附带这个插件,项目首次运行时常报 “QSqlDatabase: QMYSQL driver not loaded”。这个问题的根源不是代码写错,而是Qt的SQL插件目录下缺少对应的动态库。解决方法是确认Qt安装时选中了MySQL插件,或者在项目文件里显式引入。第一步先检查环境,在你的Qt安装目录的plugins/sqldrivers文件夹下,看是否存在qsqlmysql.dll或libqsqlmysql.so。没有的话,常见做法是重装Qt并勾选Qt SQL驱动模块,或者从源码编译该插件。
连接本身有固定的套路,需要在程序启动时创建连接,后续所有数据库操作复用这个连接。注意同一个连接不能跨线程使用,如果在工作线程里执行耗时查询,必须另外建立线程私有连接。
#include <QSqlDatabase> #include <QSqlError> #include <QMessageBox> // 建立MySQL连接的通用函数,调用一次后全局复用 bool initDatabaseConnection() { // addDatabase的第二个参数是连接名,默认使用空字符串 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("edu_mgr"); db.setUserName("db_user"); db.setPassword("db_passwd"); // 编码统一走utf8mb4,避免中文乱码 db.setConnectOptions("MYSQL_OPT_RECONNECT=1"); if (!db.open()) { QMessageBox::critical(nullptr, "数据库连接失败", db.lastError().text()); return false; } return true; }参数说明:addDatabase的第二个参数“连接名”在多连接场景下特别有用,比如一个窗口打开两个不同数据库时,可以通过连接名区分。MYSQL_OPT_RECONNECT=1是必须加的,MySQL默认在空闲超过8小时后会断开旧连接,加上这个选项让客户端自动重连,否则早上上班第一次点查询就报 “Lost connection”。生产环境我一般还会单独建一个低权限账号给程序使用,账号只授权edu_mgr库的增删改查权限,不会因为程序被破解而把整个数据库服务器的数据都暴露。
3.2 登录验证模块:参数化查询与密码哈希校验
登录是系统唯一的人口,代码质量直接决定整个应用的安全性。新手最容易犯的错误是拿用户输入的字符串直接拼接SQL,这就是典型的SQL注入入口。正确做法是使用QSqlQuery的prepare和addBindValue做参数化查询,让Qt驱动处理转义。密码校验时先对用户输入做 SHA-256 哈希,再和库里的pwd_hash比对,而不是把库里存的密文取出来做明文比较。
#include <QCryptographicHash> #include <QSqlQuery> #include <QVariant> // 登录校验函数:正确则返回true并设置成员变量currentUid和currentRole bool LoginWindow::validateUser(const QString &loginName, const QString &password) { QSqlQuery q; // 参数化查询,禁止直接拼接字符串 q.prepare("SELECT uid, role_id, pwd_hash FROM sys_user " "WHERE login_name = ? AND is_active = 1"); q.addBindValue(loginName); if (!q.exec()) { qDebug() << "登录查询失败:" << q.lastError().text(); return false; } if (!q.next()) { return false; // 账号不存在或已禁用,统一返回失败 } // 计算用户输入密码的SHA-256摘要,转成十六进制字符串 QByteArray inputHash = QCryptographicHash::hash( password.toUtf8(), QCryptographicHash::Sha256).toHex(); if (inputHash != q.value(2).toByteArray()) { return false; // 密码不匹配 } // 登录成功后把身份信息保存到当前会话 currentUid = q.value(0).toInt(); currentRole = q.value(1).toInt(); return true; }逻辑说明:这里用到了三段式写法——先prepare把SQL模板交给MySQL做语法解析,再用addBindValue填充值,最后exec执行。即使loginName里包含单引号或OR 1=1这样的恶意内容,它也会被当成一个普通字符串值处理,这是防御SQL注入的最基础手段。is_active = 1条件把已注销的账号挡在门外。密码比对用的是常量时间比较,虽然在这个场景下!=也能用,但遇到极端情况能避免通过时间差猜测哈希前缀。
3.3 主窗口菜单栏的按角色动态生成
登录成功后,接下来是按角色渲染主界面。我建议用QMenuBar动态生成菜单,把菜单结构放在一个配置里,不同角色加载不同菜单项。这样后面调整权限时只改配置,不用动窗口代码。下面这段代码实现了最基础的角色路由逻辑。
#include <QMenuBar> #include <QMenu> #include <QAction> void MainWindow::setupMenusByRole(int roleId) { QMenuBar *bar = this->menuBar(); bar->clear(); // 重新生成前先清空 // 管理员菜单:用户管理、课程管理、统计报表 if (roleId == 1) { QMenu *mgrMenu = bar->addMenu("系统管理"); mgrMenu->addAction("用户管理", this, &MainWindow::openUserMgr); mgrMenu->addAction("课程管理", this, &MainWindow::openCourseMgr); QMenu *statMenu = bar->addMenu("统计分析"); statMenu->addAction("成绩统计", this, &MainWindow::openScoreStat); } // 教师菜单:我的课程、成绩录入 if (roleId == 2) { QMenu *teachMenu = bar->addMenu("教学管理"); teachMenu->addAction("我的课程", this, &MainWindow::openMyCourses); teachMenu->addAction("成绩录入", this, &MainWindow::openScoreInput); } // 学生菜单:选课、成绩查询 if (roleId == 3) { QMenu *stuMenu = bar->addMenu("学习中心"); stuMenu->addAction("选课", this, &MainWindow::openCourseSelect); stuMenu->addAction("成绩查询", this, &MainWindow::openScoreQuery); } }这里addMenu创建顶级菜单,addAction挂接具体功能窗口的槽函数。角色判断用role_id硬编码虽然不够优雅,但对于只有三个角色的系统来说最直接。菜单项不只是一种界面入口,它本身就是权限的体现——学生界面上根本不出现“用户管理”这个动作,比在后端接口里做权限校验多了一层前端的天然隔离。当然,后端接口仍然要做权限验证,这就是后话。
4. 多角色教务功能落地:成绩录入、查询与统计
4.1 学生端成绩查询:QSqlQueryModel与QTableView配合
学生登录后最常点的就是成绩查询。Qt里展示SQL结果集最快的方式是QSqlQueryModel搭配QTableView,整个查询过程只写几行代码,不需要手动逐行填充QTableWidget。我先创建一个只读的QSqlQueryModel,绑定到界面上已有的QTableView,然后通过setQuery执行带过滤条件的SQL。注意这里仍然要用参数绑定,防止学号这类条件被篡改。
#include <QSqlQueryModel> #include <QTableView> void ScoreQueryWindow::loadScores(int studentUid, const QString &semester) { QSqlQueryModel *model = new QSqlQueryModel(this); QSqlQuery q; // 先准备SQL模板,再绑定参数 q.prepare("SELECT c.course_code, c.course_name, c.credit, e.score " "FROM enrollment e " "JOIN course c ON e.course_id = c.course_id " "WHERE e.student_uid = ? AND e.semester = ?"); q.addBindValue(studentUid); q.addBindValue(semester); if (!q.exec()) { QMessageBox::warning(this, "查询失败", q.lastError().text()); return; } model->setQuery(q); model->setHeaderData(0, Qt::Horizontal, "课程编号"); model->setHeaderData(1, Qt::Horizontal, "课程名称"); model->setHeaderData(2, Qt::Horizontal, "学分"); model->setHeaderData(3, Qt::Horizontal, "成绩"); ui->tableView->setModel(model); ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); }这段代码里有个容易被忽略的细节:QSqlQueryModel::setQuery接受一个QSqlQuery对象作为参数,执行完的查询对象会转入model管理。这样写的好处是prepare绑定逻辑和数据显示逻辑分离,排查问题时思路清晰。QHeaderView::Stretch设置让列宽自动撑满窗口,否则默认列宽可能连课程名都显示不全。另外,model不需要手动释放内存,this作为父对象会在窗口关闭时自动清理。
4.2 教师端成绩录入:事务保证写库不半途而废
成绩录入是教师端最核心的操作,我见过不少系统在学生成绩批量保存时出现“交了几十行数据,库里只进了十几行”的情况。原因是逐条执行INSERT或UPDATE,中途某条数据违反约束导致后面的全部中断,前面的又没回滚。解决办法是用数据库事务把这批操作包起来,要么全部成功,要么全部回滚。下面代码展示了常见做法:先准备好一条UPDATE语句,循环赋值并exec,最终统一提交。
#include <QSqlQuery> #include <QSqlDatabase> #include <QVariant> // 批量保存成绩:scores是一个map,key是enroll_id,value是成绩 bool ScoreInputWindow::saveScores(const QMap<int, double> &scores) { QSqlDatabase db = QSqlDatabase::database(); if (!db.transaction()) { QMessageBox::warning(this, "事务启动失败", db.lastError().text()); return false; } QSqlQuery q(db); q.prepare("UPDATE enrollment SET score = ? WHERE enroll_id = ?"); for (auto it = scores.constBegin(); it != scores.constEnd(); ++it) { q.addBindValue(it.value()); q.addBindValue(it.key()); if (!q.exec()) { // 某一条失败,回滚全部修改 db.rollback(); QMessageBox::warning(this, "保存失败", QString("记录 %1 更新失败: %2").arg(it.key()).arg(q.lastError().text())); return false; } } if (!db.commit()) { db.rollback(); QMessageBox::warning(this, "提交失败", db.lastError().text()); return false; } return true; }事务是这里最关键的机制,transaction()开启后,所有DML语句在同一个会话中执行,只有commit()成功后才真正落盘。一旦中间某条记录因为外键约束或数据类型错误而失败,立即调用rollback()把之前更新的数据全部还原。代码还需要处理一个问题:如果某个学生的成绩输入框为空,要跳过而不是写入NULL,因为成绩录入门关心的是“是否已录入”,空值和0分含义完全不同。建议录入界面上禁止空值直接提交,先做一次前端校验。
4.3 管理员端统计视图:用SQL聚合代替手工循环
管理员关心的核心问题是整体教学运行情况,比如某门课的及格率、某班级平均分。如果写代码去遍历每一条选课记录再手工累加,效率低且容易出错。更好的做法是把聚合计算交给MySQL,例如用一条SQL查询每个学期的课程平均分、选修人数和及格率,然后直接用QSqlQueryModel加载到表格控件展示。下面是一条典型的管理端统计SQL。
SELECT c.course_code, c.course_name, COUNT(e.enroll_id) AS student_count, ROUND(AVG(e.score), 2) AS avg_score, ROUND(SUM(CASE WHEN e.score >= 60 THEN 1 ELSE 0 END) / COUNT(e.enroll_id) * 100, 1) AS pass_rate FROM enrollment e JOIN course c ON e.course_id = c.course_id WHERE e.semester = ? GROUP BY e.course_id, c.course_code, c.course_name ORDER BY avg_score DESC;这条SQL把原来要在Qt里写几十行的逻辑浓缩成了数据库的一次执行。CASE WHEN子句是在统计满足条件记录数时的标准写法,在教师管理端需要特定成绩段数据时同样好使。一个经验是:涉及聚合的查询,放在数据库端做,自己写的循环通常比不过优化器的处理方式。QSqlQueryModel加载这样的查询结果非常简单,设置好表头就能显示,不需要在Qt里做任何二次计算。当数据量上升到几十万行时,数据库端聚合的速度优势更加明显,这也是把计算下推到数据库层最直接的收益。
5. 多角色教务系统避坑指南:驱动、乱码、并发与界面刷新
5.1 编译时提示QMYSQL驱动不加载,程序一启动就退出
现象:程序运行到QSqlDatabase::addDatabase("QMYSQL")后,db.open()返回false,错误信息是"QMYSQL driver not loaded",但MySQL服务本身是正常的。我接手的一个模拟项目X就因此卡了好几天,最后发现问题不是MySQL,而是Qt编译环境缺少MySQL客户端库。
原因:Qt的QMYSQL插件在编译时依赖MySQL的客户端库。如果系统里没装libmysqlclient或libmariadb,或者Qt安装时没有把插件一起编出来,运行时就会找不到驱动。
解决:先确认安装目录plugins/sqldrivers下有没有对应的qsqlmysql动态库。没有的话,两个办法选一个:一是重装Qt并勾选MySQL驱动模块;二是自己用Qt源码编译插件,编译时指定INCLUDE和LIB路径指向MySQL客户端库。编译成功后把生成的动态库复制到Qt的插件目录。另外注意32位和64位不能混用,Qt是64位,MySQL客户端库也必须是64位,否则同样加载失败。
5.2 界面显示中文变成问号或乱码
现象:数据库里SQL命令行的中文能正常显示,Qt界面上却全是?????,或者表中读出来是乱码。我曾经在开发一个图像处理Demo时遇到过类似问题,折腾了很久。
原因:多半是字符集不一致。Qt使用QString内部是UTF-16,MySQL客户端连接时默认字符集如果和数据库表的utf8mb4对不上,就会出现乱码。还有一种隐蔽情况,就是在setDatabaseName之前没设置连接字符集。
解决:在连接建立后执行SET NAMES utf8mb4,或者在setConnectOptions里加上MYSQL_SET_CHARSET_NAME=utf8mb4。所有表和库在建DDL时统一用utf8mb4,不要混用utf8与utf8mb4。如果项目是从旧库迁移来的,检查是不是有部分表建库时用了latin1,这种情况需要ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。
5.3 多个教师同时录入成绩,后提交的覆盖了先提交的
现象:两位教师在同一门课的不同班级录入成绩,A老师录完后,B老师一保存,A老师的数据没了。查看数据库,发现同一课程同一学生的成绩被后写的那批记录覆盖。
原因:这是典型的缺少“版本控制”的覆盖问题。成绩录入界面上如果只看了enrollment_id和分数,没有看该条记录是否已经被其他会话更新,后执行的UPDATE无条件覆盖了旧值。
解决:在界面层做好预检查,录入前判断数据库中的记录是否处于“未录入”的初始状态。更稳妥的做法是在course表加一个semester_status字段,表示当前学期是否已锁定成绩。成绩提交之前先SELECT score FROM enrollment WHERE enroll_id = ?,如果当前分数不是NULL,弹窗要求确认覆盖。另一种常见做法是利用事务加SELECT ... FOR UPDATE锁定记录,但在这个量级的系统里,用状态位加提示就够了。
5.4 QSqlQueryModel执行完查询后,界面上的表格不刷新
现象:第一次查询正常显示,第二次换了一个学期查询,表格里依然是第一次的数据。代码重新执行了setQuery,但界面没有变化。
原因:QSqlQueryModel在执行新查询后没有发出完整的dataChanged信号,或者表格视图的模型指针没有重新设置。还有一个陷阱:同一条QSqlQuery对象重复被setQuery使用时,旧结果不会被自动清除。
解决:每次查询都新建一个QSqlQueryModel,然后重新setModel到表格上。不要复用同一个model对象。如果是因更新数据后需要刷新视图,调用model->select()或重新执行查询并view->reset()。核心原则是:model的声明周期跟随窗口,不知道什么时候需要刷新就放弃复用,每次查询从头创建一个新model,虽然多了一行代码,但能避免大量界面显示问题。
5.5 账号被禁用或密码修改后,旧会话仍然可以继续操作
现象:管理员在后台把某位离职教师的账号禁用,但这个教师客户端仍然能进系统,甚至还能继续提交成绩。我还见到一个更隐蔽的问题:家长角色和教师角色共用同一套接口,改密码后老登录态未失效。
原因:登录成功后,把用户身份放在MainWindow的成员变量里,客户端没有在每次操作前重新校验账号状态。数据库端的is_active字段虽然改成0,但对已经登录的客户端来说,它并不知道这个变更。
解决:在每次执行敏感操作(成绩提交、用户管理)前,先查询一次sys_user.is_active,如果为0则强制退出到登录界面。更好的做法是给sys_user表加一个token_version字段,用户每次登录时生成一个随机会话编号存到内存,被禁用或改密时让token_version失效。客户端每次请求携带当前会话编号,服务器校验不匹配就拒绝响应。这套机制虽然要写几行代码,但能直接把权限控制从“界面隐藏”提升到“服务端拒绝”的级别。
6. 落地前的验证手段:造数据、写脚本、测权限,再放上线
系统写完后,我一般不会直接交给甲方使用,而是先进行三轮验证。第一轮是数据层验证,通过一段随机数据生成脚本,在edu_mgr库里造出一批学生、教师、课程和成绩记录,验证各表的唯一键和外键是否按预期工作。这轮验证的目标很直接:若数据量到几百条时出现存储异常或类型错误,说明DDL设计有问题,必须立刻返工。第二轮是操作层验证,分别用三种账号登录系统,把菜单能点的入口全部走一遍,检查不同角色之间会不会出现越权访问。我通常会在学生端手工输入一条管理员的接口URL,确认服务端会拒绝非法请求。第三轮是异常验证,模拟网络断开、MySQL服务重启、磁盘空间不足等场景,看系统是否会给用户清晰报错而不是直接崩溃。
这里我给出一个常用的小技巧:写一个Qt测试入口,在程序启动时加-selftest参数,自动执行下面这段逻辑——连接数据库,校验所有表是否存在,再插入一条测试用户并删除,全部通过时打印 “self test passed”,否则打印具体失败项,这样每次版本发布前可以先跑一遍冒烟测试。这是我个人强烈建议的做法。配合MySQL本身的事件调度器,可以写一个定时备份事件,每天凌晨把edu_mgr库导出一份SQL文件到备份目录,这个脚本必须在正式上线前配好,切不可等出了数据丢失事故再回头补。
另外要给代码留好逃生通道:所有密码不要硬编码写在main.cpp里,用一个单独的db_config.ini文件保存,并让程序启动时检查该文件权限。上线一段时间后再回头反思,这类C/S架构系统的最大优势是改动受控,最大风险也恰恰在这里——客户端更新麻烦,所以要有一个远程版本号检查机制,在登录前检查是否有新版本,有就提示下载。这算是我做过几个类似项目后养成的习惯,希望对你有帮助。
本文还有配套的精品资源,点击获取