资讯动态

医院预约挂号系统设计与实现:从并发控制到毕业答辩全指南

发布时间:2026/10/21 8:06:01 来源:尧图企业网站定制
1. 这个烂大街的毕设题目到底在考你什么每年毕业季前后我都能碰到几拨来问医院预约挂号系统的同学。有的是导师直接给了这个题有的是在Python毕设推荐清单里看到它还有的是从网上下载了一堆号称全套源码文档的压缩包结果解压完连环境都跑不起来。这个题目年年有人做表面看就是一个普通的Web系统但真把它从能演示做到能答辩需要的东西其实不少用户认证、角色权限、数据建模、事务并发、状态机、部署调试几乎把Web开发的地基全部过了一遍。先把业务边界说清楚。一个能拿去答辩的医院预约挂号系统至少要覆盖三类角色患者注册登录、浏览科室和医生、查看排班、预约挂号、取消预约、查看历史记录医生查看自己的出诊排班、查看预约患者、标记就诊状态管理员维护科室、维护医生、维护排班、查看预约统计很多人拿到题目的第一反应是这不就是增删改查嘛。这话说对了一半。增删改查只是底座真正拉开差距的是预约这个动作背后的规则一个医生一个时段号源就那么多两个人同时发起预约系统凭什么把号给其中一个人而不是另一个人这就是标题里设计与实现中设计两个字的分量。我拆解过不少网上下载的源码包页面看着五脏俱全但预约表十有八九没有唯一约束后端不加锁靠前端按钮禁用冒充防重复提交。这种项目演示没问题被评委追问两句就露馅。所以这篇文章我不打算贴一份现成代码让你复制而是把做这类系统的完整思考链路整理出来表怎么建、预约怎么防超卖、状态怎么流转、部署踩了哪些坑、文档和答辩怎么准备。你照着这个思路自己写一遍远比背一份陌生代码更稳。1.1 需求拆解把挂号翻译成技术动作把业务需求翻译成技术动作是这个项目的第一道考题。患者看医生排班对应的是按科室查医生、按医生查排班、按排班查余量预约挂号对应的是校验身份、校验排班状态、校验余量、防重复、落库、更新余量取消预约对应的是校验状态、释放号源、记录取消时间。这些一一对应的映射在需求分析阶段就要列清楚因为它们直接决定后面建几张表、写几个接口。另一个容易被忽略的点是时间窗口。不是所有时刻都能预约出诊前一天的排班能不能约就诊时间已经过了还能不能取消爽约怎么判定这些规则最好前期用文字定死落到代码里就是几个简单的状态判断如果前期不定写到后面就是到处都是补丁逻辑自己都说不清为什么这里能约那里不能约。1.2 别被全套源码带偏节奏市面上那些全套源码文档远程调试讲解定制的套餐卖的本质是解决问题的时间不是代码本身。代码拿到手不消化答辩照样过不了。我的建议是源码可以看但要带着问题看——它的表是怎么设计的它在哪里加锁它的状态机是怎么定义的把它当成参考教材而不是交付物。你真正要交付的是你自己脑子里那套能讲清楚的设计这也是我下面每个章节都把为什么这么做讲透的原因。2. 技术选型为什么我坚持用Django而不是Flask2.1 三套框架的实际体感Python做Web后端主流就是三个选择Django、Flask、FastAPI。三套我都实际写过分别说下在毕设场景下的真实感受。Django是全家桶路线ORM、Admin后台、用户认证、表单、分页、消息框架全是自带的。学习曲线前两三天略陡一旦跨过路由-视图-模型那道坎后面开发速度会非常快。这正好匹配一个人、一个学期、做一个完整系统的毕设节奏。Flask是微框架核心就路由加WSGI什么都要自己装。用Flask意味着你自己选ORM通常是SQLAlchemy、自己搭用户认证Flask-Login之类、自己拼后台管理页面。它能证明我懂底层原理代价是工作量直接翻倍而且各种扩展的版本兼容问题能折磨你好几天。FastAPI最近很火异步高性能、自带接口文档确实香。但它的异步ORM体系、Pydantic模型、依赖注入这些概念对没毕业的同学来说上手门槛偏高。非要用也不是不行但要有心理准备大部分资料是英文的踩到的坑也相对冷门答辩时评委还不一定买账。我的结论是除非导师指定或者你有明确的炫技需求否则选Django。毕设的评价核心是业务逻辑和系统设计不是框架有多新。Django把认证、Admin、ORM这些最烧时间的地基打好了省下来的时间正好花在预约并发、排班建模这些真正的难点上。2.2 前后端分离能不做就不做这里有个常见分叉口用Django模板渲染还是Django写API、前端用Vue或React我明确建议选前者。理由有三个。第一模板渲染开发速度快Django模板加Bootstrap一个页面从零到能看基本是小时级别前后端分离的话至少要多处理跨域、Token认证、前端路由、打包部署一堆事。第二预约流程需要的动态交互其实不复杂选科室后动态加载医生、查看某天余量用Ajax就能优雅解决没必要上完整的前端工程。第三答辩风险低——前后端分离一旦做了评委大概率追问你的Token过期策略是什么跨域怎么配的前端打包后怎么部署每一个都是深坑。当然如果你确实想给项目贴上前后端分离的标签我建议用DRF提供API、Vue3做前端然后把JWT认证和CORS配置写进文档至少有自圆其说的余地。但对大多数同学来说模板渲染 局部Ajax是投入产出比最高的方案。2.3 数据库从SQLite起步到MySQL收尾开发阶段我强烈建议直接用SQLite。零配置、单文件、拷来拷去方便非常适合前期调试。但必须清醒一点SQLite和MySQL在事务模型、锁行为上有明显差异所以并发相关的验证不能只信SQLite的结果。我的做法是开发期SQLite跑功能项目收尾前切换到MySQL做一轮完整的并发和压力测试。切换时重点确认三件事字段类型是否兼容尤其是DateTime的时区表现、字符集是否统一为utf8mb4、事务隔离级别是否确认。这些在毕设文档里写一句开发环境SQLite、生产环境MySQL就能体现工程意识但前提是你真的在两套环境里都跑通过。提示Django连MySQL需要mysqlclientWindows下直接装这个库经常报错建议下载预编译的whl文件安装能省掉一晚上折腾编译器的痛苦。3. 数据库建模五张核心表怎么把业务串成闭环3.1 表结构从用户到预约的一整条链我把核心模型收敛成五张表用户、科室、医生、排班、预约。下面这张表是整套系统的地基字段取舍都有讲究。表名核心字段说明用户表扩展Django自带username, password, role, phonerole区分患者/医生/管理员departmentsname, location, description, status科室基础信息doctorsuser_id, dept_id, title, specialty, introduction关联用户表和科室表schedulesdoctor_id, work_date, time_slot, total_slots, booked_slots, status医生出诊排班与号源appointmentspatient_id, schedule_id, doctor_id, appointment_date, time_slot, status, created_at, canceled_at预约记录用户表这里我要重点说。很多新手会把患者表和医生表分开建结果登录逻辑变得很别扭——患者走一套医生又要另写一套认证。正确做法是统一用Django的auth_user存账号和密码加一个role字段区分身份医生独有的职称、擅长领域等信息放doctors表通过OneToOne指向auth_user。这样Django自带的authenticate、login、logout、request.user全部复用代码量小一个量级。appointments表里我故意冗余了一个doctor_id。明明通过schedule就能找到医生为什么还存一份因为我的预约列表要频繁按医生展示直接按doctor_id过滤能少做两次join。这种适度冗余就是数据库里的反规范化写进文档里能看出你懂设计取舍而不是只会照本宣科建范式表。3.2 排班与号源两种建模方式的取舍排班是整个系统里最需要想清楚的部分。我见过两种主流做法。第一种是号源预生成。排班时直接生成N条号源记录每条对应一个可预约名额比如张医生 周二上午 10:00-10:30一条、10:30-11:00一条。预约时锁定某一条号源逻辑简单直观。缺点是数据量大改排班要级联处理一大堆子记录。第二种是排班计数。schedule表只存张医生周二上午出诊用total_slots表示总号源数booked_slots表示已约数余量就是两者之差。预约时判断booked_slots小于total_slots。优点是灵活管理员临时加号直接把total_slots加1缺点是并发控制难做。我最终选的是第二种。因为医院的排班粒度通常是上午门诊下午门诊这种粗粒度而不是精确到每个人15分钟而且毕设系统里加号停诊操作很常见计数模型的维护成本最低。代价就是预约模块必须把并发控制做扎实这正是下一章要解决的问题。3.3 用户角色与权限一个role字段加装饰器就够了Django自带完整的Group和Permission但毕设场景我更推荐一个更轻的方案用户表加role字段取值是patient、doctor、admin再写一个装饰器统一做权限校验。from functools import wraps from django.shortcuts import redirect from django.http import HttpResponseForbidden def role_required(roles): def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(login) if request.user.role not in roles: return HttpResponseForbidden(无权访问) return view_func(request, *args, **kwargs) return wrapper return decorator用法是在视图上标注 role_required([patient])。整套逻辑不到20行完全透明答辩时从路由到装饰器一路讲下来没有任何黑盒。相比之下用Django Group方案要解释组和权限的关联关系反而把简单的事说复杂了。权限这里还有个细节管理员的登录入口我单独做了一个URL模板也单独继承管理端基类跟患者端的页面结构分开。三条业务线的界面互不干扰后面做菜单级权限控制也方便。4. 预约核心逻辑事务、行锁和状态机的组合拳4.1 一次预约请求的完整生命周期预约是整个系统的心脏。我拆成六个步骤一步都不能少校验登录状态和角色必须是已登录的患者校验排班存在且可预约状态为open且在可预约窗口内校验该用户在这个排班下没有未结束的预约防止重复校验号源余量booked_slots小于total_slots在事务内更新余量并插入预约记录返回结果前端跳转到我的预约网上很多源码的问题就出在第3步和第4步。漏掉第3步用户连续点提交就会生成一堆重复预约第4步如果判断和更新之间没有原子性并发请求同时通过判断号源直接超卖。这两个问题就是评判一个预约系统有没有设计过的分水岭。4.2 用select_for_update锁住排班记录解决并发超卖的标准答案是数据库事务加行锁。Django的ORM提供了select_for_update()底层执行SELECT ... FOR UPDATE对选中的行加排他锁事务提交前其他事务对同一行的更新都会阻塞等待。from django.db import transaction from django.db.models import F transaction.atomic def create_appointment(request, schedule_id): schedule Schedule.objects.select_for_update().get(idschedule_id) if schedule.status ! open: return error_response(该排班不可预约) if schedule.booked_slots schedule.total_slots: return error_response(号源已满) if Appointment.objects.filter( patientrequest.user, scheduleschedule, status__in[booked, completed] ).exists(): return error_response(您已预约过该时段请勿重复预约) Appointment.objects.create( patientrequest.user, scheduleschedule, doctorschedule.doctor, appointment_dateschedule.work_date, time_slotschedule.time_slot, statusbooked ) schedule.booked_slots F(booked_slots) 1 schedule.save(update_fields[booked_slots]) return success_response()这段代码有三个关键点。第一select_for_update()必须在transaction.atomic块里离开事务它不生效。第二锁的是schedule这一行两个并发请求同时进来第二个会卡在查询语句上直到第一个事务提交或回滚。第三更新booked_slots用的是F表达式让数据库执行原子的字段自增避免读出来、改完、写回去这个过程中被其他事务覆盖。我实际压测过同样的代码去掉select_for_update并发50个请求预约同一个医生同一个时段生成了11个成功预约号源余量变成负数加上行锁后成功数严格等于号源数。这个对比结果如果写进毕设文档比任何架构图都有说服力。4.3 状态机让预约记录进入可管理状态预约记录不能只有有和无两种状态。我设计的集合是booked已预约待就诊、completed已完成就诊、canceled患者取消、missed爽约。流转规则集中在一个枚举类里管理class AppointmentStatus: BOOKED booked COMPLETED completed CANCELED canceled MISSED missed TRANSITIONS { BOOKED: {COMPLETED, CANCELED, MISSED}, }为什么需要状态机因为取消预约和标记就诊都依赖当前状态已取消的不能再取消已完成的不能回到待就诊爽约的不能再当成正常预约。把规则集中管理视图层只调流转方法不会出现某处漏判状态的脏逻辑。还有一个细节missed状态怎么产生我没引入Celery定时任务而是做惰性更新——患者查看预约列表时如果发现booked记录的就诊日期已经过去就自动置为missed并释放号源。对毕设系统来说这种惰性处理比定时任务简单得多也够用。4.4 取消预约与号源回补的原子性取消预约看着简单其实和预约一样需要事务保护。把状态改为取消和把booked_slots减1是两个操作必须放同一个事务里否则会出现状态取消了但号源没释放或号源释放了但状态还是booked的中间态。transaction.atomic def cancel_appointment(request, appointment_id): appointment Appointment.objects.select_for_update().get( idappointment_id, patientrequest.user ) if appointment.status ! AppointmentStatus.BOOKED: return error_response(当前状态不可取消) appointment.status AppointmentStatus.CANCELED appointment.canceled_at timezone.now() appointment.save(update_fields[status, canceled_at]) schedule Schedule.objects.select_for_update().get(idappointment.schedule_id) schedule.booked_slots F(booked_slots) - 1 schedule.save(update_fields[booked_slots]) return success_response()这里有两个容易踩的坑。第一个是必须限制patientrequest.user否则用户可以拿别人的预约单号去取消。第二个是加锁顺序这里先锁appointment再锁schedule而创建预约时先锁schedule再插appointment两个事务拿锁顺序不一致并发场景下可能死锁。怎么排查这个死锁我在调试章节专门讲。5. 管理员、患者、医生三个端口的界面与流程设计5.1 管理后台Django Admin打底再做三处自定义管理端如果完全自己写页面工作量会吃掉你大量时间。我的做法是先用Django Admin把科室、医生、排班、预约四块管理能力搭起来然后针对三个高频场景做自定义。第一处是排班管理。默认Admin表单只能一条条添加我用自定义admin action实现了批量生成排班选择医生、填起始日期和结束日期、勾选上午下午时段一键生成多条schedule记录。这个功能演示时非常抢眼因为它解决的是真实痛点不是凑功能。第二处是预约列表的筛选。list_filter按状态、科室、日期过滤search_fields支持按患者姓名和医生姓名搜索。预约查询是管理员最高频的操作筛选搜索做不好后台再好看也没用。第三处是仪表盘。我加了一个统计页展示今日预约量、近一周预约趋势、各科室预约占比数据用ORM的aggregate和annotate算出来图表用纯CSS进度条就能做得清爽。答辩时这一页能直观说明系统产生了数据、数据能被利用是很好的加分点。5.2 患者端一条路走到黑的预约流程患者端核心路径是首页进科室列表科室详情看医生列表医生详情看未来7天排班选时段确认预约最后在个人中心看记录。这条路径要做到点击次数最少、每一步有明确反馈。排班页最值得打磨。我做成医生信息 未来7天排班表的布局按日期分列每列显示上午、下午两个时段的余量余量为0的按钮直接置灰。前端拿到后端JSON数据后用JavaScript渲染Django返回JsonResponse前端用fetch或jQuery的ajax处理这是标准的局部刷新方案。个人中心要展示什么我的预约列表里每条显示医生、科室、日期、时段、状态、操作按钮。状态用不同颜色标签区分操作按钮按状态动态出现booked显示取消预约completed显示查看详情canceled和missed不再显示操作。这块逻辑就是上一章状态机在前端的映射前后端规则保持一致才不会出现页面按钮跟真实状态对不上的尴尬。5.3 医生端一个页面搞定出诊日常医生端的核心场景是今天有哪些患者约了我。我的设计是一个工作台页面左侧是排班列表右侧是选中排班下的预约患者名单每位患者后面有确认就诊按钮。这个页面同时查schedule和appointment两个表通过schedule_id关联前端做一个简单的联动交互。确认就诊这个操作需要做状态校验仅当预约状态是booked且就诊日期是今天时才允许确认防止医生把明天的患者提前标记为已就诊。这种业务校验在视图层写清楚比在模板里用if判断来得可靠。医生端我还做了一个低成本的隐藏功能当某天号源预约进度超过八成时排班列表显示进度条提醒。成本很低但体现了你考虑过用户真实使用场景比堆砌大图表更能打动评委。注意医生端和患者端的URL路由一定要分开命名空间例如/doctor/和/patient/。否则一旦出现医生也能预约自己这种荒谬漏洞答辩时很难收场。6. 调试与部署那些远程调试经常要解决的坑6.1 时区问题预约日期为什么会错一天Django默认USE_TZ为True所有时间按UTC存储。如果代码里用了datetime.now()而没用timezone.now()就会出现存进去是本地时间、读出来是UTC时间的错乱表现就是预约日期显示成前一天。我的统一约定是所有DateTimeField一律用django.utils.timezone.now()写入排班的work_date这种纯日期字段用DateField只存日期不做时区转换前端显示时间用模板过滤器或前端库按本地时区格式化。把日期和时间两类字段彻底分开这个坑基本就绝了。还有一个连带问题MySQL连接配置里的时区和Django的时区如果不一致同样会出现偏差。我一般在配置里统一设置为东八区数据库连接参数也显式指定时区双保险。6.2 死锁两个事务互相等待的完整排查链路这是我实际遇到过一次的经典问题。现象是压测时偶尔报deadlock detected错误日志里有两条SQL互相等待。起因是创建预约的事务先锁schedule再插appointment而取消预约的代码一开始写的是先锁appointment再锁schedule。当两个事务并发时事务A锁了schedule等待appointment事务B锁了appointment等待schedule正好形成一个环。完整排查过程是这样的。第一步去数据库错误日志里grep到死锁的SQL拿到两个事务各自执行到的语句。第二步用命令查看最近一次死锁的详细信息确认锁模式是排他锁。第三步画出两个事务的持锁和等待关系发现闭环。第四步修改取消预约代码让加锁顺序和创建预约保持一致——都是先锁schedule再操作appointment。第五步重新压测死锁消失。这套看日志-画环-统一顺序-复测的方法对一切锁顺序不一致导致的死锁都适用。我把这段排查过程原样写进了毕设文档答辩时评委看到你连死锁都系统性处理过印象分会明显不一样。6.3 部署三件套静态文件、字符集、域名白名单开发环境一切正常、部署到服务器就变形的经典问题有三个。第一个是静态文件全丢。原因多半是DEBUG关闭后Django不再托管静态文件需要用collectstatic把static目录收集到STATIC_ROOT再由Web服务器托管。STATIC_URL、STATIC_ROOT、STATICFILES_DIRS三个配置各司其职缺一个都会出问题。第二个是中文乱码。Django侧的LANGUAGE_CODE设为中文MySQL侧的库表字符集也必须确认是utf8mb4。很多服务器默认是latin1建库时不指定就埋了雷。第三个是ALLOWED_HOSTS。DEBUG关闭后Django会拦截所有不在白名单里的Host服务器IP或域名没加进去整个站点直接403。这三个都属于配置性错误但恰恰是远程调试里最常被问到的三类求助内容部署文档里提前写清楚能省下大量来回折腾的时间。7. 文档与答辩代码之外的分数怎么拿7.1 两张图定乾坤答辩PPT里最有力的不是页面截图而是两张图系统架构图和数据库ER图。架构图表达浏览器-服务端应用-数据库三层结构以及每层核心组件ER图画清楚五张表之间的主外键关系、一对多关系。建议用绘图工具排版导出矢量图别拿截图软件随便画。别小看这两张图。评委扫一眼就知道你有没有全局视角架构图里是否画出Web服务器、数据库、缓存这些层次说明你想没想过真实运行环境ER图外键关系是否清晰说明你建模是否认真。两张图占的版面不大但信息密度极高值得花半天时间好好画。7.2 准备好一个难点故事评委最常问的问题是你觉得这个项目里哪个地方最难如果回答都挺简单的等于主动放弃这轮加分。我的建议是准备一个完整的难点故事就用预约并发这件事问题并发预约时号源可能超卖排查用压测脚本并发50个请求复现发现成功预约数超过号源数方案事务加select_for_update行锁配合F表达式原子自增验证加上锁之后成功数严格等于号源数这个故事有现象、有数据、有解法、有验证是一条完整闭环。评委想听的就是这种发现问题-分析问题-解决问题的过程而不是背概念、念手册。7.3 文档结构的顺序与测试记录的含金量毕设文档建议按这个顺序写绪论背景与意义、需求分析功能点加用例图、系统设计技术架构、数据库设计、接口设计、系统实现核心模块代码与截图、系统测试功能测试加并发测试、总结与展望。其中最有含金量的是系统测试这一章。不要只贴打开页面显示正常这种套话要贴真实的边界测试记录连续点击两次预约提交、两个账号同时抢最后一个号源、取消预约后立即再次预约、超过就诊日期未取消被标记为爽约。每一条都对应一个真实的功能点测试结果和预期一致用表格列出。这些记录放在文档里比任何系统运行稳定的空话都有说服力也让整个项目从能跑变成被验证过。按这套思路做完你会发现最难的其实不是写代码而是把每个决策背后的理由想清楚。这也是为什么我一直建议参考源码但不要抄源码——把技术方案吃透变成自己能表达的东西这个项目才真正属于你。

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

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

免费获取报价 →
↑