尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Django医院挂号系统实战:从模型设计到并发控制

Django医院挂号系统实战:从模型设计到并发控制 简介这是一套基于Python Django框架开发的医院挂号诊疗系统毕业设计源码案例面向计算机相关专业的在校学生、教师以及初入行的开发者适用于毕业设计、课程设计、作业、项目初期立项演示也可作为Django全栈开发的学习进阶素材。压缩包内文件数量达到2000个其中JavaScript脚本1633个、HTML页面249个、CSS样式53个另有JSON数据文件、Python后端文件和说明文档覆盖前端交互、页面布局与后端逻辑结构清晰便于查阅和二次开发。资源包大小约5.57MB附带详细设计文档项目代码已经过测试并成功运行获得导师认可答辩评审分达95分整体完成度较高。目前已有54人学习对于需要快速搭建挂号诊疗场景、理解Django项目结构或完成课程设计与毕业论文的读者具有很强的参考价值可直接使用或在此基础上扩展功能。1. 医院挂号系统不只是一个 CRUD先看这个 Django 项目的分层设计Django 写的医院挂号诊疗系统在毕业设计和课程设计里出现频率很高但大多数人拿到源码后只把页面跑通就停了真正去拆数据流的人不多。这份资源带完整文档和测试通过的源码核心是把「科室—医生—号源—挂号记录—缴费记录」这条链路在 Django 里完整落地前端基于 Bootstrap 全家桶日期时间选择用的是 bootstrap-datetimepicker整体是典型 MTV 结构没有引入前后端分离适合用来理解 Django 原生工作机制也适合改造成小型诊所的预约系统。它解决的不仅是「挂个号」这个动作还包括科室管理、医生排班、患者档案、挂号费统计这些配套场景。对刚接触 Python 和 Django 的人把这份代码从头读一遍比照着教程敲十遍有效得多。2. 数据模型与挂号流程建模从科室表到挂号记录的字段设计2.1 五张核心表的关联关系与选型理由在 Django 里建模医院挂号最先要决定的是「号源」怎么表达。常见做法有两种一种是单独建一张排班表每天由后台生成当天可挂的号码段另一种是不建号源表把挂号记录当作事实表医生上只存一个每日号源上限通过统计当天已挂数量判断是否约满。这个资源用的是第二种理由是它省掉了一套定时生成号源的逻辑在毕设演示和中小流量场景下完全够用代码量也少一截。建议先理解这套关联关系Department科室一对多 Doctor医生Patient患者一对多 Appointment挂号记录Appointment 外键关联 Doctor 和 Patient再外键关联一个 User 记录操作人。挂号费、缴费记录等扩展信息挂在 Appointment 上而不是反过来这样查询链路短患者查历史记录、医生查当日号单都只需要一次 join。字段设计可以直接照下表对齐这套命名在资源文档里也能一一对应上模型关键字段类型与约束说明Departmentname, floorCharField, PositiveSmallIntegerField科室名称加 unique 约束Doctorname, department(FK), title, daily_limitForeignKey, PositiveIntegerField职称可空号源上限默认 30Patientname, id_card, phone, birth_dateCharField, DateFieldid_card 加 unique 约束Appointmentdoctor(FK), patient(FK), date, time_slot, status, feeDateField, CharField, DecimalFieldstatus 表示已挂/已就诊/已取消Paymentappointment(OneToOne), amount, pay_timeOneToOneField, DecimalField缴费记录一对一挂接选 PositiveIntegerField 而不是 IntegerField是为了在数据库层面挡住负数号源。fee 用 DecimalField 而不是 FloatField避免金额精度问题这是批量统计挂号费时最容易踩的坑FloatField 累加会出现 0.1 0.2 不等于 0.3 的问题。2.2 Appointment 模型代码与迁移要点核心模型可以这样写和资源里的结构基本一致from django.db import models from django.contrib.auth.models import User class Department(models.Model): name models.CharField(科室名称, max_length50, uniqueTrue) floor models.PositiveSmallIntegerField(所在楼层, default1) def __str__(self): return self.name class Doctor(models.Model): name models.CharField(医生姓名, max_length30) department models.ForeignKey(Department, on_deletemodels.CASCADE, related_namedoctors) title models.CharField(职称, max_length20, blankTrue) daily_limit models.PositiveIntegerField(每日号源上限, default30) def __str__(self): return f{self.department.name}-{self.name} class Appointment(models.Model): STATUS_CHOICES ( (booked, 已挂号), (done, 已就诊), (cancelled, 已取消), ) doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE, related_nameappointments) patient models.ForeignKey(Patient, on_deletemodels.CASCADE, related_nameappointments) operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue) date models.DateField(就诊日期) time_slot models.CharField(时段, max_length20, default上午) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultbooked) fee models.DecimalField(挂号费, max_digits8, decimal_places2, default10.00) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [date, time_slot] indexes [models.Index(fields[doctor, date])]这里有两个细节值得注意。related_name 的作用是定义反向查询名doctor.appointments.all() 可以直接拿到某位医生的全部挂号记录不写的话 Django 默认生成 appointment_set可读性差很多。operator 用 on_deletemodels.SET_NULL 而不是 CASCADE因为后台操作员工号被删除时挂号记录本身是业务数据不能跟着删SET_NULL 要求字段可空所以补了 nullTrue。Meta 里给 (doctor, date) 建联合索引是因为这个项目里查询最频繁的接口就是「某医生某天还剩几个号」。没有这个索引数据量到几万条时页面会明显变慢。之后执行 python manage.py makemigrations 和 python manage.py migrate 生成并应用迁移如果初次跑 migrate 报 MySQL 客户端依赖错误常见做法是先装 mysqlclientWindows 下下载对应的 whl 包再 pip installLinux 下先确保安装了 libmysqlclient-dev。2.3 号源校验逻辑放在哪一层号源校验是这个系统的核心业务规则每个医生每天最多挂 daily_limit 个号。我一般把这个校验放在 Form 层的 clean 方法里而不是视图里这样无论从哪个入口提交都会走同一套规则不会出现视图 A 校验了、视图 B 漏掉的情况。from django import forms from .models import Appointment, Doctor class AppointmentForm(forms.Form): doctor forms.ModelChoiceField( querysetDoctor.objects.select_related(department), label医生) date forms.DateField(label就诊日期, widgetforms.DateInput( attrs{class: form-control, id: id_date})) time_slot forms.ChoiceField( choices((上午, 上午), (下午, 下午)), label时段) def clean(self): cleaned super().clean() doctor cleaned.get(doctor) date cleaned.get(date) if doctor and date: booked Appointment.objects.filter( doctordoctor, datedate, status__in(booked, done) ).count() if booked doctor.daily_limit: raise forms.ValidationError( f{doctor.name} 在 {date} 的号源已满) return cleaned这段代码的核心是用 count() 做数量判断而不是把记录全部取到内存里。status__in(booked, done) 表示已挂号和已就诊的都占用号源只有 cancelled 的号自动释放。但这里有一个看不到的隐患count 校验不做并发控制两个请求同时提交时可能都通过校验导致超挂。这个问题到最后一章讲事务和行锁时一并解决做并发压测的人一定会遇到。3. 视图、路由与前端的协作挂号表单是怎么提交的3.1 URL 设计与视图函数拆分项目是 MTV 架构路由全部在 urls.py 里显式声明没有用 DRF 那套。常见做法是挂号相关的路径单独放一个 app 管理创建 app 用 python manage.py startapp registration然后在主 urls.py 里用 include 挂进来。视图以函数视图为主逻辑直观调试时打断点也方便。# registration/urls.py from django.urls import path from . import views urlpatterns [ path(book/, views.book_appointment, namebook), path(cancel/int:pk/, views.cancel_appointment, namecancel), path(my/, views.my_appointments, namemy_appointments), ]path 里的 int:pk 是 Django 的路径转换器它保证了 cancel/abc/ 这种非法请求直接返回 404不会进视图再抛 ValueError。name 参数是 URL 的别名模板里用 {% url book %} 反查完整路径后期调整路由结构时不用改模板这一点在频繁改需求的毕设阶段非常省事。主 urls.py 里对应写 path(registration/, include(registration.urls))前缀统一挂在 registration 下。3.2 视图里的数据流与 messages 消息传递挂号视图的逻辑是GET 请求渲染空表单POST 请求校验、建记录、重定向。注意重定向而不是直接渲染结果页这对应了 PRG 模式Post-Redirect-Get防止用户刷新页面时重复提交挂号。from decimal import Decimal from django.shortcuts import render, redirect from django.contrib import messages from .forms import AppointmentForm from .models import Appointment def book_appointment(request): if request.method POST: form AppointmentForm(request.POST) if form.is_valid(): doctor form.cleaned_data[doctor] Appointment.objects.create( doctordoctor, patientrequest.user.patient, operatorrequest.user, dateform.cleaned_data[date], time_slotform.cleaned_data[time_slot], feeDecimal(10.00), ) messages.success(request, 挂号成功请按时就诊) return redirect(my_appointments) else: form AppointmentForm() return render(request, registration/book.html, {form: form})这里注意三点request.user.patient 依赖的是 User 与 Patient 之间的一对一关联前提是在 User 创建时同步建了 Patient 档案资源里是在注册视图里一起处理的。fee 先取 Decimal(10.00) 是硬编码实际项目里应改成从科室或医生配置读取资源文档里有对应的配置项。messages.success 写入一次性的会话消息模板里消费完就清除这正是「重定向传递数据」的标准做法比在 URL 里拼参数干净。3.3 Bootstrap 与 datetimepicker 的接入前端这边资源里已经内置了 bootstrap.css、animate.css、font-awesome.css 和 bootstrap-datetimepicker.css整体是 Bootstrap 3 时代的组件栈。日期选择控件不直接依赖 Django 的 DateInput而是用 bootstrap-datetimepicker 在前端格式化后再提交接入方式如下!-- templates/registration/book.html 关键片段 -- link relstylesheet href{% static bootstrap-datetimepicker.min.css %} script src{% static bootstrap-datetimepicker.min.js %}/script script $(function () { $(#id_date).datetimepicker({ format: yyyy-mm-dd, minView: month, autoclose: true, startDate: new Date() }); }); /scriptformat 必须与 Django 的 DateField 默认解析格式一致写成 yyyy-mm-dd 后提交的值就是 2025-01-15 这种字符串Django 能直接转成 Python 的 date 对象。minView: month 表示只选日期不选具体时间避免用户把时间也带上导致日期字段校验失败。startDate: new Date() 禁止选过去的日期这里有一个常见坑如果前端控件禁选了过去日期但后端 Form 没有对应校验绕过前端直接 POST 仍然可以挂过去的号建议后端也校验 date date.today()。3.4 表单错误回显clean 方法里 raise 的 ValidationError 不会挂在某个字段下而是进入 non_field_errors模板里如果只遍历 field.errors 就会出现「提示了号源已满但界面上看不到」的诡异现象。正确写法要同时渲染两类错误{% if form.non_field_errors %} div classalert alert-danger {% for e in form.non_field_errors %}{{ e }}{% endfor %} /div {% endif %} {% for field in form %} div classform-group {% if field.errors %}has-error{% endif %} {{ field.label_tag }} {{ field }} {% for e in field.errors %}span classhelp-block{{ e }}/span{% endfor %} /div {% endfor %}has-error 类来自 Bootstrap 3可以让输入框边框变红配合 help-block 的错误文案交互上比较完整。这个模式在资源的前端模板里反复出现抽出来作为公共 include 会更省事直接改一份模板就能统一全部表单的错误样式。4. 后台管理、统计查询与 ORM 性能细节4.1 Admin 定制list_display、过滤与搜索django admin 界面美化是搜索引擎里的高频需求。这个资源没有引入第三方美化包用原生 Admin 调好的效果也够用关键是不要把注册写成裸的 admin.site.register(Appointment)那只能得到一个没有任何操作的列表页。定制度高的写法如下from django.contrib import admin from .models import Appointment, Department, Doctor admin.register(Appointment) class AppointmentAdmin(admin.ModelAdmin): list_display (id, doctor, patient, date, time_slot, status, fee) list_filter (status, date) search_fields (patient__name, doctor__name, patient__phone) date_hierarchy date list_per_page 20search_fields 里用 patient__name 这种双下划线写法做跨表搜索这是 Django ORM 的约定表示沿着外键关系查关联模型的字段。date_hierarchy 会在列表页顶部生成按日期逐级下钻的筛选条对挂号这类强日期属性的数据非常实用。list_per_page 控制分页后台数据量上来之后不设分页会导致整页卡死。如果确实要美化成更现代的侧边栏风格常见做法是引入 django-simpleui 或 django-jazzmin只需要在 INSTALLED_APPS 里加一项Admin 的面貌就变了业务代码一行不用动。这个资源里没有默认带可以自行按版本安装。4.2 挂号量统计与聚合查询挂号日报、科室统计这类需求用 ORM 的 annotate 聚合就能完成不需要写原始 SQL也不会因为数据库切换而失效。按月统计各科室挂号人数和挂号费收入from django.db.models import Count, Sum from .models import Appointment report (Appointment.objects .filter(date__month6, status__in(booked, done)) .values(doctor__department__name) .annotate(totalCount(id), fee_sumSum(fee)) .order_by(-total))values 指定分组字段底层生成 GROUP BYannotate 定义聚合列Count 统计记录数Sum 累加挂号费order_by(-total) 按挂号量倒序。这里有一个精度陷阱Sum(fee) 返回的是 Decimal 对象不能在 Python 端直接和 float 做乘除运算后再比较否则会报 TypeError 或丢失精度统一用 Decimal 处理。另外 date__month6 这种写法在某些数据库上不走索引数据量大时建议改成 date__range(start, end) 的范围查询。4.3 执行查询与删除对象时的几个坑热词里经常有人搜「django执行查询-删除对象」和「django reverse resolve」这两个点在实际开发中都容易出事。queryset.delete() 与 instance.delete() 行为不同批量删除不会触发模型里重写的 delete() 方法和信号Appointment 级联删除时关联的 Payment 会被数据库一并删掉这种删除不可恢复线上操作前务必先在事务里 count 确认影响范围。URL 反查可以用 reverse 和 resolve 互相校验from django.urls import reverse, resolve assert reverse(book) /registration/book/ assert resolve(/registration/book/).view_name bookresolve 是把请求路径解析回视图函数和 URL name测试里常用它断言某个路径确实绑定了预期的视图改路由时能第一时间发现拼写错误。资源里没有写自动化测试这部分建议补上两条断言成本很低但收益很高。4.4 常见的查询性能问题对照现象常见原因处理方式挂号列表页加载慢每行都查一次关联表N1 查询视图里加 select_related(doctor, patient)按日期筛选很慢date 字段无索引建单列索引或在 Meta 里加入索引后台保存报唯一约束错误id_card 重复录入用 get_or_create 或 pre_save 校验前端日期少一天USE_TZTrue 但数据库时区未对齐连接串加时区参数统一用 Asia/ShanghaiN1 是这里最常见的性能问题列表页渲染 50 条挂号记录模板里每访问一次 appointment.doctor.name 就发一条 SQL总共 51 条查询。加 select_related 后变成一条 JOIN50 条变 1 条。资源的小数据量下看不出来但答辩时如果被问到「数据量大了怎么办」能答出这一条很加分。5. 并发挂号与事务边界用 select_for_update 守住最后一号第 2 章留了一个并发隐患两个请求同时提交时count() 都读到 8都小于 daily_limit10结果挂出 11 个号。常规修法是在事务里加行锁让后到的请求等前一个提交后再重新判断。from django.db import transaction from .models import Doctor, Appointment transaction.atomic def book_with_lock(doctor_id, date, patient): doctor Doctor.objects.select_for_update().get(pkdoctor_id) booked (Appointment.objects .filter(doctordoctor, datedate, status__in(booked, done)) .count()) if booked doctor.daily_limit: return False Appointment.objects.create(doctordoctor, patientpatient, datedate, feedoctor.daily_limit) return Trueselect_for_update() 会在数据库层面对命中的行加排他锁MySQL InnoDB 下这段事务提交前其他事务的相同查询会被阻塞。注意这个查询必须在 transaction.atomic 块内执行脱离事务直接调用会抛 TransactionManagementError。锁的粒度是 Doctor 行而不是 Appointment 表并发挂不同医生的号互不阻塞吞吐量不受影响。验证方式有两种。一种是在 MySQL 里开两个终端手动模拟事务观察第二个事务是否等待另一种是用 Django 测试客户端压两个线程python manage.py shellfrom django.test import Client import threading def hit(): c Client() c.post(/registration/book/, {doctor: 1, date: 2025-01-20, time_slot: 上午}) t1 threading.Thread(targethit) t2 threading.Thread(targethit) t1.start(); t2.start(); t1.join(); t2.join()跑完后查挂号记录数如果仍然大于号源上限说明锁没生效优先检查是否用了 MyISAM 表——InnoDB 的行锁在 MyISAM 下会退化成表锁或直接失效。生产部署到云服务器时常见做法是用宝塔面板配 Python 项目管理器把 gunicorn 和 nginx 串起来但堡垒机前的最后一关一定是数据库引擎建表时确认 ENGINEInnoDB这条不满足其他优化都白做。把这段逻辑跑一遍再配合 explain 看执行计划超挂问题基本就堵死了。本文还有配套的精品资源点击获取
返回列表