Python+Django+MySql医院挂号系统实战:号源并发与排班冲突解决方案
发布时间:2026/10/2 18:23:04来源:尧图网络
简介这是一套面向高校计算机相关专业学生的医院挂号诊疗管理系统完整项目源码采用PythonDjangoMySql技术栈开发适合作为毕业设计、期末大作业或课程设计的参考方案也适合想通过实战项目熟悉Web开发流程的初学者。压缩包共包含2000个文件以1633个JavaScript脚本、249个HTML页面、53个CSS样式表及33个HTM文件为主另有JSON配置、Python后端代码、XML与Markdown说明文档等整体约6.25MB前端页面与后端逻辑分层清晰。项目代码附有注释新手也能看懂下载后简单部署即可运行可帮助读者快速理解挂号、诊疗等核心业务模块的实现思路掌握Django与MySQL的配合方式并在此基础上进行二次开发或功能扩展。目前已有331人学习下载可作为高分毕设的可靠参考。1. 从一份能跑起来的挂号系统源码说起PythonDjangoMySql 到底能做出什么很多计算机专业的同学在选题阶段都会卡在同一个地方想做一个有业务深度、能写进简历、答辩时又不容易被问穿的系统但翻遍网上的开源项目要么是纯 CRUD 的博客后台要么是缺数据库、缺文档、跑不起来的半成品。医院挂号诊疗管理系统恰好落在一个很舒服的位置上——它天然包含排班、号源、挂号、缴费、就诊、处方这几条真实业务链用 Python Django MySql 实现既能体现后端建模能力又不会像微服务那样把新手劝退。这篇文章不讲空泛的“系统概述”而是把一套可复现的挂号诊疗系统从环境搭建、数据建模、核心业务实现到部署排错完整走一遍重点放在号源并发、排班冲突、状态流转这些真正会翻车的地方。适合正在做毕设、需要一套能讲清楚设计取舍的完整项目的同学也适合想用 Django 练手真实业务的后端入门者。2. 环境搭建与项目骨架把 Django 和 MySql 先跑通2.1 版本选型与依赖清单做毕设最怕的就是环境装到一半报错所以第一步不是写代码而是把版本锁死。Django 和 MySql 的兼容性在近几年已经比较稳定但 Python 版本、mysqlclient 驱动、MySql 服务端三者之间仍有坑。我一般会选 Python 3.10 Django 4.2 LTS MySql 8.0 这个组合原因是 Django 4.2 是长期支持版官方文档全遇到问题搜索到的答案最多MySql 8.0 对窗口函数和 CTE 支持好后面做排班统计会用到。组件推荐版本说明Python3.10.x3.12 部分驱动轮子还不全新手容易卡在编译Django4.2 LTS长期支持教程和第三方包兼容性最好MySql8.0.x支持窗口函数字符集用 utf8mb4mysqlclient2.2.xDjango 官方推荐驱动比 pymysql 性能好redis7.x可选用于号源缓存和会话安装命令按顺序执行不要跳步# 创建虚拟环境避免污染全局 Python python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/Mac 激活 source venv/bin/activate # 安装核心依赖 pip install django4.2.11 pip install mysqlclient2.2.4 pip install redis5.0.3这里要说明一下为什么用 mysqlclient 而不是 pymysql。pymysql 是纯 Python 实现安装简单但在高并发查询下性能明显不如 C 扩展的 mysqlclient。毕设答辩时如果老师问到数据库驱动选型这就是一个能加分的点。mysqlclient 在 Windows 上安装偶尔会报缺少 Visual C Build Tools解决办法是去装一个 Microsoft C Build Tools或者直接用 pip 安装预编译 wheel 包。2.2 创建 Django 项目与数据库配置环境好了之后创建项目和 app。挂号系统我一般拆成三个 appaccounts管用户和权限registration管挂号排班clinic管就诊和处方。拆分的理由是职责清晰后面写论文的模块设计章节也好对应。django-admin startproject hospital_system cd hospital_system python manage.py startapp accounts python manage.py startapp registration python manage.py startapp clinic接着改settings.py里的数据库配置。这里有个高频翻车点MySql 8.0 默认认证插件是caching_sha2_password而老版本 mysqlclient 可能不兼容报错Authentication plugin caching_sha2_password cannot be loaded。解决办法是在 MySql 里把用户认证方式改成mysql_native_password或者升级 mysqlclient 到 2.2 以上。# hospital_system/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: hospital_db, USER: hospital_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, CONN_MAX_AGE: 60, # 连接复用减少频繁建连开销 } }CONN_MAX_AGE这个参数值得单独说。Django 默认每个请求结束就关闭数据库连接挂号系统在高峰期会有大量短查询频繁建连会拖慢响应。设置成 60 秒表示连接在请求结束后保留 60 秒供复用。但要注意如果你的部署环境用了多线程或异步连接复用需要配合连接池否则可能出现连接泄漏。毕设规模下 60 秒足够生产环境建议上django-db-connection-pool。数据库建好之后执行迁移# 先在 MySql 里建库 mysql -u root -p -e CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; python manage.py makemigrations python manage.py migrate python manage.py createsuperuserutf8mb4而不是utf8的原因很简单MySql 的utf8其实是阉割版最多存 3 字节存不了 emoji 和部分生僻字。患者姓名里如果有生僻字用utf8会直接报错或存成问号。这个细节写进论文的数据库设计章节是实打实的工程考量。2.3 用 Django 自带 ORM 还是手写 SQL这是新手最容易纠结的问题。我的建议是核心业务用 ORM复杂统计查询用手写 SQL 或raw()。挂号系统的号源扣减、状态流转用 ORM 配合事务完全够用而且可读性好、防 SQL 注入。但像“统计某科室近 30 天每天挂号量”这种带日期分组和窗口函数的查询ORM 写起来又长又难维护直接上原生 SQL 更清晰。# registration/models.py 片段 from django.db import models class Department(models.Model): name models.CharField(max_length50, uniqueTrue, verbose_name科室名称) location models.CharField(max_length100, verbose_name位置) is_active models.BooleanField(defaultTrue) class Meta: db_table department class Doctor(models.Model): name models.CharField(max_length30) department models.ForeignKey(Department, on_deletemodels.PROTECT) title models.CharField(max_length20) # 主任医师、副主任医师等 daily_quota models.PositiveIntegerField(default30, verbose_name每日号源上限) class Meta: db_table doctoron_deletemodels.PROTECT而不是CASCADE是因为医生关联着历史挂号记录删医生不能把患者的就诊记录一起删掉这是医疗数据的基本要求。答辩时老师如果问数据完整性怎么保证这就是答案。3. 号源、排班与挂号核心业务表设计与状态流转3.1 排班表与号源表的拆分逻辑很多同学做挂号系统时会把排班和号源合成一张表结果做到后面发现号源要支持退号、改约、加号状态越来越复杂表字段越加越多。正确的做法是拆成两张表schedule记录医生某天上午/下午出诊quota记录这个排班下每个时间段的号源数量和剩余量。class Schedule(models.Model): doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE) work_date models.DateField() period models.CharField(max_length10, choices[(AM, 上午), (PM, 下午)]) total_quota models.PositiveIntegerField(default30) remaining_quota models.PositiveIntegerField(default30) version models.IntegerField(default0) # 乐观锁版本号 class Meta: db_table schedule unique_together (doctor, work_date, period) indexes [models.Index(fields[work_date, period])]unique_together保证同一个医生同一天同一时段只能有一条排班从数据库层面杜绝重复排班。version字段是乐观锁后面讲并发扣减时会用到。indexes加在日期和时段上因为患者查号时最常用的条件就是“某天某时段有哪些医生”。号源扣减是整个系统最容易出问题的地方。假设两个患者同时点“确认挂号”如果代码写成先查再减就会出现超卖。看下面这段错误示范# 错误示范先查后减并发下必然超卖 schedule Schedule.objects.get(idschedule_id) if schedule.remaining_quota 0: schedule.remaining_quota - 1 schedule.save() # 创建挂号记录...问题在于get和save之间有时间窗口两个请求可能都读到remaining_quota1然后都减到 0结果卖出两个号。正确做法是用数据库的原子更新from django.db import transaction from django.db.models import F transaction.atomic def grab_schedule(schedule_id, patient): # 原子扣减只有 remaining_quota 0 时才会更新成功 updated Schedule.objects.filter( idschedule_id, remaining_quota__gt0 ).update(remaining_quotaF(remaining_quota) - 1) if updated 0: raise ValueError(号源已满) schedule Schedule.objects.get(idschedule_id) # 创建挂号记录 Registration.objects.create( scheduleschedule, patientpatient, statusbooked )F(remaining_quota) - 1让数据库在服务端完成减法而不是先读到 Python 再写回避免了竞态。filter(remaining_quota__gt0)保证只有还有号时才更新updated 0说明号已满。整个操作包在transaction.atomic里扣减和创建记录要么都成功要么都回滚。这套写法在毕设答辩时是绝对的亮点因为它体现了对并发问题的真实理解。3.2 挂号状态机与退号处理挂号记录不能只有“已挂号”和“已取消”两个状态真实业务里至少要有已预约、已就诊、已取消、爽约。状态流转必须有明确的规则不能随便改。当前状态允许操作目标状态约束条件已预约就诊已就诊就诊日期当天已预约取消已取消就诊日期前一天 24 点前已预约爽约爽约就诊日期已过且未就诊已取消无-不可恢复已就诊无-不可逆退号时要把号源还回去同样用原子操作transaction.atomic def cancel_registration(reg_id): reg Registration.objects.select_for_update().get(idreg_id) if reg.status ! booked: raise ValueError(当前状态不可取消) if reg.schedule.work_date timezone.now().date(): raise ValueError(就诊当天不可取消) reg.status cancelled reg.save() # 号源加回 Schedule.objects.filter(idreg.schedule_id).update( remaining_quotaF(remaining_quota) 1 )select_for_update()会在事务里给这行记录加行锁防止同一个挂号记录被并发取消两次。注意它必须在事务内使用否则锁会立即释放等于没加。3.3 用 Django 表单和权限控制挂号入口挂号入口不能谁都能调患者只能给自己挂号医生只能看自己的排班。Django 的权限系统配合自定义装饰器就能搞定。from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied def patient_required(view_func): login_required def wrapper(request, *args, **kwargs): if request.user.role ! patient: raise PermissionDenied(仅患者可挂号) return view_func(request, *args, **kwargs) return wrapper patient_required def book_view(request, schedule_id): if request.method POST: try: grab_schedule(schedule_id, request.user.patient_profile) return JsonResponse({code: 0, msg: 挂号成功}) except ValueError as e: return JsonResponse({code: 1, msg: str(e)})role字段是在用户扩展表里定义的Django 默认 User 模型没有角色概念需要建一个UserProfile一对一关联。这里不要直接改 User 模型除非项目一开始就用了自定义用户模型中途改会非常痛苦。4. 就诊、处方与统计把业务闭环补完整4.1 就诊记录与处方的关联设计挂号之后就是就诊。医生看到自己的候诊列表点进去写病历、开处方。这里的关键是就诊记录和挂号记录要一对一关联处方和就诊记录一对多关联。class MedicalRecord(models.Model): registration models.OneToOneField(Registration, on_deletemodels.PROTECT) doctor models.ForeignKey(Doctor, on_deletemodels.PROTECT) chief_complaint models.TextField(verbose_name主诉) diagnosis models.TextField(verbose_name诊断) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table medical_record class Prescription(models.Model): record models.ForeignKey(MedicalRecord, on_deletemodels.CASCADE) drug_name models.CharField(max_length100) dosage models.CharField(max_length50) quantity models.PositiveIntegerField() price models.DecimalField(max_digits10, decimal_places2) class Meta: db_table prescriptionOneToOneField保证一次挂号只对应一份病历on_deletemodels.PROTECT防止误删挂号记录导致病历丢失。处方用CASCADE是因为处方依附于病历病历删了处方没有存在意义。价格用DecimalField而不是FloatField因为浮点数算钱会出现0.1 0.2 0.30000000000000004这种问题涉及金额必须用定点数。4.2 医生排班冲突检测排班冲突是另一个高频 bug。医生同一天同一时段不能有两个排班虽然数据库有unique_together兜底但前端提交时应该先检测并给出友好提示而不是让用户看到数据库报错。def create_schedule(doctor_id, work_date, period, total_quota): exists Schedule.objects.filter( doctor_iddoctor_id, work_datework_date, periodperiod ).exists() if exists: raise ValueError(f{work_date} {period} 该医生已有排班) # 检查医生当天是否已排满两个时段 day_count Schedule.objects.filter( doctor_iddoctor_id, work_datework_date ).count() if day_count 2: raise ValueError(该医生当天排班已满) return Schedule.objects.create( doctor_iddoctor_id, work_datework_date, periodperiod, total_quotatotal_quota, remaining_quotatotal_quota )这里除了唯一性检测还加了“一天最多两个时段”的业务规则。规则写在代码里而不是数据库约束里是因为业务规则可能变改代码比改表结构灵活。4.3 挂号量统计与 MySql 窗口函数论文里通常需要一张“各科室每日挂号量趋势图”这个查询用 ORM 写会很别扭直接上 SQL。from django.db import connection def dept_daily_stats(start_date, end_date): sql SELECT d.name AS dept_name, s.work_date, COUNT(r.id) AS reg_count, SUM(COUNT(r.id)) OVER (PARTITION BY d.name ORDER BY s.work_date) AS cumulative FROM registration r JOIN schedule s ON r.schedule_id s.id JOIN doctor doc ON s.doctor_id doc.id JOIN department d ON doc.department_id d.id WHERE s.work_date BETWEEN %s AND %s AND r.status ! cancelled GROUP BY d.name, s.work_date ORDER BY d.name, s.work_date with connection.cursor() as cursor: cursor.execute(sql, [start_date, end_date]) columns [col[0] for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()]SUM(COUNT(r.id)) OVER (PARTITION BY d.name ORDER BY s.work_date)是窗口函数按科室分组、按日期累加一次查询就能拿到每日量和累计量。如果用 ORM 得查两次再在 Python 里拼数据量大时性能差很多。注意%s占位符是 Django 的参数化查询写法不要用字符串拼接否则有 SQL 注入风险。5. 避坑与排查那些让系统跑不起来的真实问题5.1 坑一MySql 连接报错 2002 或 1045现象运行python manage.py migrate时报error 2002 (hy000): cant connect to local mysql server through socket或Access denied for user。原因2002 通常是 MySql 服务没启动或者HOST配成了localhost而 MySql 走的是 socket 连接但 socket 路径不对1045 是用户名密码错误或该用户没有远程访问权限。解决先确认服务在跑systemctl status mysql或 Windows 服务面板查看。然后把HOST改成127.0.0.1强制走 TCP避免 socket 路径问题。权限问题执行GRANT ALL PRIVILEGES ON hospital_db.* TO hospital_user% IDENTIFIED BY password; FLUSH PRIVILEGES;。注意%允许任意主机生产环境要收紧到具体 IP。5.2 坑二Django 静态文件在开发环境正常部署后 404现象DEBUGTrue时 CSS、JS 都能加载改成DEBUGFalse后页面样式全丢控制台报 404。原因Django 在DEBUGFalse时不再自动服务静态文件需要collectstatic把文件收集到STATIC_ROOT再由 Nginx 之类的服务器托管。解决在settings.py里配好STATIC_ROOT BASE_DIR / staticfiles执行python manage.py collectstatic然后 Nginx 配置location /static/ { alias /path/to/staticfiles/; }。开发阶段如果不想折腾可以临时用python manage.py runserver --insecure但这只是权宜之计。5.3 坑三号源扣减在压测下仍然超卖现象用 JMeter 或 locust 模拟 100 并发抢 10 个号结果卖出 12 个。原因虽然用了F()表达式但如果没有包在事务里或者数据库隔离级别是READ COMMITTED且更新语句没有正确加锁仍可能出问题。更隐蔽的原因是代码里在扣减之后又做了一次save()把内存里的旧值写回去了。解决确保扣减和创建记录在同一个transaction.atomic块里扣减只用update()不要再用save()。压测时把 MySql 的innodb_flush_log_at_trx_commit临时设为 2 可以提升性能但生产环境要设回 1 保证不丢数据。5.4 坑四时区问题导致排班日期错乱现象明明排的是今天的班患者端显示成昨天或者退号时提示“就诊当天不可取消”但实际还没到就诊日。原因Django 默认用 UTC 存储时间USE_TZTrue时timezone.now()返回 UTC 时间而work_date是DateField存的是本地日期两者比较时差 8 小时。解决在settings.py里设TIME_ZONE Asia/Shanghai和USE_TZ True所有时间比较用timezone.localtime(timezone.now()).date()转成本地日期。不要图省事把USE_TZ设成False那样跨时区部署会出更大问题。5.5 坑五mysqlclient 安装失败提示缺少 mysql_config现象pip install mysqlclient报EnvironmentError: mysql_config not found。原因mysqlclient 编译时需要 MySql 的开发库头文件系统里只装了 MySql 服务端没装开发包。解决Ubuntu/Debian 执行sudo apt-get install default-libmysqlclient-dev build-essentialCentOS 执行sudo yum install mysql-devel gccWindows 则装 Visual C Build Tools。装完再pip install mysqlclient即可。6. 让这套系统在答辩和简历里真正立住的两个技巧第一个技巧是把号源并发这件事讲透。大部分毕设系统都停留在“能跑就行”但如果你能在论文里画出一张并发扣减的时序图说明先查后减为什么会超卖、F()表达式和事务是怎么解决的、压测数据是多少老师会立刻意识到你不是抄的。具体做法是写一个简单的压测脚本用 Python 的threading模拟 50 个并发请求抢 10 个号对比错误写法和正确写法的结果。import threading import requests results [] def grab(i): r requests.post(http://127.0.0.1:8000/api/book/, json{schedule_id: 1}) results.append(r.json()[code]) threads [threading.Thread(targetgrab, args(i,)) for i in range(50)] for t in threads: t.start() for t in threads: t.join() success results.count(0) print(f成功挂号数: {success}) # 正确实现应该正好等于号源总数跑出来如果成功数等于号源数说明并发控制到位如果大于说明还有竞态。这个脚本可以直接放进论文的测试章节比截图几个页面有说服力得多。第二个技巧是给系统加一个简单的操作日志。不用上复杂的审计框架建一张operation_log表记录谁在什么时间对哪条挂号记录做了什么操作。答辩时如果老师问“怎么追溯误操作”这就是现成的答案。实现上可以用 Django 的signals在Registration保存后自动写日志代码量不超过 20 行但体现的是工程意识。from django.db.models.signals import post_save from django.dispatch import receiver receiver(post_save, senderRegistration) def log_registration_change(sender, instance, created, **kwargs): OperationLog.objects.create( userinstance.patient.user, actioncreate if created else update, target_idinstance.id, detailfstatus{instance.status} )我自己做这类系统最大的教训就是不要等到功能全写完再考虑并发和日志这两样东西越早加成本越低。等到答辩前一周发现超卖回头改表结构和业务代码那才是真的后悔药都没得吃。把号源扣减和状态流转这两块吃透这套医院挂号诊疗管理系统就不只是一个毕设作业而是你能在面试里讲二十分钟的真实项目。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网