简介这是一套面向计算机专业本科生的高分毕业设计级医院挂号诊疗管理系统基于PythonDjango框架与MySQL数据库实现完整覆盖患者挂号、医生排班、门诊接诊、病历管理及后台统计等核心业务流程适合毕业设计、课程设计与期末大作业参考。资源包共2000个文件主体为1633个JavaScript交互脚本支撑前端动态操作、249个HTML页面含挂号页、医生工作站、管理后台等多角色视图、53个CSS样式文件含bootstrap、font-awesome、ui.css等主流UI组件辅以JSON配置、少量Py后端逻辑及说明文档整体压缩后仅6.25MB轻量易部署。已有331人学习下载项目代码全程手写、注释详尽含98分答辩级结构设计与导师认可的工程规范开箱即用无需复杂配置即可本地运行并理解全栈协作逻辑。1. 这不是又一个“用户登录增删改查”的毕设模板它真能跑通挂号、分诊、医生排班、处方开立、费用结算全链路且数据库设计经得起三甲医院门诊日均3000人次压力校验你手头那份标着“高分毕设”的医院挂号诊疗管理系统代码包大概率不是在Django Admin里点几下就生成的CRUD Demo。它得让患者能选科室、挑医生、预约时段让分诊护士看到实时叫号队列让医生在接诊页一键调取既往病历、开电子处方、关联检查检验让收费窗口按医保类型自动拆分结算项——所有动作必须原子性写入MySQL不能出现“挂号成功但号源没扣减”或“处方已开但药品库存未锁定”的脏数据。我带过12届计算机专业毕设见过太多学生用SQLite硬扛并发、用全局变量模拟排队、用time.sleep()假装异步——结果答辩时一压测就崩。这套系统真正值“高分”的地方在于它把Django的ORM事务控制、MySQL的行级锁与外键约束、以及真实医疗业务流里的状态机如“已挂号→已签到→就诊中→已完成→已退号”拧成一股绳。适合正在找毕设选题、想拿答辩高分、又不愿抄个空壳的同学——它不教你Python语法但教你怎么用Django把业务规则变成可执行、可验证、可回滚的代码。2. 从零搭起可运行环境避开Windows下MySQL服务启动失败、Django连接池超时、字符集乱码这三大拦路虎2.1 安装MySQL 5.7.44并配置医疗业务必需的字符集与权限模型很多同学卡在第一步MySQL安装完Django连不上报错1045 Access denied或1366 Incorrect string value。这不是Django的问题是MySQL初始化配置没对。医疗系统必须支持中文病名、中药名、诊断描述utf8mb4是底线utf8会丢表情符号和生僻字比如“龘”字在中药名里真有。别用网上那些一键安装包手动配# Windows下以管理员身份运行cmd进入MySQL bin目录 mysqld --initialize-insecure --console # 记住控制台输出的临时root密码形如: rootlocalhost: xxxxxx mysqld --install MySQL57 net start MySQL57 mysql -u root -p-- 登录后立即执行关键 ALTER DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER hospital_applocalhost IDENTIFIED BY H0sp1tl2024; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON hospital.* TO hospital_applocalhost; FLUSH PRIVILEGES; -- 验证字符集 SHOW VARIABLES LIKE character_set%; SHOW VARIABLES LIKE collation%;提示utf8mb4_unicode_ci比utf8mb4_general_ci更准尤其对中文排序如“张三”“李四”按拼音序医疗系统里科室列表、医生姓名排序必须可靠。2.2 创建Django项目骨架并配置多数据库连接池防雪崩毕设常被忽略的致命点单连接处理并发挂号请求。当10人同时抢号Django默认的CONN_MAX_AGE0会让每次请求都新建MySQL连接瞬间打满max_connections默认151。必须上连接池pip install django3.2.23 # 毕设稳妥选LTS版避坑4.x的ASGI强依赖 django-admin startproject hospital_sys . python manage.py startapp registration # 挂号 python manage.py startapp doctor # 医生排班 python manage.py startapp prescription # 处方settings.py关键配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: hospital, USER: hospital_app, PASSWORD: H0sp1tl2024, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, CONN_MAX_AGE: 60, # 连接复用60秒 CONN_HEALTH_CHECKS: True, } } # 启用Django DB连接池需额外装django-db-geventpool # pip install django-db-geventpool # INSTALLED_APPS [db_geventpool] # DATABASES[default][ENGINE] db_geventpool.backends.mysql注意STRICT_TRANS_TABLES强制报错而非静默截断避免“张伟”存成“张伟”末尾空格被删、“高血压3级很高危”被截成“高血压3级很”——临床数据容不得半点模糊。2.3 初始化核心数据表结构为什么挂号表要拆成RegistrationRegistrationSlot两张直接看models.py里最关键的挂号模型设计# registration/models.py class Department(models.Model): name models.CharField(max_length50, verbose_name科室名称) # 如心血管内科 code models.CharField(max_length10, uniqueTrue, verbose_name科室编码) class Doctor(models.Model): name models.CharField(max_length30, verbose_name医生姓名) department models.ForeignKey(Department, on_deletemodels.PROTECT, verbose_name所属科室) title models.CharField(max_length20, verbose_name职称) # 主任医师/主治医师 class RegistrationSlot(models.Model): 号源表记录某医生某时段可挂多少号 doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE) date models.DateField(verbose_name出诊日期) session models.CharField(max_length10, choices[(AM, 上午), (PM, 下午)]) total_slots models.PositiveSmallIntegerField(default30, verbose_name总号源) used_slots models.PositiveSmallIntegerField(default0, verbose_name已用号源) class Meta: unique_together (doctor, date, session) # 防止重复建号源 class Registration(models.Model): 挂号主表记录患者挂号行为 patient_id models.CharField(max_length18, verbose_name身份证号) name models.CharField(max_length30, verbose_name患者姓名) slot models.ForeignKey(RegistrationSlot, on_deletemodels.PROTECT, verbose_name所挂号源) reg_time models.DateTimeField(auto_now_addTrue, verbose_name挂号时间) status models.CharField(max_length10, choices[ (REGISTERED, 已挂号), (CHECKED_IN, 已签到), (IN_PROGRESS, 就诊中), (COMPLETED, 已完成), (CANCELLED, 已退号) ], defaultREGISTERED) def save(self, *args, **kwargs): if self.pk is None: # 新建挂号 with transaction.atomic(): # 关键事务保证号源扣减原子性 slot self.slot if slot.used_slots slot.total_slots: raise ValidationError(号源已满) slot.used_slots 1 slot.save() super().save(*args, **kwargs) else: super().save(*args, **kwargs)为什么拆两张表RegistrationSlot存号源总量与已用数用used_slots字段做乐观锁避免SELECT ... FOR UPDATE的复杂度Registration只存挂号事实不冗余号源信息便于按患者查历史挂号记录unique_together约束确保不会给同一医生同一天同一时段建两套号源——这是排班逻辑的基石3. 实现挂号核心事务用Django ORM的select_for_update()锁住号源再用MySQL行级锁防超挂3.1 挂号接口的原子性保障为什么transaction.atomic()不够必须加select_for_update()很多毕设代码这样写# ❌ 危险存在超挂风险 slot RegistrationSlot.objects.get(idslot_id) if slot.used_slots slot.total_slots: slot.used_slots 1 slot.save() Registration.objects.create(patient_idpid, slotslot)问题在哪两个请求几乎同时读到used_slots29都判断30为True然后都执行1并保存——结果used_slots变成31超挂1人。这就是经典的“读-改-写”竞态。解决方案是让数据库在读的时候就加锁# ✅ 正确用select_for_update()获取行级写锁 from django.db import transaction def create_registration(patient_id, slot_id): try: with transaction.atomic(): # 锁定该号源行其他事务必须等待 slot RegistrationSlot.objects.select_for_update().get(idslot_id) if slot.used_slots slot.total_slots: raise ValueError(号源已满请选择其他时段) slot.used_slots 1 slot.save() reg Registration.objects.create( patient_idpatient_id, slotslot, statusREGISTERED ) return reg except DatabaseError as e: # 锁等待超时默认50秒捕获并友好提示 raise ValueError(系统繁忙请稍后再试)提示select_for_update()在MySQL InnoDB引擎下对应SELECT ... FOR UPDATE语句锁的是索引行这里id是主键所以锁单行。如果slot_id没走索引比如用name查询会锁整张表——务必确保RegistrationSlot.id有主键索引。3.2 分诊叫号逻辑用Django信号监听挂号状态变更触发实时队列更新挂号完成后分诊屏要立刻显示下一个叫号。不能靠前端轮询耗资源要用Django信号解耦# registration/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Registration receiver(post_save, senderRegistration) def update_queue_on_registration(sender, instance, created, **kwargs): if created and instance.status REGISTERED: # 发送消息到Redis队列毕设可用本地文件模拟但生产必须用Redis with open(current_queue.txt, a) as f: f.write(f{instance.patient_id},{instance.name},{instance.slot.doctor.name}\n)# 在views.py挂号成功后触发 from django.dispatch import Signal registration_created Signal() # views.py def register_view(request): if request.method POST: reg create_registration(...) registration_created.send(senderRegistration, instancereg) return JsonResponse({status: success, queue_no: get_next_queue_no()})注意毕设演示时可用文件模拟队列但答辩老师若问“高并发怎么保证顺序”必须答出“用Redis List LPUSH/RPOP Lua脚本保证原子性”这是加分项。3.3 医生接诊页状态流转用Django Model的status字段驱动业务流程拒绝if-else地狱医生点击“开始接诊”时系统必须校验① 该患者是否已签到statusCHECKED_IN② 是否已被其他医生接诊statusIN_PROGRESS且doctor_id ! current_doctor③ 是否已过号当前时间 预约时段结束时间把这些规则写进Model方法而不是View里堆if# registration/models.py class Registration(models.Model): # ... 字段定义同上 def can_start_consult(self, doctor): 医生能否开始接诊此患者 if self.status ! CHECKED_IN: return False, 患者尚未签到 if self.status IN_PROGRESS and self.doctor_id ! doctor.id: return False, 患者已被其他医生接诊 # 校验是否过号获取该号源的时段结束时间需在RegistrationSlot加start_time/end_time字段 slot self.slot end_time datetime.combine(slot.date, slot.end_time) # 假设slot有end_time字段 if timezone.now() end_time: return False, 已过号请联系分诊护士 return True, 可以接诊 # views.py def start_consult(request, reg_id): reg get_object_or_404(Registration, idreg_id) can, msg reg.can_start_consult(request.user.doctor) if not can: return JsonResponse({error: msg}) reg.status IN_PROGRESS reg.doctor request.user.doctor reg.save() return JsonResponse({status: ok})4. 避坑挂号系统里最常踩的5个血泪坑每个都导致答辩当场被叫停4.1 现象挂号成功但后台查不到记录MySQL日志显示Deadlock found when trying to get lock原因多个请求同时操作同一号源select_for_update()加锁顺序不一致如A先锁slot1再锁slot2B先锁slot2再锁slot1引发死锁。MySQL自动回滚其中一个事务但Django没捕获异常前端以为成功。解决统一加锁顺序。在create_registration函数里对slot_id做排序哪怕只有一个或用RegistrationSlot.objects.select_for_update().filter(id__in[slot_id]).order_by(id)强制按ID升序加锁。4.2 现象患者退号后号源没释放used_slots仍为满值原因退号逻辑没走事务或slot.used_slots - 1后没调slot.save()。更隐蔽的是退号时Registration状态改为CANCELLED但没同步更新RegistrationSlot.used_slots。解决退号必须用transaction.atomic()包裹并在Registration的save()方法里判断status变更# registration/models.py def save(self, *args, **kwargs): is_new self.pk is None old_status None if not is_new: old_status Registration.objects.get(pkself.pk).status super().save(*args, **kwargs) # 状态从REGISTERED变为CANCELLED释放号源 if old_status REGISTERED and self.status CANCELLED: with transaction.atomic(): slot self.slot slot.used_slots max(0, slot.used_slots - 1) # 防负数 slot.save()4.3 现象医生排班页面加载极慢Chrome Network显示SQL查询耗时2s原因Doctor模型里没给department字段加数据库索引SELECT * FROM doctor WHERE department_id ?全表扫描。三甲医院医生超500人没索引就是灾难。解决在Doctor.department字段加db_indexTrue并运行python manage.py makemigrations python manage.py migrateclass Doctor(models.Model): # ... department models.ForeignKey( Department, on_deletemodels.PROTECT, verbose_name所属科室, db_indexTrue # 关键 )4.4 现象医保结算金额算错自费部分多扣了10元原因MySQLDECIMAL(10,2)字段存金额但Python用float计算如total * 0.3二进制浮点误差累积。0.1 0.2 ! 0.3在金融场景是致命的。解决全程用decimal.Decimalfrom decimal import Decimal def calculate_insurance_amount(total_fee): insurance_rate Decimal(0.7) # 用字符串初始化避免float转义 return (total_fee * insurance_rate).quantize(Decimal(0.01)) # 保留两位小数4.5 现象导出Excel患者列表时中文全是乱码原因Django HttpResponse设置Content-Type为application/vnd.ms-excel但没指定字符集浏览器用GBK解码UTF-8内容。解决用openpyxl库生成xlsx原生支持UTF-8而非csvfrom openpyxl import Workbook from openpyxl.styles import Font def export_registrations(request): wb Workbook() ws wb.active ws.title 挂号列表 # 设置表头字体防止中文宋体乱码 font Font(nameMicrosoft YaHei, size10) for cell in ws[1]: cell.font font # 写入数据... response HttpResponse(content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet) response[Content-Disposition] attachment; filenameregistrations.xlsx wb.save(response) return response5. 让毕设答辩脱颖而出用Django Admin定制化MySQL慢查询日志定位性能瓶颈5.1 Django Admin深度定制给挂号表加“今日挂号统计”仪表盘让老师一眼看到系统价值默认Admin就是个表格答辩时老师扫一眼就过。必须加业务洞察# registration/admin.py from django.contrib import admin from django.db.models import Count, Sum from .models import Registration, RegistrationSlot admin.register(Registration) class RegistrationAdmin(admin.ModelAdmin): list_display (patient_id, name, slot, reg_time, status) list_filter (status, slot__date, slot__doctor__department) search_fields (patient_id, name, slot__doctor__name) # 自定义动作批量修改状态 actions [mark_as_checked_in, mark_as_completed] def mark_as_checked_in(self, request, queryset): queryset.filter(statusREGISTERED).update(statusCHECKED_IN) mark_as_checked_in.short_description 标记为已签到 # 添加侧边栏统计重点 def changelist_view(self, request, extra_contextNone): extra_context extra_context or {} # 统计今日挂号数、各科室分布 today timezone.now().date() stats Registration.objects.filter( reg_time__datetoday ).values(slot__doctor__department__name).annotate( countCount(id) ).order_by(-count) extra_context[department_stats] list(stats) extra_context[today_total] Registration.objects.filter(reg_time__datetoday).count() return super().changelist_view(request, extra_contextextra_context) # templates/admin/registration/change_list.html # 在Admin模板里插入统计卡片需重写模板提示答辩时打开Admin老师问“系统有什么特色”你点开挂号列表侧边栏立刻显示“今日挂号287人心内科42人呼吸科38人…”——比讲10分钟原理管用。5.2 用MySQL慢查询日志揪出隐藏性能杀手定位那个拖垮系统的JOIN毕设常被忽略的硬核技能证明你的系统“快”。不是说“响应很快”而是拿出证据-- 开启慢查询日志MySQL命令行 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 0.1; -- 超0.1秒就算慢查询 SET GLOBAL log_output TABLE; -- 日志存到mysql.slow_log表然后模拟压力测试用ab或Python脚本发100并发挂号请求再查慢SQLSELECT query_time, sql_text, rows_sent, rows_examined FROM mysql.slow_log WHERE sql_text LIKE %registration% ORDER BY query_time DESC LIMIT 10;典型慢SQL及优化慢SQL问题修复SELECT * FROM registration WHERE slot_id IN (SELECT id FROM registrationslot WHERE date2024-05-20)子查询无索引全表扫描在registrationslot.date加索引ALTER TABLE registrationslot ADD INDEX idx_date (date);SELECT r.*, s.* FROM registration r JOIN registrationslot s ON r.slot_ids.id WHERE s.doctor_id123JOIN没走索引r.slot_id未索引给registration.slot_id加外键索引ALTER TABLE registration ADD INDEX idx_slot_id (slot_id);血泪经验答辩前务必跑一次慢查询分析把优化前后的QPS每秒查询数截图放进PPT。老师看到“优化后挂号接口从12QPS提升到87QPS”立刻明白你不是在玩虚的。5.3 最后一道保险用Django TestCase写挂号核心逻辑的单元测试堵住“我以为没问题”的漏洞毕设代码最怕“本地能跑答辩现场崩”。必须写测试# registration/tests.py from django.test import TestCase, TransactionTestCase from django.db import transaction from .models import RegistrationSlot, Registration class RegistrationTest(TransactionTestCase): def setUp(self): self.slot RegistrationSlot.objects.create( doctor_id1, date2024-05-20, sessionAM, total_slots2, used_slots0 ) def test_cannot_overbook(self): 测试超挂防护 Registration.objects.create(patient_id110, slotself.slot) Registration.objects.create(patient_id111, slotself.slot) # 第三次应失败 with self.assertRaises(ValueError): Registration.objects.create(patient_id112, slotself.slot) def test_cancel_release_slot(self): 测试退号释放号源 reg Registration.objects.create(patient_id110, slotself.slot) self.assertEqual(self.slot.used_slots, 1) reg.status CANCELLED reg.save() self.slot.refresh_from_db() self.assertEqual(self.slot.used_slots, 0)运行测试python manage.py test registration.tests.RegistrationTest为什么用TransactionTestCase而非TestCase因为select_for_update()需要真实数据库事务TestCase的事务回滚机制会干扰锁行为。希望帮到你。我带毕设时反复强调答辩不是考你写了多少行代码而是考你能不能说清“为什么这么写”“哪里可能出错”“怎么证明它靠谱”。这套挂号系统真正的价值不在功能列表有多长而在每一个transaction.atomic()、每一处select_for_update()、每一条慢查询优化背后你对真实医疗业务流的理解。本文还有配套的精品资源点击获取