☰
QT+Mysql医院预约管理系统实战:从环境搭建到避坑指南
2026/10/10 3:07:24 网站建设 项目流程

简介:这是一套基于Qt与MySQL开发的医院预约管理系统完整项目资料,面向计算机相关专业的在校学生、教师及企业开发者,可用于毕业设计、课程设计、作业提交或项目初期立项演示,也适合希望进阶学习桌面端开发的小白。压缩包共47个文件,约31KB,以15个cpp源文件、15个h头文件与12个ui界面文件为核心,另含pro工程文件、user配置、README说明及授权码文本,覆盖登录、注册、挂号、选科室、选时间、医生与管理员管理等模块,目录结构清晰,便于按功能模块检索与二次开发。项目已通过测试运行,功能完整,并附详细文档与全部资料,答辩评审分达95分,代码可直接修改扩展。目前已有62人学习关注,适合需要完整参考实现与排错思路的读者下载使用。

1. 从一张挂号单说起:QT+Mysql 的医院预约管理系统到底在解决什么

如果你在门诊大厅蹲过一上午,会发现最耗时间的环节往往不是医生问诊,而是排队、填表、反复确认科室和号源。一个基于 QT + Mysql 的医院预约管理系统,本质上就是把这条线下链路搬到桌面端:患者端完成注册、选科室、选医生、选时段、提交预约,管理端完成号源维护、预约审核、数据统计。它不追求高并发分布式,而是追求“单机可跑、逻辑闭环、数据可查”,非常适合作为课程设计、毕业设计或桌面端管理系统的练手项目。

这套方案适合三类人:一是正在找完整 QT 项目练手的学生,二是需要交一份“能演示、能讲清、能改”的桌面应用作业的开发者,三是想用 QT 快速验证一个业务管理流程的工程师。它的技术栈不复杂,但坑不少,尤其是数据库连接、时间冲突判断和界面与数据同步这三块,翻车率极高。下面我按“先跑通、再讲透、最后避坑”的顺序,把整套落地路径拆开讲。

2. 环境搭不起来,后面全是空谈:QT 与 Mysql 的选型与连接

2.1 为什么是 QT + Mysql,而不是别的组合

桌面端管理系统的选型,核心看三点:界面开发效率、数据库部署成本、跨平台需求。QT 的 Widgets 模块对表格、表单、弹窗的支持非常成熟,拖拽式布局加信号槽机制,能让一个预约界面的交互逻辑在几百行内写完。Mysql 则是课程设计和中小型项目里最常见的关系型数据库,安装简单、文档多、社区答案密集,遇到问题容易搜到解决方案。

对比另外两种常见组合:C# + SQL Server 在 Windows 上很顺,但跨平台和部署成本高;Python + SQLite 开发快,但界面美观度和打包体积往往不理想。QT + Mysql 的平衡点在于:C++ 的性能足够,QT 的界面能力足够,Mysql 的数据管理能力足够,三者叠加后,一个完整的预约系统可以在单机环境下跑通全部核心流程。

常见做法是:QT 版本选 5.14 或 6.x 的 LTS 版本,Mysql 选 8.0 社区版,编译器用 MinGW 或 MSVC 都可以,但要注意 QT 的 Mysql 驱动版本必须和 Mysql 客户端库匹配。我一般会先确认 QT 安装时勾选了 Sources 和 Mysql 驱动相关组件,否则后面编译驱动会多花一两个小时。

2.2 用 QSqlDatabase 建立连接的最小可跑代码

下面这段代码是连接 Mysql 的最小闭环,包含驱动加载、连接参数设置和失败提示。把它放在 main.cpp 或主窗口初始化里都能跑。

#include <QSqlDatabase> #include <QSqlError> #include <QDebug> bool connectToMysql() { // 加载 QMYSQL 驱动,若返回空说明驱动未编译或路径不对 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); // 本机数据库,远程则改 IP db.setPort(3306); // Mysql 默认端口 db.setDatabaseName("hospital_appointment"); // 提前建好的库名 db.setUserName("root"); // 建议新建专用账号,不要直接用 root db.setPassword("your_password"); if (!db.open()) { qDebug() << "连接失败:" << db.lastError().text(); return false; } qDebug() << "数据库连接成功"; return true; }

逻辑说明:addDatabase("QMYSQL")是告诉 QT 用哪个驱动,如果这里报“Driver not loaded”,说明 QT 的 Mysql 驱动没有编译或没有放到插件目录。参数说明:setHostName和setPort决定连哪台机器,setDatabaseName必须对应一个已经存在的库,setUserName和setPassword建议单独建一个只有增删改查权限的账号,避免用 root 导致权限过大。

2.3 建库建表:三张核心表撑起整个预约流程

预约系统的数据模型不复杂,但表结构设计直接影响后续查询和冲突判断。我一般会建三张核心表:用户表、医生排班表、预约记录表。

CREATE DATABASE IF NOT EXISTS hospital_appointment DEFAULT CHARSET utf8mb4; USE hospital_appointment; -- 用户表:患者和管理员共用,用 role 区分 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0 -- 0 患者,1 管理员 ); -- 医生排班表:每个医生每天每个时段一条记录 CREATE TABLE schedules ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(50) NOT NULL, department VARCHAR(50) NOT NULL, work_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL, -- 如 "08:00-09:00" max_num INT DEFAULT 10, booked_num INT DEFAULT 0, UNIQUE KEY uk_doctor_date_slot (doctor_name, work_date, time_slot) ); -- 预约记录表:关联用户和排班 CREATE TABLE appointments ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, schedule_id INT NOT NULL, status TINYINT DEFAULT 0, -- 0 待确认,1 已确认,2 已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (schedule_id) REFERENCES schedules(id) );

逻辑说明:users表用role字段区分患者和管理员,避免建两张表增加登录逻辑复杂度。schedules表用唯一索引uk_doctor_date_slot防止同一医生同一时段重复排班,这是后面做冲突判断的第一道防线。appointments表用外键关联用户和排班,保证数据一致性。参数说明:utf8mb4支持中文和特殊字符,booked_num和max_num配合使用可以控制号源上限。

提示:建表时先把外键和唯一索引加上,后面写业务代码会省很多校验逻辑。如果先建表后加索引,已有脏数据可能导致索引创建失败。

3. 预约流程的代码落地:从登录到提交的完整链路

3.1 登录与角色分流:一个信号槽搞定界面跳转

登录界面是用户接触的第一个环节,逻辑不复杂但容易在角色判断上写乱。我一般会在登录按钮的槽函数里完成三件事:查库、比对密码、根据角色打开不同窗口。

void LoginDialog::onLoginClicked() { QString username = ui->editUser->text().trimmed(); QString password = ui->editPwd->text().trimmed(); if (username.isEmpty() || password.isEmpty()) { QMessageBox::warning(this, "提示", "用户名和密码不能为空"); return; } QSqlQuery query; query.prepare("SELECT id, role FROM users WHERE username = ? AND password = ?"); query.addBindValue(username); query.addBindValue(password); if (!query.exec() || !query.next()) { QMessageBox::warning(this, "提示", "用户名或密码错误"); return; } int userId = query.value("id").toInt(); int role = query.value("role").toInt(); if (role == 1) { AdminWindow *w = new AdminWindow(); w->show(); } else { PatientWindow *w = new PatientWindow(userId); w->show(); } this->close(); }

逻辑说明:prepare加addBindValue是防 SQL 注入的标准写法,不要用字符串拼接。参数说明:trimmed()去掉首尾空格,避免用户误输入空格导致登录失败。角色分流用role字段判断,患者窗口需要把userId传进去,后面查预约记录时要用。

3.2 号源查询与时间冲突判断:两个必须写对的 SQL

患者选科室、选医生、选日期后,需要查出可用号源。这里有两个关键点:一是只查还有余号的排班,二是提交预约时要再次检查是否已约过同一时段。

// 查询某医生某天的可用号源 QSqlQuery query; query.prepare("SELECT id, time_slot, max_num, booked_num " "FROM schedules " "WHERE doctor_name = ? AND work_date = ? AND booked_num < max_num " "ORDER BY time_slot"); query.addBindValue(doctorName); query.addBindValue(date); query.exec(); while (query.next()) { QString slot = query.value("time_slot").toString(); int remain = query.value("max_num").toInt() - query.value("booked_num").toInt(); // 把 slot 和 remain 填入下拉框或表格 }

逻辑说明:booked_num < max_num是过滤已满号源的核心条件,ORDER BY time_slot保证时段按顺序展示。参数说明:doctorName和date来自界面选择,remain是剩余号源数,可以直接显示给患者。

提交预约时,必须再查一次该用户是否已经约过同一医生同一天同一时段,避免重复提交。

// 提交前检查重复预约 QSqlQuery check; check.prepare("SELECT COUNT(*) FROM appointments a " "JOIN schedules s ON a.schedule_id = s.id " "WHERE a.user_id = ? AND s.doctor_name = ? " "AND s.work_date = ? AND s.time_slot = ? AND a.status != 2"); check.addBindValue(userId); check.addBindValue(doctorName); check.addBindValue(date); check.addBindValue(timeSlot); check.exec(); check.next(); if (check.value(0).toInt() > 0) { QMessageBox::warning(this, "提示", "您已预约过该时段,请勿重复提交"); return; }

逻辑说明:status != 2表示已取消的预约不算重复,允许重新预约。参数说明:COUNT(*)返回匹配记录数,大于 0 就拦截。这个检查必须放在事务里,和后面的插入操作一起提交,否则并发下仍可能重复。

3.3 事务提交:预约插入与号源更新必须原子化

预约提交涉及两个写操作:插入预约记录、更新排班表的已约人数。这两个操作必须在一个事务里,否则可能出现“预约记录有了但号源没扣”的脏数据。

QSqlDatabase::database().transaction(); QSqlQuery insert; insert.prepare("INSERT INTO appointments (user_id, schedule_id, status) VALUES (?, ?, 0)"); insert.addBindValue(userId); insert.addBindValue(scheduleId); QSqlQuery update; update.prepare("UPDATE schedules SET booked_num = booked_num + 1 " "WHERE id = ? AND booked_num < max_num"); update.addBindValue(scheduleId); if (insert.exec() && update.exec() && update.numRowsAffected() == 1) { QSqlDatabase::database().commit(); QMessageBox::information(this, "成功", "预约提交成功"); } else { QSqlDatabase::database().rollback(); QMessageBox::warning(this, "失败", "号源不足或系统繁忙,请重试"); }

逻辑说明:update语句里的booked_num < max_num是第二道防线,即使前面查的时候有余号,提交瞬间被别人抢走也会更新失败,numRowsAffected() == 1确保确实扣减了号源。参数说明:transaction()开启事务,commit()提交,rollback()回滚。这个写法在单机环境下足够可靠,并发再高就需要加锁或改用乐观锁版本号。

注意:QT 的 QSqlQuery 在事务中如果某条语句失败,后续语句可能继续执行,所以必须用if判断每一步的返回值,不能只看最后一条。

4. 避坑指南:数据库连接、时间处理和界面同步的五个血泪教训

4.1 坑一:QMYSQL 驱动加载失败,报“Driver not loaded”

现象:程序启动后连接数据库直接失败,QSqlDatabase::drivers()里看不到 QMYSQL。

原因:QT 安装时没有编译 Mysql 驱动,或者编译出来的qsqlmysql.dll没有放到sqldrivers插件目录。

解决:先确认 QT 安装目录下plugins/sqldrivers里有没有qsqlmysql.dll。如果没有,需要手动编译驱动:找到 QT 源码里的qtbase/src/plugins/sqldrivers/mysql,用 qmake 生成 Makefile,编译后把生成的 dll 复制到插件目录。编译时要在.pro文件里指定 Mysql 的头文件和库路径,否则会报找不到mysql.h。

4.2 坑二:日期格式不匹配,查询结果永远为空

现象:界面选的日期是2025-03-15,数据库里存的也是2025-03-15,但查询就是查不到。

原因:QT 的QDateEdit控件默认返回QDate类型,直接toString()可能带出本地化格式,比如2025/3/15,和数据库的DATE类型不匹配。

解决:统一用toString("yyyy-MM-dd")格式化后再传给 SQL。插入时也用同样的格式,不要依赖数据库的自动转换。我一般会在工具类里写一个formatDate函数,所有日期都走这个函数,避免各处写法不一致。

4.3 坑三:界面表格不刷新,数据改了但看不到

现象:管理员在后台改了排班,患者端刷新按钮点了没反应,表格还是旧数据。

原因:QT 的QTableWidget或QTableView不会自动监听数据库变化,必须手动重新查询并重置模型。

解决:每次刷新时先clearContents()再重新查库填充,或者用QSqlTableModel加select()重新加载。如果数据量大,建议用QSqlQueryModel配合分页查询,避免一次性加载全部记录导致界面卡顿。

4.4 坑四:中文乱码,姓名和科室显示成问号

现象:数据库里存的中文,在 QT 界面显示成???或乱码。

原因:Mysql 连接字符集不是utf8mb4,或者 QT 的编码设置不对。

解决:在连接成功后立即执行SET NAMES utf8mb4,确保客户端和服务器字符集一致。QT 侧在main.cpp里设置QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"))。建库建表时也要指定DEFAULT CHARSET utf8mb4,三层都对齐才能彻底解决。

4.5 坑五:外键约束导致删除失败,但错误提示看不懂

现象:管理员想删除一个用户,操作失败,QT 只报“无法执行查询”。

原因:appointments表有外键指向users,直接删用户会触发外键约束。

解决:要么先删该用户的预约记录再删用户,要么把外键改成ON DELETE CASCADE。我一般建议软删除,给users表加一个is_deleted字段,删除时只改标记,不物理删除,这样历史预约记录还能保留。

5. 进阶技巧:用视图和存储过程把统计查询从界面层剥离

5.1 为什么要把统计逻辑下沉到数据库

预约系统跑起来后,管理员最常问的是“今天哪个科室预约最多”“哪个医生号源最紧张”。如果这些统计逻辑写在 QT 界面层,每次都要拼 SQL、遍历结果集,代码又长又容易出错。更好的做法是在数据库里建视图和存储过程,界面层只负责调用和展示。

视图的好处是:把多表关联和聚合逻辑固化在数据库里,界面层用一条SELECT * FROM view_name就能拿到结果。存储过程的好处是:可以把参数化查询和业务判断封装起来,减少网络传输和重复代码。

5.2 建一个科室预约统计视图

CREATE OR REPLACE VIEW v_department_stats AS SELECT s.department, COUNT(a.id) AS total_appointments, SUM(CASE WHEN a.status = 1 THEN 1 ELSE 0 END) AS confirmed_num, SUM(CASE WHEN a.status = 2 THEN 1 ELSE 0 END) AS cancelled_num FROM schedules s LEFT JOIN appointments a ON s.id = a.schedule_id GROUP BY s.department;

逻辑说明:LEFT JOIN保证没有预约的科室也能出现在统计里,SUM(CASE WHEN ...)是条件聚合的常用写法。参数说明:total_appointments是总预约数,confirmed_num是已确认数,cancelled_num是已取消数。界面层直接查这个视图,按total_appointments降序排列即可。

5.3 用存储过程处理“取消预约并释放号源”

取消预约不是简单改状态,还要把排班表的booked_num减回去。这个操作适合用存储过程封装,保证原子性。

DELIMITER // CREATE PROCEDURE cancel_appointment(IN appt_id INT) BEGIN DECLARE v_schedule_id INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '取消失败,请重试'; END; START TRANSACTION; SELECT schedule_id INTO v_schedule_id FROM appointments WHERE id = appt_id AND status != 2; IF v_schedule_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '预约不存在或已取消'; END IF; UPDATE appointments SET status = 2 WHERE id = appt_id; UPDATE schedules SET booked_num = booked_num - 1 WHERE id = v_schedule_id AND booked_num > 0; COMMIT; END // DELIMITER ;

逻辑说明:EXIT HANDLER捕获异常后回滚并抛出错误信息,SIGNAL用于主动报错。参数说明:appt_id是预约记录 ID,v_schedule_id是中间变量。QT 侧调用时用CALL cancel_appointment(?)即可,不需要在界面层写两条 SQL。

5.4 验证方法:用三条查询确认数据一致性

系统跑起来后,怎么确认数据没乱?我一般会跑三条校验查询:

校验项SQL 思路预期结果
号源是否超卖SELECT * FROM schedules WHERE booked_num > max_num返回 0 行
预约是否孤儿SELECT * FROM appointments WHERE schedule_id NOT IN (SELECT id FROM schedules)返回 0 行
状态是否合法SELECT * FROM appointments WHERE status NOT IN (0,1,2)返回 0 行

这三条查询建议做成管理端的一个“数据校验”按钮,每次管理员登录后点一下,心里有底。如果返回非空,说明之前的业务逻辑有漏洞,需要回查事务代码。

5.5 一个我踩过的坑:视图里的聚合字段不能直接更新

有一次我想在界面层直接改视图里的confirmed_num,结果 QT 报“视图不可更新”。后来才明白,包含GROUP BY和聚合函数的视图是只读的,只能查不能改。如果要改状态,必须回到基表操作。这个坑让我养成了一个习惯:视图只用于查询和展示,所有写操作都走基表或存储过程。

提示:如果确实需要“可更新视图”,视图定义里不能有聚合函数、GROUP BY、DISTINCT 等,否则数据库会拒绝更新。与其绕这个弯,不如把写操作单独封装。

最后说一个我的个人习惯:每次改完数据库相关代码,先跑一遍上面那三条校验查询,再点一遍完整预约流程。这个习惯帮我省了很多“演示时翻车”的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询