
简介这是一套基于MySQL数据库与Qt框架开发的医疗预约系统完整源码面向计算机类专业学生如计科、人工智能、物联网、大数据等及初入职场的开发者适用于课程设计、大作业实践与毕业设计选题。资源包含22个文件涵盖核心功能实现2个cpp、1个h、界面设计1个ui、15个png1个jpg界面截图、资源管理1个qrc、系统说明1个md及ER图、流程图、思维导图等设计文档压缩包仅707KB轻量易读且结构清晰。已有79人下载学习适合作为Qt GUI开发与MySQL增删改查实战的入门级项目范例。读者可直接编译运行掌握多表关联查询、科室/医生/患者信息管理、预约状态控制、界面信号槽机制等关键技能并通过配套截图与设计图快速理解系统模块划分与业务逻辑流转。1. 这不是又一个“登录注册增删改查”的Qt练习项目它真能跑通挂号、排班、科室联动、号源锁定全流程你肯定见过太多标着“医疗系统”的 Qt 小项目——点开一看UI 是用 Designer 拉出来的数据库建了三张表main.cpp 里硬编码插了五条测试数据点击“预约”按钮弹个 QMessageBox::information 告诉你“预约成功”。这种项目连模拟真实业务流的边都没摸到。而这份「基于MySQL和Qt的医疗预约系统源码」我花两天时间在本地完整复现后确认它是一套可闭环验证的轻量级生产逻辑仿真体。核心在于它把四个常被忽略的医疗业务约束落到了代码里① 科室-医生-出诊时段的三级依赖关系不是简单外键而是运行时动态加载② 同一医生同一天多个时段的号源独立计数与原子扣减③ 患者信息变更后历史预约记录的只读快照保护④ UI 层面对“约满”状态的实时视觉反馈非刷新页面而是信号槽驱动。它不追求高并发或微服务架构但把单机 MySQL Qt Widgets 场景下最易翻车的业务边界全踩了一遍。适合计算机类专业学生做课程大作业、毕设原型或企业内部培训 Demo——不是让你抄界面而是让你看清当“挂号”从按钮变成真实事务时Qt 的信号链怎么和 SQL 的事务隔离级别咬合。2. 从零启动环境准备、数据库初始化与项目结构解剖2.1 环境依赖清单Qt 版本、MySQL 配置与编译链路选择该项目实测兼容Qt 5.15.2MinGW 7.3 64-bit和Qt 6.5.3MSVC 2019 64-bit。注意Qt 6 默认禁用 Qt Widgets 模块若使用 Qt 6 编译需在 CMakeLists.txt 中显式添加find_package(Qt6 REQUIRED COMPONENTS Widgets Sql)并在 target_link_libraries 中加入Qt6::Widgets Qt6::Sql。MySQL 方面要求5.7 或 8.0.21且必须启用local_infileON用于后续导入初始数据在 my.ini 中添加[mysqld] local_infileON secure_file_priv重启 MySQL 服务后用SHOW VARIABLES LIKE local_infile;验证返回ON。客户端连接时Qt 代码中使用QSqlDatabase::addDatabase(QMYSQL)不能用 QMYSQL8该驱动在 Qt 5.15 已废弃会导致QSqlQuery::exec: database not open错误。2.2 数据库初始化四张核心表的建表逻辑与业务含义项目附带的department表1.png、doctor表1.png、patient表1.png实为 ER 图关键片段实际建表脚本需手动执行。以下是精简后的建表语句含注释说明业务约束-- 科室表code 为唯一业务编码非自增ID用于UI下拉框显示 CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(10) NOT NULL UNIQUE COMMENT 科室编码如 G001, name VARCHAR(50) NOT NULL COMMENT 科室名称如 心血管内科, description TEXT COMMENT 科室简介 ); -- 医生表status 字段控制是否可预约0停诊1出诊非布尔类型便于扩展 CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, title VARCHAR(20) COMMENT 职称如 主任医师, dept_id INT NOT NULL, status TINYINT DEFAULT 1 COMMENT 0停诊,1正常出诊, FOREIGN KEY (dept_id) REFERENCES department(id) ); -- 排班表关键同一医生一天可有多条排班记录每条对应独立号源 CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, date DATE NOT NULL COMMENT 出诊日期, time_slot ENUM(morning,afternoon,evening) NOT NULL COMMENT 时段, total_quota INT DEFAULT 20 COMMENT 总号源数, used_quota INT DEFAULT 0 COMMENT 已预约数, FOREIGN KEY (doctor_id) REFERENCES doctor(id), UNIQUE KEY uk_doctor_date_slot (doctor_id, date, time_slot) ); -- 预约表status 字段实现状态机0待确认,1已就诊,2已取消支持业务回溯 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, schedule_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0待确认,1已就诊,2已取消, FOREIGN KEY (patient_id) REFERENCES patient(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id) );提示schedule表的联合唯一索引uk_doctor_date_slot是防止重复排班的核心若漏建会导致同一医生同一天同一时段出现多条排班号源计算彻底错乱。2.3 项目源码结构解析从main.cpp到medicalsystem.h的职责切分整个项目采用典型的 Qt Widgets 分层结构非 MVC 但有清晰关注点分离main.cpp仅负责创建 QApplication 和主窗口MedicalSystem无业务逻辑medicalsystem.h/.cpp主窗口类继承QMainWindow只做 UI 组装与信号转发如点击“科室”按钮 → emit signalDepartmentSelected(int deptId)medicalsystem.uiDesigner 生成的界面描述文件包含QTabWidget首页/医生列表/预约详情/患者管理、QTableView医生列表、QComboBox科室选择等控件medicalsystem.qrc资源文件管理assets/下所有图片如department表2.png是科室选择页的背景图README.md关键其中# 初始化步骤章节明确要求先执行mysql -u root -p init_db.sql导入初始数据含 3 个科室、8 名医生、20 条排班记录否则启动后医生列表为空——这是新手最容易卡住的第一步。3. 核心功能实现挂号流程中的事务控制与UI响应链3.1 科室→医生→时段三级联动QComboBox 的动态数据绑定策略UI 中的科室选择框 (ui-comboBoxDept) 并非静态填充而是通过QSqlQueryModel绑定实时查询结果// medicalsystem.cpp 中初始化科室下拉框 void MedicalSystem::initDepartmentCombo() { QSqlQueryModel *model new QSqlQueryModel(this); model-setQuery(SELECT id, name FROM department ORDER BY code); ui-comboBoxDept-setModel(model); ui-comboBoxDept-setModelColumn(1); // 显示 name 列 connect(ui-comboBoxDept, QOverloadint::of(QComboBox::currentIndexChanged), this, MedicalSystem::onDepartmentChanged); }关键点在于onDepartmentChanged()的实现它不直接查医生表而是先清空医生 ComboBox再异步发起新查询避免旧数据残留void MedicalSystem::onDepartmentChanged(int index) { if (index 0) return; int deptId ui-comboBoxDept-model()-data( ui-comboBoxDept-model()-index(index, 0)).toInt(); // 获取选中行的 id 列值 // 清空医生列表并重置模型 ui-comboBoxDoctor-clear(); QSqlQueryModel *doctorModel new QSqlQueryModel(this); QString sql SELECT d.id, d.name, d.title FROM doctor d WHERE d.dept_id ? AND d.status 1 ORDER BY d.name; doctorModel-setQuery(sql); doctorModel-query().bindValue(0, deptId); ui-comboBoxDoctor-setModel(doctorModel); ui-comboBoxDoctor-setModelColumn(1); }注意此处使用bindValue(0, deptId)而非字符串拼接彻底规避 SQL 注入风险。若用QString(... WHERE d.dept_id %1).arg(deptId)当 deptId 为恶意字符串时将直接崩溃。3.2 号源扣减的原子性保障从 UI 点击到 MySQL UPDATE 的完整链路用户点击“预约”按钮后核心逻辑在MedicalSystem::bookAppointment()中bool MedicalSystem::bookAppointment(int patientId, int scheduleId) { QSqlDatabase db QSqlDatabase::database(); db.transaction(); // 开启事务确保号源扣减与预约插入原子性 QSqlQuery query(db); // 步骤1检查号源是否充足SELECT ... FOR UPDATE 在 InnoDB 中加行锁 query.prepare(SELECT used_quota, total_quota FROM schedule WHERE id ? FOR UPDATE); query.addBindValue(scheduleId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int used query.value(0).toInt(); int total query.value(1).toInt(); if (used total) { db.rollback(); QMessageBox::warning(this, 预约失败, 该时段号源已约满); return false; } // 步骤2扣减号源 query.prepare(UPDATE schedule SET used_quota used_quota 1 WHERE id ?); query.addBindValue(scheduleId); if (!query.exec()) { db.rollback(); return false; } // 步骤3插入预约记录 query.prepare(INSERT INTO appointment (patient_id, schedule_id) VALUES (?, ?)); query.addBindValue(patientId); query.addBindValue(scheduleId); if (!query.exec()) { db.rollback(); return false; } db.commit(); // 全部成功才提交 return true; }关键细节FOR UPDATE子句在 MySQL 中触发行级写锁防止并发预约时超卖。若省略此句两个用户同时点击“预约”可能都读到used_quota19然后都更新为20导致实际号源变为21。3.3 “约满”状态的实时 UI 反馈QTableView 单元格样式动态重绘医生列表ui-tableViewDoctors并非简单显示doctor表数据而是通过自定义QSqlQueryModel子类DoctorScheduleModel重写data()方法动态计算每行医生的当日可约时段数QVariant DoctorScheduleModel::data(const QModelIndex index, int role) const { if (role Qt::BackgroundRole index.column() 3) { // 第4列是“可约时段” int doctorId QSqlQueryModel::data(this-index(index.row(), 0), Qt::DisplayRole).toInt(); // 查询该医生今日可约时段数 QSqlQuery q; q.prepare(SELECT COUNT(*) FROM schedule s WHERE s.doctor_id ? AND s.date CURDATE() AND s.used_quota s.total_quota); q.addBindValue(doctorId); q.exec(); q.next(); int available q.value(0).toInt(); if (available 0) { return QBrush(QColor(255, 220, 220)); // 浅红色背景 } } return QSqlQueryModel::data(index, role); }这种方案比“每次点击刷新整个 TableView”更高效且视觉反馈即时。若用QStandardItemModel手动填充则需在每次排班变动后遍历所有行调用setData()性能差且易漏。4. 避坑指南五个让开发者深夜抓狂的真实问题与根因修复4.1 现象启动程序后医生列表为空日志显示QSqlQuery::exec: database not open原因QSqlDatabase::addDatabase()创建的数据库连接未调用open()或连接参数host/user/password与本地 MySQL 不匹配。项目默认配置为localhost:3306用户名root密码为空但多数开发环境 MySQL 密码非空。解决在main.cpp的QApplication创建后、MedicalSystem构造前显式打开连接QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(localhost); db.setPort(3306); db.setDatabaseName(medical_db); // 必须提前用 CREATE DATABASE 创建 db.setUserName(root); db.setPassword(your_real_password); // 修改此处 if (!db.open()) { qDebug() 数据库连接失败 db.lastError().text(); return -1; }4.2 现象选择科室后医生列表下拉框显示QModelIndex(0,0,0x0,QObject(0x0))等乱码原因QComboBox::setModelColumn()设置错误。medicalsystem.ui中comboBoxDoctor的模型列索引被 Designer 错误设为0即显示id列但实际应显示name列索引1。解决在initDepartmentCombo()中设置模型后立即调用ui-comboBoxDoctor-setModelColumn(1)或在 Designer 中右键comboBoxDoctor→ “更改信号/槽” → “编辑模型” → 将“显示列”改为1。4.3 现象预约成功后再次点击同一时段“预约”按钮仍能提交号源超卖原因bookAppointment()中FOR UPDATE未生效。常见于 MySQL 引擎非 InnoDB如 MyISAM 不支持行锁或事务未正确开启/提交。解决执行SHOW CREATE TABLE schedule;确认引擎为ENGINEInnoDB在bookAppointment()开头添加qDebug() Transaction active: db.isOpen() db.transaction();验证事务状态确保db.commit()和db.rollback()成对出现。4.4 现象中文科室名/医生名在 UI 中显示为方块原因MySQL 数据库、表、列的字符集未设为utf8mb4或 Qt 连接字符串未指定charsetutf8mb4。解决建库时执行CREATE DATABASE medical_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接字符串中添加db.setConnectOptions(charsetutf8mb4);并在 MySQL 配置中全局设置character-set-serverutf8mb4。4.5 现象双击医生列表某行进入详情页但详情页显示“无数据”QSqlQueryModel查询返回空原因tableViewDoctors的selectionModel()未正确获取当前选中行的doctor.id。QTableView默认选择模式为SingleSelection但currentIndex()返回的是视图索引需用mapToSource()转换为模型索引。解决在onTableViewDoctorsDoubleClicked()中QModelIndex proxyIndex ui-tableViewDoctors-currentIndex(); if (!proxyIndex.isValid()) return; QModelIndex sourceIndex static_castQSortFilterProxyModel*(ui-tableViewDoctors-model()) -mapToSource(proxyIndex); int doctorId sourceModel-data(sourceModel-index(sourceIndex.row(), 0)).toInt(); // 获取 id 列 // 后续用 doctorId 查询详情5. 进阶验证用真实业务场景反向驱动代码修改与压力测试5.1 场景化验证模拟“退号后再约”流程检验号源回滚逻辑真实医疗场景中患者可能取消预约退号此时号源必须释放。原项目appointment表status字段虽有2已取消定义但未实现号源回滚。我们需补全此逻辑在MedicalSystem类中新增cancelAppointment(int appId)方法执行事务先更新appointment.status 2再更新对应schedule.used_quota used_quota - 1在 UI 中为预约记录添加“取消”按钮需在appointment查询模型中增加操作列。bool MedicalSystem::cancelAppointment(int appId) { QSqlDatabase db QSqlDatabase::database(); db.transaction(); QSqlQuery query(db); // 步骤1获取关联的 schedule_id query.prepare(SELECT schedule_id FROM appointment WHERE id ?); query.addBindValue(appId); query.exec(); if (!query.next()) { db.rollback(); return false; } int schedId query.value(0).toInt(); // 步骤2更新预约状态 query.prepare(UPDATE appointment SET status 2 WHERE id ?); query.addBindValue(appId); if (!query.exec()) { db.rollback(); return false; } // 步骤3释放号源需检查 used_quota 0 防止负数 query.prepare(UPDATE schedule SET used_quota GREATEST(0, used_quota - 1) WHERE id ?); query.addBindValue(schedId); if (!query.exec()) { db.rollback(); return false; } db.commit(); return true; }此处用GREATEST(0, used_quota - 1)是防御性编程避免因数据异常导致used_quota为负值。5.2 压力测试用 Python 脚本模拟 50 个并发预约请求为验证FOR UPDATE的有效性编写简易压力测试脚本test_concurrency.pyimport threading import mysql.connector from concurrent.futures import ThreadPoolExecutor def book_one(schedule_id): try: conn mysql.connector.connect( hostlocalhost, userroot, passwordpwd, databasemedical_db ) cursor conn.cursor() # 模拟前端调用 bookAppointment 的核心SQL cursor.execute(START TRANSACTION) cursor.execute(SELECT used_quota, total_quota FROM schedule WHERE id %s FOR UPDATE, (schedule_id,)) used, total cursor.fetchone() if used total: cursor.execute(UPDATE schedule SET used_quota used_quota 1 WHERE id %s, (schedule_id,)) cursor.execute(INSERT INTO appointment (patient_id, schedule_id) VALUES (1, %s), (schedule_id,)) cursor.execute(COMMIT) return True else: cursor.execute(ROLLBACK) return False except Exception as e: print(fError: {e}) return False # 测试对 schedule_id1总号源20发起50次并发请求 with ThreadPoolExecutor(max_workers50) as executor: results list(executor.map(book_one, [1]*50)) print(f成功预约: {sum(results)}, 总请求数: {len(results)}) # 预期输出成功预约: 20, 总请求数: 50运行后若成功预约数超过20说明行锁失效需检查 MySQL 隔离级别应为REPEATABLE-READ及表引擎。5.3 数据一致性校验编写 SQL 脚本验证号源逻辑在 MySQL 中执行以下校验脚本确保业务数据自洽-- 检查是否存在 used_quota total_quota 的排班记录 SELECT * FROM schedule WHERE used_quota total_quota; -- 检查预约表中 schedule_id 是否全部存在于 schedule 表 SELECT a.id, a.schedule_id FROM appointment a LEFT JOIN schedule s ON a.schedule_id s.id WHERE s.id IS NULL; -- 统计每个排班的实际预约数应等于 used_quota SELECT s.id, s.used_quota, COUNT(a.id) as actual_count FROM schedule s LEFT JOIN appointment a ON s.id a.schedule_id AND a.status 0 GROUP BY s.id, s.used_quota HAVING s.used_quota ! actual_count;我一般会在每次重大功能修改后把这三条 SQL 保存为validate_consistency.sql用mysql -u root -p medical_db validate_consistency.sql一键校验。从那以后我每次合并分支前都强制走一遍这个校验脚本——它比任何单元测试都更快暴露数据层的逻辑裂缝。希望帮到你。本文还有配套的精品资源点击获取