ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QT+SQL教室管理系统:排课冲突检测与数据库设计实战

QT+SQL教室管理系统:排课冲突检测与数据库设计实战 简介这是一套基于Qt与SQL数据库开发的教室管理系统完整源码面向计算机相关专业学生及企业员工可用于课程设计、毕业设计、大作业或初期项目立项演示也适合作为Qt界面编程与数据库操作的实战练习素材。压缩包共70个文件约9.17MB以cpp源文件、h头文件、o编译产物为主另含Makefile构建脚本、README说明文档及可执行程序覆盖主窗口、菜单、教室、员工、用户、报修、空教室查询等多个功能模块目录结构清晰便于按模块阅读与二次开发。目前已有102人学习下载。读者可从中获取完整的项目工程结构、Qt信号槽与界面布局实现、SQL数据库连接与增删改查逻辑以及多窗口跳转与权限管理的组织方式对理解桌面端管理系统的开发流程具有较高的借鉴价值。1. 教室管理系统为什么总在“排课冲突”上翻车排课冲突这件事做过教室管理系统的人都懂——它不是简单的“查一下有没有人用”而是时间、教室、教师、班级四个维度同时约束的组合问题。用 QT SQL 数据库做教室管理系统核心价值就在于把这种多维约束落到一张可查询、可事务回滚的数据表上而不是靠 Excel 手工比对。这套方案适合两类人一是课程设计或实训阶段需要交付完整桌面应用的开发者二是想把“界面 数据库”这条链路真正跑通、理解事务和索引在实际项目里怎么用的工程师。源码和项目说明打包在一起意味着你拿到的不只是界面还有建表语句、查询逻辑和模块划分思路。下面我按“建库建表 → QT 连接 → 核心查询 → 冲突检测 → 避坑 → 进阶”的顺序把这条链路拆开讲清楚。2. 从建库到建表教室管理系统的数据模型怎么定2.1 四张核心表与字段设计教室管理系统的数据模型不需要花哨但字段类型和约束必须一次定对否则后面改表结构比重新写界面还痛苦。我一般会拆成四张表教室表、教师表、课程表、排课记录表。教室表存容量、类型普通/多媒体/机房教师表存工号、姓名、可授课程课程表存课程名、班级、周次排课记录表是核心存教室ID、教师ID、课程ID、星期几、第几节、起止周。-- 教室表容量和类型决定后续排课筛选条件 CREATE TABLE classroom ( room_id INTEGER PRIMARY KEY AUTOINCREMENT, room_name TEXT NOT NULL UNIQUE, -- 如 A101 capacity INTEGER NOT NULL DEFAULT 0, room_type TEXT NOT NULL DEFAULT 普通, -- 普通/多媒体/机房 building TEXT ); -- 排课记录表冲突检测的核心用联合唯一索引兜底 CREATE TABLE schedule ( schedule_id INTEGER PRIMARY KEY AUTOINCREMENT, room_id INTEGER NOT NULL, teacher_id INTEGER NOT NULL, course_id INTEGER NOT NULL, week_day INTEGER NOT NULL, -- 1~7 section INTEGER NOT NULL, -- 1~5 节 start_week INTEGER NOT NULL, end_week INTEGER NOT NULL, FOREIGN KEY (room_id) REFERENCES classroom(room_id) );逻辑说明room_name加 UNIQUE 是为了防止同一间教室被重复录入schedule表不直接加“教室时间”的唯一约束因为起止周是区间同一教室不同周次可以复用唯一约束会误杀。参数上week_day用 1~7 而不是字符串查询时比较效率高section用整数方便做“第几节到第几节”的区间判断。2.2 索引加在哪三个必须建的索引很多人建完表就急着写 QT 界面结果排课查询一卡就是几秒。血泪经验是索引要在写查询之前建好。排课冲突检测最常用的过滤条件是教室、星期、节次所以至少建三个索引。-- 按教室星期节次查冲突覆盖最频繁的查询路径 CREATE INDEX idx_schedule_room_time ON schedule(room_id, week_day, section); -- 按教师查是否时间冲突 CREATE INDEX idx_schedule_teacher_time ON schedule(teacher_id, week_day, section); -- 按课程查排课情况 CREATE INDEX idx_schedule_course ON schedule(course_id);逻辑说明联合索引的顺序很关键room_id在前是因为教室冲突是最高频的检测场景。如果反过来把week_day放最前按教室查时就用不上索引前缀。参数上SQLite 对联合索引的左侧前缀生效所以(room_id, week_day, section)能同时服务“只查教室”和“查教室星期”两种条件。注意不要给每个字段单独建索引写入时会拖慢插入速度排课批量导入时差异很明显。2.3 用事务保证批量排课的原子性排课往往是一次导入一个班级整学期的课中途失败不能留半截数据。常见做法是用事务包住批量插入任何一条冲突就整体回滚。import sqlite3 def batch_insert_schedule(conn, records): records: list of (room_id, teacher_id, course_id, week_day, section, start_week, end_week) try: conn.execute(BEGIN) for r in records: # 先查冲突再插入避免脏数据 conflict conn.execute( SELECT COUNT(*) FROM schedule WHERE room_id? AND week_day? AND section? AND NOT (end_week ? OR start_week ?), (r[0], r[3], r[4], r[5], r[6]) ).fetchone()[0] if conflict 0: raise ValueError(f教室冲突: {r}) conn.execute( INSERT INTO schedule VALUES (NULL,?,?,?,?,?,?,?), r ) conn.commit() except Exception as e: conn.rollback() print(批量排课失败已回滚:, e)逻辑说明BEGIN显式开启事务rollback保证要么全成功要么全失败。冲突查询里的NOT (end_week ? OR start_week ?)是区间不重叠的标准写法意思是“新排课的周次区间和已有记录有交集”。参数上start_week和end_week传的是新记录的起止周顺序不能反否则区间判断会失效。这个函数在 QT 里可以放在一个独立的数据库访问类中界面层只负责收集数据。3. QT 连接 SQL 数据库从驱动加载到界面绑定3.1 QSqlDatabase 的驱动选择与连接串QT 连数据库有两条路一是用 QSqlDatabase 走 ODBC 或原生驱动二是直接调 SQLite 的 C 接口。教室管理系统数据量不大我一般直接用 QSQLITE 驱动零配置、单文件、方便打包。// main.cpp 中初始化数据库连接 #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug bool initDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(classroom.db); // 数据库文件放在可执行文件同目录 if (!db.open()) { qDebug() 打开数据库失败: db.lastError().text(); return false; } QSqlQuery query; // 开启外键约束SQLite 默认关闭 query.exec(PRAGMA foreign_keys ON); return true; }逻辑说明addDatabase(QSQLITE)注册驱动setDatabaseName指定文件路径。注意 SQLite 默认不启用外键必须手动执行PRAGMA foreign_keys ON否则删教室时不会拦截关联的排课记录。参数上数据库文件名建议用相对路径打包发布时把 .db 文件放在 exe 同级目录避免绝对路径在别人机器上找不到。3.2 用 QSqlTableModel 把排课表绑到 QTableView界面层最省事的做法是用 QSqlTableModel 直接绑 QTableView增删改查几乎不用写 SQL。但排课表涉及多表关联QSqlTableModel 只适合单表所以我的做法是教室表、教师表用 QSqlTableModel排课记录用 QSqlQueryModel 加自定义查询。// 排课记录用自定义查询带教室名和教师名 QSqlQueryModel *model new QSqlQueryModel(this); model-setQuery( SELECT s.schedule_id, c.room_name, t.teacher_name, co.course_name, s.week_day, s.section, s.start_week, s.end_week FROM schedule s JOIN classroom c ON s.room_id c.room_id JOIN teacher t ON s.teacher_id t.teacher_id JOIN course co ON s.course_id co.course_id ORDER BY s.week_day, s.section ); model-setHeaderData(0, Qt::Horizontal, 编号); model-setHeaderData(1, Qt::Horizontal, 教室); model-setHeaderData(2, Qt::Horizontal, 教师); ui-tableView-setModel(model);逻辑说明setQuery里用 JOIN 把 ID 换成可读名称界面直接显示“A101 / 张老师 / 数据结构”。setHeaderData改表头避免界面出现英文字段名。参数上ORDER BY week_day, section让课表按时间顺序排列符合使用习惯。注意 QSqlQueryModel 是只读的如果要支持界面直接编辑得换成 QSqlTableModel 或自己写提交逻辑。3.3 参数化查询防止注入与乱码QT 里拼接 SQL 字符串是新手最容易翻车的地方尤其是课程名带单引号时直接报错。正确做法是用 QSqlQuery 的 bindValue。QSqlQuery query; query.prepare(INSERT INTO course (course_name, class_name) VALUES (?, ?)); query.bindValue(0, ui-courseEdit-text()); query.bindValue(1, ui-classEdit-text()); if (!query.exec()) { qDebug() 插入课程失败: query.lastError().text(); }逻辑说明preparebindValue把参数和 SQL 结构分离既防注入又自动处理引号转义。参数上bindValue的索引从 0 开始也可以用具名绑定:name。中文乱码问题通常出在数据库编码SQLite 默认 UTF-8QT 的 QString 也是 UTF-8一般不会乱如果从外部 CSV 导入要确认文件本身是 UTF-8 而不是 GBK。4. 排课冲突检测一条 SQL 查清四维约束4.1 教室冲突、教师冲突、班级冲突的判定逻辑排课冲突本质是“同一时间段内同一资源不能被占用两次”。资源有教室、教师、班级三类判定逻辑完全一样只是过滤字段不同。下面这条 SQL 一次查出某教室在某天某节是否已被占用。-- 检测指定教室在指定时间是否冲突 SELECT s.schedule_id, c.room_name, co.course_name FROM schedule s JOIN classroom c ON s.room_id c.room_id JOIN course co ON s.course_id co.course_id WHERE s.room_id ? AND s.week_day ? AND s.section ? AND NOT (s.end_week ? OR s.start_week ?);逻辑说明四个问号分别是教室ID、星期、节次、新排课起始周、新排课结束周。NOT (end_week start OR start_week end)是判断两个闭区间是否重叠的标准表达式。参数上如果查教师冲突把s.room_id ?换成s.teacher_id ?查班级冲突则 JOIN 课程表后按class_name过滤。注意周次区间是闭区间第 1 周到第 16 周和第 16 周到第 18 周在第 16 周是重叠的这个边界最容易漏。4.2 用 QT 封装冲突检测函数并返回可读提示界面点“保存”时不能只弹“失败”要告诉用户跟哪条记录冲突。我一般封装一个返回冲突详情的函数。struct ConflictInfo { bool hasConflict; QString detail; }; ConflictInfo checkRoomConflict(int roomId, int weekDay, int section, int startWeek, int endWeek) { QSqlQuery query; query.prepare( SELECT c.room_name, co.course_name, s.start_week, s.end_week FROM schedule s JOIN classroom c ON s.room_id c.room_id JOIN course co ON s.course_id co.course_id WHERE s.room_id ? AND s.week_day ? AND s.section ? AND NOT (s.end_week ? OR s.start_week ?) ); query.addBindValue(roomId); query.addBindValue(weekDay); query.addBindValue(section); query.addBindValue(startWeek); query.addBindValue(endWeek); query.exec(); if (query.next()) { return {true, QString(与 %1 的《%2》冲突第%3-%4周) .arg(query.value(0).toString()) .arg(query.value(1).toString()) .arg(query.value(2).toInt()) .arg(query.value(3).toInt())}; } return {false, }; }逻辑说明返回结构体而不是布尔值界面层可以直接把detail弹给用户。query.next()只取第一条冲突记录实际项目里可以循环取全部。参数上addBindValue按顺序对应问号顺序不能错。这个函数在插入前调用插入后再查一次做二次确认防止并发写入。4.3 批量排课时的冲突预检与回滚策略一个班级整学期几十条排课逐条弹窗确认不现实。我的做法是先全部预检收集所有冲突一次性展示用户确认后再走事务插入。QListQString preCheckConflicts(const QListScheduleRecord records) { QListQString conflicts; for (const auto r : records) { auto info checkRoomConflict(r.roomId, r.weekDay, r.section, r.startWeek, r.endWeek); if (info.hasConflict) { conflicts.append(QString(第%1条: %2).arg(records.indexOf(r)1).arg(info.detail)); } } return conflicts; }逻辑说明预检阶段只读不写把所有冲突收集完再决定是否继续。参数上records.indexOf(r)在循环里效率不高数据量大时改用下标遍历。注意预检和实际插入之间有时间差如果多人同时操作插入时还要再查一次这就是“二次确认”的意义。5. 教室管理系统开发避坑五条血泪经验5.1 现象排课保存后界面不刷新重启才看到新数据原因QSqlTableModel 或 QSqlQueryModel 没有重新执行查询模型缓存了旧结果。解决插入成功后调用model-select()或重新setQueryQSqlQueryModel 需要重新设置查询语句才能刷新。5.2 现象删除教室时报外键错误但明明没关联排课原因SQLite 外键约束默认关闭之前插入的脏数据没被拦截现在开启后历史数据成了孤儿记录。解决先执行PRAGMA foreign_keys ON再用DELETE FROM schedule WHERE room_id NOT IN (SELECT room_id FROM classroom)清理孤儿数据。5.3 现象中文课程名在界面上显示为问号原因数据库连接或文件编码不是 UTF-8常见于从 GBK 环境迁移过来的数据。解决确认 .db 文件本身是 UTF-8QT 源码文件保存为 UTF-8 带 BOM 或无 BOM 均可但要在main里设置QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8))。5.4 现象排课查询越来越慢几千条后要等好几秒原因没建索引或者索引顺序不对导致全表扫描。解决用EXPLAIN QUERY PLAN看执行计划确认走了idx_schedule_room_time。如果显示SCAN TABLE schedule说明索引没命中检查 WHERE 条件字段顺序是否和索引前缀一致。5.5 现象批量导入中途失败数据库里留了半截数据原因没有用事务每条 INSERT 自动提交。解决用BEGIN和COMMIT包住批量操作异常时ROLLBACK。注意 SQLite 的BEGIN要显式写QT 的QSqlDatabase::transaction()也可以但嵌套事务不支持别在事务里再开事务。6. 进阶技巧用视图和触发器把冲突检测下沉到数据库6.1 建一个冲突视图查询直接复用与其在 QT 里反复写那段 JOIN 加区间判断不如在数据库里建一个视图把“当前所有排课及其时间占用”固化下来。CREATE VIEW v_schedule_detail AS SELECT s.schedule_id, c.room_name, t.teacher_name, co.course_name, co.class_name, s.week_day, s.section, s.start_week, s.end_week FROM schedule s JOIN classroom c ON s.room_id c.room_id JOIN teacher t ON s.teacher_id t.teacher_id JOIN course co ON s.course_id co.course_id;逻辑说明视图不存数据只保存查询定义QT 里直接SELECT * FROM v_schedule_detail WHERE room_name? AND week_day?即可。参数上视图里的字段名就是 QT 里query.value(room_name)能取到的列名比数字索引可读。注意视图不能建索引复杂查询还是靠基表索引。6.2 用触发器拦截冲突插入更狠的做法是在schedule表上建 BEFORE INSERT 触发器冲突直接RAISE(ABORT)这样无论从 QT 还是命令行插入都拦得住。CREATE TRIGGER trg_schedule_conflict BEFORE INSERT ON schedule FOR EACH ROW BEGIN SELECT RAISE(ABORT, 教室时间冲突) WHERE EXISTS ( SELECT 1 FROM schedule WHERE room_id NEW.room_id AND week_day NEW.week_day AND section NEW.section AND NOT (end_week NEW.start_week OR start_week NEW.end_week) ); END;逻辑说明NEW代表即将插入的行EXISTS子查询检测是否有重叠记录有则RAISE(ABORT)终止插入并返回错误信息。参数上触发器只拦教室冲突教师和班级冲突可以再建两个触发器或者合并到一个里用 OR 连接。注意触发器会增加每次插入的开销批量导入时如果预检已经做过可以临时DROP TRIGGER再重建但生产环境不建议这么干。6.3 验证方法用 EXPLAIN 和手工造数据压测改完索引或触发器别凭感觉说“快了”。用EXPLAIN QUERY PLAN看是否走索引再手工造 5000 条排课记录用 QT 的QElapsedTimer测查询耗时。QElapsedTimer timer; timer.start(); // 执行冲突检测查询 qDebug() 冲突检测耗时: timer.elapsed() ms;逻辑说明QElapsedTimer是 QT 自带的毫秒级计时器比QTime精度高。参数上5000 条记录下冲突检测应在 10ms 以内超过 50ms 说明索引有问题。我自己的习惯是每次改完 SQL 都跑一遍这个计时不靠“感觉快了”下结论。这套 QT SQL 的教室管理系统真正值钱的地方不是界面多漂亮而是数据模型和冲突检测逻辑经得起批量排课的考验。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进