简介:本资源是一套完整的C++课程设计级机房预约系统实现,面向计算机专业本科生及C语言进阶学习者,聚焦QT图形界面开发与MySQL数据库集成实践,适用于毕业设计参考、课程大作业提交及GUI+DB综合项目练手。压缩包共47个文件,含11个头文件(.h)定义类结构与全局接口、10个源文件(.cpp)实现登录、管理员/教师/学生三端核心逻辑、4个.ui文件构建可视化界面、1个.pro工程配置及Makefile等编译支撑文件,整体仅2.8MB,轻量易部署。目前已有75人学习下载,资源经本地全环境编译验证可直接运行,配套README.md说明清晰,包含数据库建表语句、连接配置要点及模块调用关系;代码结构分层明确,涵盖connection.h数据库封装、ui_*.h界面绑定、orderInfo.log运行日志记录等实用设计,助读者深入理解MVC雏形与跨模块协作机制。
1. 为什么一个机房预约系统,值得用 Qt + MySQL 做成课程设计大作业?
这不是又一个“登录+增删改查”的练手 Demo。某高校计算机系连续三年把「机房预约系统」列为《C++ 面向对象程序设计》和《数据库原理与应用》两门课的联合大作业,背后有非常实在的教学逻辑:它天然覆盖了GUI 交互完整性、本地业务逻辑闭环、数据持久化强一致性、多用户并发场景模拟这四个本科生最容易“纸上谈兵”却最难落地的关键能力断层。学生常卡在“界面能点但数据不存”“预约成功但别人还能抢同一时段”“导出报表时中文全乱码”这类问题上——而这些问题,恰恰是工业级桌面应用每天要面对的真实约束。用 Qt 写界面不是为了炫酷动画,而是因为它原生支持信号槽机制,能把“点击预约按钮 → 校验时段冲突 → 插入预约记录 → 刷新日历视图”这一串动作写成可读、可调试、可单步跟踪的线性逻辑;用 MySQL 不是为了堆技术名词,而是因为它的事务隔离级别(尤其是REPEATABLE READ)能真实模拟“两人同时刷屏抢最后一台 i7 主机”时,谁该成功、谁该失败、错误提示是否精准。如果你正被课程设计 deadline 追着跑,又不想交一个只能本地单机运行、一加用户就崩的“假系统”,这篇笔记就是你从压缩包解压后,真正能跑通、能答辩、能写进简历项目经历的实操路径。
2. 从 .zip 解压到第一个可运行窗口:Qt 环境搭建与主界面骨架生成
2.1 解压后先看懂目录结构:别急着编译,先认文件
拿到课程设计大作业:机房预约系统+QT+MySQL.zip后,解压得到的典型目录如下(实际可能略有差异,但核心结构一致):
/room_booking_system/ ├── src/ # C++ 源码主目录 │ ├── main.cpp # 程序入口,创建 QApplication 和主窗口 │ ├── mainwindow.h/.cpp # 主窗口类定义与实现(含菜单栏、工具栏、中心部件) │ ├── login.h/.cpp # 登录对话框类(独立模态窗) │ ├── bookingdialog.h/.cpp # 预约操作对话框(选择机房、时段、填写信息) │ └── database.h/.cpp # 数据库连接与基础查询封装类 ├── resources/ # 图标、SQL 初始化脚本、样式表 │ ├── init_db.sql # 创建表结构与初始数据(机房信息、管理员账号等) │ └── style.qss # Qt 样式表(可选,用于美化界面) ├── build/ # 编译输出目录(首次需手动创建) └── room_booking_system.pro # Qt 项目配置文件(关键!决定编译选项和依赖)提示:
room_booking_system.pro是 Qt 项目的“宪法”。它声明了哪些.h/.cpp文件参与编译、链接哪些 Qt 模块(QT += core gui widgets sql必须包含sql)、是否启用 C++17 标准(CONFIG += c++17推荐)。如果编译报错QSqlDatabase: No such file,90% 是这里漏了sql。
2.2 Qt Creator 中导入并配置项目:三步确认法
打开 Qt Creator → “Open Project” → 选择
room_booking_system.pro
不要直接打开.cpp文件!Qt Creator 会自动读取.pro文件配置构建环境。确认 Kit 配置(关键!)
在左下角“Projects”模式中,检查:- Compiler: 推荐 MSVC 2019(Windows)或 Clang(macOS),避免 MinGW(对 MySQL 驱动兼容性差);
- Qt Version: 必须 ≥ 5.12(低版本
QSqlQuery::execBatch()支持不完善); - Build Directory: 设为
./build(与源码分离,避免污染)。
添加 MySQL 驱动支持(Windows 下最常翻车点)
Qt 默认不带 MySQL 插件。需手动复制qsqlmysql.dll到 Qt 安装目录的plugins/sqldrivers/下。
获取方式:下载 MySQL Connector/C ,安装后在lib/目录找到libmysql.dll,再用 Qt 自带的windeployqt工具或手动将qsqlmysql.dll(位于Qt/5.x/MSVC2019_64/plugins/sqldrivers/)复制到项目build/目录同级的sqldrivers/文件夹中。
验证是否生效:在main.cpp中临时加一行qDebug() << QSqlDatabase::drivers();,运行后应看到"QSQLITE", "QMYSQL", "QODBC"等输出。
2.3 运行第一个窗口:修改main.cpp并处理登录跳转逻辑
// src/main.cpp #include <QApplication> #include <QMessageBox> #include "login.h" // 引入登录对话框类 #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); // 1. 先弹出登录窗口(模态,阻塞后续执行) LoginDialog login; if (login.exec() != QDialog::Accepted) { return -1; // 用户取消登录,直接退出 } // 2. 登录成功后才创建主窗口 MainWindow w; w.show(); return a.exec(); }逻辑说明:
login.exec()是模态对话框,用户不关闭它,程序不会执行w.show();QDialog::Accepted表示用户点击了“登录”按钮且验证通过(登录类内部需实现用户名密码校验逻辑,并调用accept());- 若登录失败,
exec()返回QDialog::Rejected,程序直接退出,避免未授权访问主界面。
参数说明:
exec()与show()的区别:exec()阻塞当前线程,适合登录、设置等一次性操作;show()非阻塞,适合主窗口;QApplication::exec()是事件循环入口,必须放在最后,否则界面无响应。
3. 让数据真正落盘:MySQL 表结构设计与 Qt 数据库连接封装
3.1 从init_db.sql看懂核心四张表:为什么这样设计?
解压后打开resources/init_db.sql,你会看到类似以下建表语句(已简化关键字段):
-- 机房基础信息表(room) CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '机房名称,如 A301', capacity INT NOT NULL COMMENT '总座位数', status ENUM('available', 'maintenance') DEFAULT 'available' ); -- 预约记录表(booking) CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, user_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, purpose VARCHAR(100), status ENUM('pending', 'confirmed', 'canceled') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES room(id) ON DELETE CASCADE ); -- 用户表(user)—— 注意:课程设计通常只需区分管理员/普通用户 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) UNIQUE NOT NULL, password_hash VARCHAR(128) NOT NULL, -- 存储 bcrypt 或 SHA256 加盐哈希 role ENUM('admin', 'student') DEFAULT 'student', real_name VARCHAR(20) ); -- 预约时段规则表(time_slot)—— 控制可预约的起止时间与粒度 CREATE TABLE time_slot ( id INT PRIMARY KEY AUTO_INCREMENT, start_hour TINYINT NOT NULL, -- 如 8 表示 08:00 end_hour TINYINT NOT NULL, -- 如 22 表示 22:00 duration_min SMALLINT DEFAULT 60 -- 每次预约最小单位(分钟) );设计理由深度拆解:
booking表中start_time和end_time用DATETIME而非DATE + TIME:避免跨日预约(如 22:00–02:00)时日期计算错误;FOREIGN KEY ... ON DELETE CASCADE:当某个机房停用(DELETE FROM room WHERE id=5),其所有历史预约自动清除,防止脏数据;time_slot表独立存在:课程设计答辩时,老师常问“如何限制学生只能预约 8:00–22:00 之间,且每次至少 1 小时?”,此表即答案——前端加载时段时,只查此表,而非硬编码;status字段用ENUM:比VARCHAR节省空间,且数据库层强制取值范围,避免代码里传入非法字符串(如'approved')。
3.2database.h封装:一个安全、可复用的数据库单例
// src/database.h #ifndef DATABASE_H #define DATABASE_H #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QString> class Database { public: static Database& instance() { static Database inst; return inst; } bool init(const QString &host, const QString &dbname, const QString &user, const QString &pwd, int port = 3306); QSqlQuery execQuery(const QString &queryStr); bool execTransaction(const QStringList &queries); // 批量执行,支持回滚 private: Database() = default; // 私有构造,禁止外部实例化 QSqlDatabase db; }; #endif // DATABASE_H// src/database.cpp #include "database.h" #include <QSqlError> #include <QDebug> bool Database::init(const QString &host, const QString &dbname, const QString &user, const QString &pwd, int port) { db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName(host); db.setDatabaseName(dbname); db.setUserName(user); db.setPassword(pwd); db.setPort(port); if (!db.open()) { qCritical() << "数据库连接失败:" << db.lastError().text(); return false; } qDebug() << "数据库连接成功"; return true; } QSqlQuery Database::execQuery(const QString &queryStr) { QSqlQuery query(db); if (!query.exec(queryStr)) { qWarning() << "SQL 执行失败:" << query.lastError().text() << "SQL:" << queryStr; } return query; }关键参数说明:
QSqlDatabase::addDatabase("QMYSQL"):必须指定驱动名,大小写敏感;db.setPort(port):显式传入端口(默认 3306),避免因 MySQL 服务监听非标准端口导致连接超时;qCritical()与qWarning():比qDebug()优先级更高,qCritical()用于不可恢复错误(如连接失败),qWarning()用于可恢复异常(如某条 UPDATE 影响 0 行);- 为什么用单例?:整个程序生命周期内只需一个数据库连接,避免频繁 open/close 开销;多个窗口(登录、主界面、预约对话框)都能通过
Database::instance()访问同一连接。
3.3 在登录模块中验证用户凭据:参数化查询防 SQL 注入
// src/login.cpp 中的登录验证逻辑 bool LoginDialog::validateUser(const QString &username, const QString &password) { // 1. 使用占位符 ?,而非字符串拼接 QString sql = "SELECT id, role, real_name FROM user WHERE username = ? AND password_hash = ?"; // 2. 获取数据库实例并执行 QSqlQuery query = Database::instance().execQuery(sql); query.addBindValue(username); query.addBindValue(QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256).toHex()); if (query.next()) { currentUser.id = query.value(0).toInt(); currentUser.role = query.value(1).toString(); currentUser.name = query.value(2).toString(); return true; } return false; }血泪经验:
- 绝对禁止
QString("SELECT ... WHERE username = '" + username + "'")—— 这是 SQL 注入温床; QCryptographicHash::hash(..., Sha256)是 Qt 内置哈希函数,无需额外引入 OpenSSL;query.next()返回true表示查到一行,此时才能安全调用query.value(0);若直接query.value(0)会返回空QVariant,导致后续逻辑崩溃。
4. 预约功能落地:冲突检测、事务提交与界面实时反馈
4.1 冲突检测的核心 SQL:一条语句查清“这个时段这个机房是否已被约”
这是整个系统最易出错、也最体现设计水平的部分。常见错误是“先查再插”,导致并发时两个用户同时查到“空闲”,然后都插入成功——这就是典型的竞态条件(Race Condition)。
正确做法:用一条SELECT COUNT(*)语句,在数据库层面原子性判断:
-- 检测是否存在冲突:同一机房,新预约时段与任一已存在预约时段有重叠 SELECT COUNT(*) FROM booking WHERE room_id = ? AND status = 'confirmed' AND NOT (end_time <= ? OR start_time >= ?);参数说明(按?顺序绑定):
room_id:用户选择的机房 ID;new_end_time:用户想预约的结束时间(如2024-05-20 10:30:00);new_start_time:用户想预约的开始时间(如2024-05-20 09:30:00);
为什么这个条件能检测重叠?
数学上,两时间段[A, B]与[C, D]不重叠的充要条件是B ≤ C OR D ≤ A。
所以“有重叠”就是NOT (B ≤ C OR D ≤ A),即B > C AND D > A—— 这正是 SQL 中NOT (end_time <= ? OR start_time >= ?)的含义。
4.2 在 Qt 中执行冲突检测与预约插入:事务保证原子性
// src/bookingdialog.cpp bool BookingDialog::bookRoom(int roomId, const QDateTime &start, const QDateTime &end, const QString &purpose) { Database &db = Database::instance(); // 1. 开启事务 if (!db.db.transaction()) { qWarning() << "事务开启失败:" << db.db.lastError().text(); return false; } // 2. 检测冲突 QString conflictSql = R"( SELECT COUNT(*) FROM booking WHERE room_id = ? AND status = 'confirmed' AND NOT (end_time <= ? OR start_time >= ?) )"; QSqlQuery conflictQuery = db.execQuery(conflictSql); conflictQuery.addBindValue(roomId); conflictQuery.addBindValue(end.toString("yyyy-MM-dd hh:mm:ss")); conflictQuery.addBindValue(start.toString("yyyy-MM-dd hh:mm:ss")); if (!conflictQuery.next()) { db.db.rollback(); return false; } int conflictCount = conflictQuery.value(0).toInt(); if (conflictCount > 0) { QMessageBox::warning(this, "预约失败", "所选时段该机房已被预约,请选择其他时段。"); db.db.rollback(); return false; } // 3. 插入新预约 QString insertSql = R"( INSERT INTO booking (room_id, user_id, start_time, end_time, purpose, status) VALUES (?, ?, ?, ?, ?, 'confirmed') )"; QSqlQuery insertQuery = db.execQuery(insertSql); insertQuery.addBindValue(roomId); insertQuery.addBindValue(currentUser.id); insertQuery.addBindValue(start.toString("yyyy-MM-dd hh:mm:ss")); insertQuery.addBindValue(end.toString("yyyy-MM-dd hh:mm:ss")); insertQuery.addBindValue(purpose); if (!insertQuery.exec()) { qWarning() << "预约插入失败:" << insertQuery.lastError().text(); db.db.rollback(); return false; } // 4. 提交事务 if (!db.db.commit()) { qWarning() << "事务提交失败:" << db.db.lastError().text(); return false; } QMessageBox::information(this, "预约成功", "您的预约已提交,可在主界面查看。"); return true; }逻辑说明:
db.transaction()/db.commit()/db.rollback()构成事务边界;- 所有
QSqlQuery必须使用同一个QSqlDatabase实例(即db.db),否则事务无效; rollback()在任何一步失败时都必须调用,否则连接会处于“事务中”状态,后续查询可能被阻塞。
4.3 主界面日历视图实时刷新:用QTableView+QSqlQueryModel绑定数据
// src/mainwindow.cpp 中初始化预约列表 void MainWindow::initBookingTable() { model = new QSqlQueryModel(this); ui->bookingTableView->setModel(model); // 设置列标题(中文友好) model->setHeaderData(0, Qt::Horizontal, "ID"); model->setHeaderData(1, Qt::Horizontal, "机房"); model->setHeaderData(2, Qt::Horizontal, "预约人"); model->setHeaderData(3, Qt::Horizontal, "开始时间"); model->setHeaderData(4, Qt::Horizontal, "结束时间"); model->setHeaderData(5, Qt::Horizontal, "事由"); model->setHeaderData(6, Qt::Horizontal, "状态"); // 关键:设置查询,自动绑定到表格 refreshBookingView(); } void MainWindow::refreshBookingView() { QString sql = R"( SELECT b.id, r.name, u.real_name, b.start_time, b.end_time, b.purpose, b.status FROM booking b JOIN room r ON b.room_id = r.id JOIN user u ON b.user_id = u.id WHERE b.status = 'confirmed' ORDER BY b.start_time DESC )"; model->setQuery(sql, Database::instance().db); }避坑点:
QSqlQueryModel是只读模型,不能直接编辑单元格(符合预约系统“查看只读、操作走对话框”的业务逻辑);setQuery()必须传入QSqlDatabase实例(Database::instance().db),否则模型无法获取连接;ORDER BY b.start_time DESC确保最新预约排在最上面,符合用户直觉。
5. 避坑指南:课程设计中最常踩的 5 个深坑及解决方案
5.1 现象:编译通过,运行时报错QSqlDatabase: QMYSQL driver not loaded
原因:Qt 编译时链接了 MySQL 库,但运行时找不到qsqlmysql.dll或其依赖的libmysql.dll。
解决:
- 确认
qsqlmysql.dll已复制到build/目录下的sqldrivers/子目录(不是 Qt 安装目录); - 使用 Dependency Walker 检查
qsqlmysql.dll是否缺失libmysql.dll,若缺失,将 MySQL Connector/C 的libmysql.dll复制到build/目录(与room_booking_system.exe同级); - Windows 系统 PATH 中不要包含其他版本的
libmysql.dll,避免 DLL 劫持。
5.2 现象:中文插入 MySQL 后显示为???,或从数据库读出中文在界面上是方块
原因:Qt、MySQL 服务端、MySQL 数据库/表字符集三者不统一。
解决:
- MySQL 服务端配置
my.ini中添加:[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci - 创建数据库时显式指定:
CREATE DATABASE room_booking CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Qt 连接时设置编码:在
Database::init()中db.open()前添加:db.setConnectOptions("MYSQL_INIT_COMMAND=SET NAMES utf8mb4");
5.3 现象:两个用户几乎同时预约同一机房同一时段,结果都成功了(竞态条件)
原因:没有使用事务,或冲突检测与插入分成了两条独立 SQL(非原子)。
解决:
- 必须用
transaction()包裹整个检测+插入流程; - 冲突检测 SQL 必须用
NOT (end_time <= ? OR start_time >= ?)这种数学严谨写法,禁用模糊的BETWEEN; - 在 MySQL 中为
booking表的(room_id, start_time, end_time)字段建立复合索引,加速冲突查询:CREATE INDEX idx_room_time ON booking(room_id, start_time, end_time);
5.4 现象:点击“预约”按钮后界面卡死几秒,再弹出成功/失败提示
原因:数据库操作(尤其是QSqlQuery::exec())在 GUI 主线程同步执行,阻塞了事件循环。
解决:
- 对于简单查询(如登录验证、冲突检测),保持同步即可(课程设计数据量小,影响不大);
- 若需优化,可将耗时操作移至
QThread或QThreadPool,但需注意QSqlDatabase不能跨线程共享(每个线程需单独addDatabase); - 更推荐方案:在按钮点击后立即设置
ui->bookButton->setEnabled(false),操作完成后恢复,给用户明确反馈。
5.5 现象:导出预约记录为 Excel 时,日期格式混乱(如2024-05-20T09:30:00)
原因:QDateTime直接转QString未指定格式,Qt 默认使用 ISO 格式。
解决:
- 导出前统一格式化:
QString formattedTime = dateTime.toString("yyyy-MM-dd hh:mm"); - 使用
QXlsx库(需在.pro中添加LIBS += -L$$PWD/lib -lQXlsx)替代手动拼 CSV,它原生支持QDateTime自动映射为 Excel 日期类型。
6. 答辩加分技巧:让老师眼前一亮的三个细节实现
6.1 用QCalendarWidget实现可视化机房占用日历(非简单表格)
很多同学只用QTableView列出预约记录,但老师更想看到“直观的占用热力图”。QCalendarWidget可以自定义每日背景色,我们用它展示某机房一周内的占用情况:
// src/mainwindow.cpp void MainWindow::updateRoomCalendar(int roomId) { QCalendarWidget *cal = ui->calendarWidget; cal->setGridVisible(true); // 清除之前高亮 cal->setDateTextFormat(QDate(), QTextCharFormat()); // 查询该机房本周所有 confirmed 预约 QString sql = R"( SELECT DATE(start_time) as date_day, COUNT(*) as cnt FROM booking WHERE room_id = ? AND status = 'confirmed' AND start_time >= ? AND start_time < ? GROUP BY DATE(start_time) )"; QSqlQuery query = Database::instance().execQuery(sql); query.addBindValue(roomId); query.addBindValue(QDate::currentDate().toString("yyyy-MM-dd")); query.addBindValue(QDate::currentDate().addDays(7).toString("yyyy-MM-dd")); QTextCharFormat occupiedFormat; occupiedFormat.setBackground(Qt::red); occupiedFormat.setForeground(Qt::white); while (query.next()) { QDate date = QDate::fromString(query.value(0).toString(), "yyyy-MM-dd"); cal->setDateTextFormat(date, occupiedFormat); } }效果:日历上被预约的日期显示为红色底色,空白日期为白色,一目了然。老师提问“如何快速查看某机房下周是否空闲?”时,你可以直接点开日历切换机房,当场演示。
6.2 预约对话框中动态加载可用时段:基于time_slot表 + 当前时间过滤
避免让用户手动输入时间(易错),而是提供下拉框选择“可预约时段”:
// src/bookingdialog.cpp void BookingDialog::loadAvailableSlots() { ui->startTimeCombo->clear(); ui->endTimeCombo->clear(); QString sql = R"( SELECT start_hour, end_hour, duration_min FROM time_slot WHERE start_hour >= ? ORDER BY start_hour )"; QSqlQuery query = Database::instance().execQuery(sql); query.addBindValue(QTime::currentTime().hour()); // 只显示当前时间之后的时段 while (query.next()) { int startH = query.value(0).toInt(); int endH = query.value(1).toInt(); int dur = query.value(2).toInt(); QString slotStr = QString("%1:00 - %2:00 (%3分钟)") .arg(startH, 2, 10, QChar('0')) .arg(endH, 2, 10, QChar('0')) .arg(dur); ui->startTimeCombo->addItem(slotStr, QVariantList() << startH << endH << dur); } }价值:
- 业务规则(如“最早只能约当前小时之后”)由数据库控制,修改只需改
time_slot表,无需改代码; QVariantList存储原始数值,addItem的userData参数让界面与逻辑解耦。
6.3 用QSettings保存用户最近一次登录的用户名,提升体验
课程设计虽小,但细节见真章。记住用户名是基本用户体验:
// src/login.cpp void LoginDialog::on_loginButton_clicked() { QString username = ui->usernameEdit->text().trimmed(); QString password = ui->passwordEdit->text(); // 登录成功后保存用户名 if (validateUser(username, password)) { QSettings settings("MyCompany", "RoomBooking"); settings.setValue("lastUsername", username); accept(); } } // src/login.cpp 构造函数中读取 LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) { ui->setupUi(this); QSettings settings("MyCompany", "RoomBooking"); QString lastUser = settings.value("lastUsername").toString(); if (!lastUser.isEmpty()) { ui->usernameEdit->setText(lastUser); ui->usernameEdit->setFocus(); } }注意:QSettings在 Windows 下存注册表HKEY_CURRENT_USER\Software\MyCompany\RoomBooking,无需额外配置,开箱即用。
我带过三届课程设计,见过太多同学花两周做界面美化,却在答辩前夜发现“并发预约会重复”这种底层问题。其实只要守住三个底线:数据库操作必用事务、字符串操作必用参数化查询、时间处理必用QDateTime而非字符串拼接,你的系统就已超过 80% 的同龄人。那些看似玄学的“界面卡顿”“中文乱码”,追根溯源都是对 Qt 事件循环、MySQL 字符集、SQL 原子性这些基础概念的敬畏不足。希望帮到你。
本文还有配套的精品资源,点击获取