资讯动态

QT+MySQL医院预约管理系统:排班冲突与号源超卖解决方案

发布时间:2026/10/9 22:31:18 来源:尧图企业网站定制
简介这是一套基于Qt与MySQL开发的医院预约管理系统完整项目资料面向计算机相关专业的在校学生、教师及企业开发者可用于毕业设计、课程设计、作业提交或项目初期立项演示。项目已通过导师指导与答辩评审获得95分成绩代码经过实际运行测试功能完整可靠。压缩包共47个文件约31KB包含15个cpp源文件、15个h头文件、12个ui界面文件以及pro工程配置、user用户配置、md说明文档和txt授权码等覆盖登录、注册、科室选择、时间预约、医生与管理员管理等核心模块目录结构清晰便于按功能模块检索与二次开发。目前已有62人学习关注。对于基础较好的读者可在此基础上修改扩展实现更多功能对于初学者也能通过阅读源码理解Qt界面设计与MySQL数据库交互的完整实现思路是一份兼具参考价值与实用性的高分项目资源。1. 医院预约管理系统为什么总在排班冲突上翻车做过门诊业务系统的人大多有过这种经历挂号窗口排着长队系统却提示某个医生同一时段被约了两次或者患者明明选了周三上午数据库里却存成了周四下午。这类问题的根子往往不在业务逻辑写得多复杂而在于时间片模型和数据库约束没设计好。基于 QT MySQL 的医院预约管理系统本质上是把「医生—排班—号源—患者」这条链路用桌面端界面串起来让挂号、退号、改约、查询在一个客户端里闭环。它适合两类人一类是课程设计或毕设阶段需要一套完整可跑系统的学生另一类是刚接触桌面端 关系型数据库组合、想找一个业务边界清晰的练手项目的开发者。标题里说的「详细文档 全部资料」落到实操上就是数据库脚本、QT 工程源码、界面原型和部署说明这几块下面按能复现的顺序拆开讲。2. 从挂号窗口倒推号源表、排班表、预约表怎么切2.1 先定业务实体再谈建表很多人一上来就打开 MySQL 建表结果建到一半发现字段不够用。我一般会先在纸上把实体列出来科室、医生、排班计划、号源、患者、预约记录。这里的关键判断是——排班和号源要不要拆成两张表。如果拆开排班表存「某医生某天上午在哪个诊室」号源表存「这个排班下第 1 到第 30 号分别被谁占了」如果不拆就把剩余号量直接放在排班表里做递减。前者查询灵活但写入量大后者简单但并发下容易超卖。门诊量不大的场景我倾向拆开因为退号、改约、停诊这些操作都需要精确到单个号源。科室和医生是一对多医生和排班是一对多排班和号源是一对多患者和预约记录是一对多。这四组关系定下来外键和索引基本就有方向了。注意别在医生表里冗余科室名称科室改名时你会后悔的。2.2 建库建表脚本与字段说明下面这段 SQL 是核心四张表的建法字符集统一用 utf8mb4时间字段用 DATETIME 而不是 TIMESTAMP避免时区带来的玄学问题。-- 科室表最上层分类挂号时先选科室 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, dept_desc VARCHAR(200) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生表归属科室职称影响挂号费 CREATE TABLE doctor ( doctor_id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(30) NOT NULL, dept_id INT NOT NULL, title VARCHAR(20) DEFAULT 住院医师, reg_fee DECIMAL(6,2) DEFAULT 10.00, CONSTRAINT fk_doctor_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表医生 日期 时段 唯一防止重复排班 CREATE TABLE schedule ( schedule_id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, room_no VARCHAR(10) DEFAULT NULL, total_num INT NOT NULL DEFAULT 0, UNIQUE KEY uk_doc_date_slot (doctor_id, work_date, time_slot), CONSTRAINT fk_sch_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 号源表每个排班下逐号生成状态标记是否被占 CREATE TABLE reg_source ( source_id INT PRIMARY KEY AUTO_INCREMENT, schedule_id INT NOT NULL, seq_no INT NOT NULL COMMENT 第几号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约 1已约 2停诊, UNIQUE KEY uk_sch_seq (schedule_id, seq_no), CONSTRAINT fk_src_sch FOREIGN KEY (schedule_id) REFERENCES schedule(schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_doc_date_slot这个唯一索引是防重复排班的第一道闸uk_sch_seq保证同一排班下号源不重号。status用 TINYINT 而不是布尔是为了以后扩展「锁定中」这类中间态。reg_fee用 DECIMAL 不用 FLOAT金额计算别给自己埋雷。2.3 预约记录表与并发占号预约记录表要记录「谁在什么时候约了哪个号源」同时保留操作时间用于退号审计。CREATE TABLE appointment ( appt_id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, source_id INT NOT NULL, appt_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效 0已退, UNIQUE KEY uk_source (source_id), CONSTRAINT fk_appt_src FOREIGN KEY (source_id) REFERENCES reg_source(source_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_source是防超卖的核心一个号源只能有一条有效预约。占号时不要先查再改直接用「更新号源状态 插入预约」放在一个事务里靠唯一索引兜底。START TRANSACTION; UPDATE reg_source SET status 1 WHERE source_id ? AND status 0; -- 若 affected_rows 0说明已被占回滚 INSERT INTO appointment (patient_id, source_id) VALUES (?, ?); COMMIT;UPDATE ... WHERE status 0这一步的 affected_rows 是判断是否抢到的依据比先 SELECT 再判断可靠得多。事务隔离级别用默认的 REPEATABLE READ 就够别乱改。3. QT 端怎么把数据库操作包成可维护的类3.1 用 QSqlDatabase 建立单例连接QT 操作 MySQL 需要先确认驱动可用。编译时如果报「QMYSQL driver not loaded」八成是没装 MySQL 客户端库或者驱动没编进去这是新手最常见的翻车点。连接建议封装成单例避免每个窗口各开一个连接。// dbhelper.h class DbHelper { public: static DbHelper instance(); QSqlDatabase db() const { return m_db; } bool open(const QString host, const QString user, const QString pwd, const QString name); private: DbHelper() default; QSqlDatabase m_db; }; // dbhelper.cpp bool DbHelper::open(const QString host, const QString user, const QString pwd, const QString name) { m_db QSqlDatabase::addDatabase(QMYSQL); m_db.setHostName(host); m_db.setUserName(user); m_db.setPassword(pwd); m_db.setDatabaseName(name); if (!m_db.open()) { qDebug() open failed: m_db.lastError().text(); return false; } return true; }addDatabase只调用一次重复调用会覆盖默认连接。lastError().text()一定要打出来否则连不上时你只能靠猜。生产环境把账号密码放配置文件别硬编码在源码里。3.2 挂号操作的完整调用链挂号按钮点下去背后要走「校验号源 → 事务占号 → 刷新界面」三步。下面是一个精简版实现。bool RegService::makeAppointment(int patientId, int sourceId) { QSqlDatabase db DbHelper::instance().db(); db.transaction(); QSqlQuery q(db); // 第一步原子占号 q.prepare(UPDATE reg_source SET status1 WHERE source_id? AND status0); q.addBindValue(sourceId); if (!q.exec() || q.numRowsAffected() 0) { db.rollback(); return false; // 号已被占或不存在 } // 第二步写预约记录 q.prepare(INSERT INTO appointment(patient_id, source_id) VALUES(?,?)); q.addBindValue(patientId); q.addBindValue(sourceId); if (!q.exec()) { db.rollback(); return false; } return db.commit(); }numRowsAffected()是判断占号是否成功的唯一依据别用SELECT去查状态再判断那中间有时间窗口。prepareaddBindValue防注入也避免拼接字符串时引号出错。事务里任何一步失败都要rollback否则号源状态和预约记录会对不上。3.3 界面刷新与信号槽解耦挂号成功后要刷新号源列表但服务层不该直接操作界面控件。常见做法是服务层发信号界面层接槽。// 服务层声明 signals: void appointmentDone(int sourceId, bool ok); // 界面层连接 connect(m_service, RegService::appointmentDone, this, RegWindow::onAppointmentDone); void RegWindow::onAppointmentDone(int sourceId, bool ok) { if (ok) { refreshSourceList(); // 重新查库刷新 } else { QMessageBox::warning(this, 提示, 该号源已被预约); } }信号槽把「业务结果」和「界面表现」分开以后换界面不用动服务层。refreshSourceList里重新查一次数据库别在内存里手动改状态否则多窗口同时操作时数据会不一致。4. 排班冲突与号源超卖避坑与排查清单4.1 同一医生同一天被排了两个上午现象排班管理界面能保存两条「张医生 3 月 5 日上午」的记录。原因建表时没加uk_doc_date_slot唯一索引或者加了但代码里用INSERT IGNORE把错误吞掉了。解决先确认索引存在SHOW INDEX FROM schedule;能看到uk_doc_date_slot插入时捕获错误码 1062 并提示「该时段已排班」不要静默忽略。4.2 两个窗口同时挂号同一个号被约了两次现象两个挂号员几乎同时点确认两条预约记录都写进去了。原因占号逻辑写成了「先 SELECT 查 status0再 UPDATE」中间有窗口。解决改成UPDATE ... WHERE status0并检查numRowsAffected配合appointment表的uk_source唯一索引双保险。排查时看reg_source.status和appointment是否一一对应对不上就是事务没包好。4.3 退号后号源没释放现象患者退号了但该号显示「已约」别人约不了。原因退号只改了appointment.status0忘了把reg_source.status改回 0。解决退号同样放事务里两条 UPDATE 一起提交。排查 SQLSELECT s.source_id, s.status, a.status FROM reg_source s LEFT JOIN appointment a ON s.source_ida.source_id WHERE s.status1 AND (a.status0 OR a.appt_id IS NULL);查出来就是脏数据。4.4 QT 程序换台电脑就连不上数据库现象开发机正常拷到另一台机器提示驱动加载失败。原因目标机器没装 MySQL 客户端库或者 QT 的 sqldrivers 目录下缺qsqlmysql.dll。解决确认plugins/sqldrivers下有对应驱动缺 MySQL 客户端库就补上连接参数从配置文件读别写死 IP。排查时先跑QSqlDatabase::drivers()看列表里有没有 QMYSQL。4.5 日期存进去差一天现象界面选 3 月 5 日数据库里是 3 月 4 日。原因QT 的QDate转字符串时格式和 MySQL 期望的不一致或者时区处理有偏差。解决统一用toString(yyyy-MM-dd)传 DATE 字段DATETIME 用yyyy-MM-dd HH:mm:ss别用本地化格式。排查时直接看数据库里存的值再对比界面显示值。5. 让这套系统真正能演示初始化数据与验收脚本5.1 一键灌入演示数据系统能不能跑起来一半看有没有像样的初始数据。下面这段脚本生成 3 个科室、6 名医生、未来 7 天的排班和号源方便演示。-- 科室 INSERT INTO department(dept_name, dept_desc) VALUES (内科,常见内科疾病),(外科,外科诊疗),(儿科,儿童诊疗); -- 医生 INSERT INTO doctor(doctor_name, dept_id, title, reg_fee) VALUES (医生A,1,主任医师,50.00),(医生B,1,主治医师,20.00), (医生C,2,主任医师,50.00),(医生D,2,住院医师,10.00), (医生E,3,主治医师,20.00),(医生F,3,住院医师,10.00); -- 排班用存储过程批量生成未来 7 天上午班 DELIMITER $$ CREATE PROCEDURE gen_schedule() BEGIN DECLARE d INT DEFAULT 0; DECLARE doc INT; WHILE d 7 DO SET doc 1; WHILE doc 6 DO INSERT INTO schedule(doctor_id, work_date, time_slot, room_no, total_num) VALUES(doc, DATE_ADD(CURDATE(), INTERVAL d DAY), 1, CONCAT(R, doc), 20); SET doc doc 1; END WHILE; SET d d 1; END WHILE; END$$ DELIMITER ; CALL gen_schedule(); -- 为每个排班生成 20 个号源 INSERT INTO reg_source(schedule_id, seq_no, status) SELECT s.schedule_id, n.n, 0 FROM schedule s JOIN (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19 UNION SELECT 20) n WHERE s.total_num n.n;gen_schedule用双层循环生成排班号源用笛卡尔积一次插入。total_num和号源数量要对齐否则会出现「显示 20 个号实际只有 15 个」的尴尬。演示前先CALL一次别每次启动都灌会重复。5.2 验收时该查哪几张表演示前跑一遍下面这组查询能提前发现大部分数据问题。检查项SQL 要点期望结果排班是否重复按 doctor_id, work_date, time_slot 分组 count1无结果号源是否超卖reg_source.status1 但 appointment 无对应有效记录无结果号源数量是否对齐按 schedule_id 分组 count(seq_no) 对比 total_num两者相等退号是否释放appointment.status0 但 reg_source.status 仍为 1无结果这四条查完没问题基本演示不会当场翻车。验收脚本建议存成.sql文件每次改完业务逻辑跑一遍。5.3 一个我常用来兜底的技巧如果时间紧、来不及做完整的并发测试我会在reg_source上加一个触发器状态从 0 变 1 时自动检查appointment是否已有记录有就报错回滚。这相当于给数据库再加一道保险虽然性能略降但演示场景完全够用。触发器不是必须的但当你对上层代码没把握时它是最省心的后悔药。写触发器时注意别和事务里的逻辑打架先在小库上验证再上正式数据。这套系统真正的价值不在于界面多漂亮而在于把「时间片 唯一约束 事务」这条线走通。我自己的习惯是每加一个业务操作先问一句「并发下会不会出脏数据」再动手写代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑