ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Django医院挂号系统实战:从数据建模到并发防超卖的完整方案

Django医院挂号系统实战:从数据建模到并发防超卖的完整方案 简介这是一份基于Python Django框架开发的医院挂号诊疗系统毕业设计源码案例适合计算机相关专业学生用于毕业设计、课程设计或项目初期演示也适合希望学习Django Web开发的小白进阶。项目已获导师认可并通过答辩评审代码经过测试运行成功功能完整具备实际参考价值。资源包共2000个文件大小仅5.57MB其中以1633个JavaScript文件、249个HTML文件、53个CSS文件等前端资源为主辅以少量Python后端文件以及JSON、XML、TXT、MD等多种格式的配置说明与文档整体体量紧凑便于快速查阅。目前已有54人浏览/学习说明其内容受到初步认可适合直接参考使用。下载后可获得完整项目源码与详细设计文档既能直接用于毕设答辩演示也可在此基础上二次开发扩展挂号、诊疗相关功能在动手实践中提升Django全栈开发能力。1. 一个 Django 毕业设计最容易翻车的地方从来不是 Django拿到“医院挂号诊疗系统”这类题目大多数人的第一反应是先把用户登录、医生列表、挂号按钮做出来跑通页面就以为大功告成。但真正拿去答辩或者演示时被问住的问题往往很一致同一位医生上午的号被两个人同时挂到怎么办患者挂了号又取消号源要不要还回去医生排班和挂号记录之间怎么对账这些问题都不能靠前端弹窗解决必须回到数据库和业务层的设计上。这篇文章从 Django 的实际开发路径出发把一套可以拿去交作业、也可以作为真实小诊所内部工具的挂号系统拆开讲。你会看到从模型设计、号源锁定、视图事务处理到 Admin 后台配置的完整落地方案每一段都有可以直接抄的代码和参数说明。选题本身不追求“系统演示有多炫”而是围绕 Django 开发中最核心的模型关系、事务控制、查询优化和后台定制这几件事把挂号业务彻底跑通。适合两类人看一类是正在做毕业设计、需要完整源码思路和文档支撑的学生另一类是已经写过几个 Django 项目、但还没处理过“高并发预约类业务”的开发者。前者能从里拿到可直接复现的代码骨架后者可以从号源锁定的细节里看到自己和生产级实现的差距。2. 挂号诊疗系统的数据建模从 ER 图到 Django Models 的落地方法2.1 先想清楚业务边界再写 Model做医院挂号系统最忌讳的是上来就建十几个表。以最常见的门诊挂号场景为例真正必需的核心业务对象只有四个科室、医生、排班、号源/挂号记录。患者信息可以并入 Django 自带的 User 模型通过扩展字段保存姓名、身份证号和手机号不需要单独建一张患者表去维护登录关系否则后面做认证要写大量冗余代码。围绕这四个对象关系是这样的科室对医生是一对多一个科室有多个医生每个医生只属于一个科室。医生对排班是一对多一个医生可以有多个时段的出诊安排。排班对号源是一对多一个排班产生多个号比如上午 30 个号。挂号记录关联排班和患者一次挂号对应一个排班下的一个号位。这就是典型的“排班产生号池挂号消耗号池”结构。把号池做成独立的 Model而不是在挂号记录里简单存一个医生 ID是这套系统能否处理“退号、换号、号源统计”的关键。你会发现后面写视图时90% 的查询和更新都在围绕这个号池模型转。2.2 核心 Model 定义与关键字段参数下面这份代码是可以直接放进models.py用的核心结构去掉了教科书里常见的冗余字段保留真正会影响业务逻辑的部分。from django.db import models from django.contrib.auth.models import User from django.core.validators import MinValueValidator, MaxValueValidator class Department(models.Model): 科室 name models.CharField(科室名称, max_length50, uniqueTrue) intro models.TextField(科室简介, blankTrue) def __str__(self): return self.name class Meta: db_table hospital_department verbose_name 科室 verbose_name_plural verbose_name class Doctor(models.Model): 医生与 User 一对一扩展登录账号 user models.OneToOneField(User, on_deletemodels.CASCADE, verbose_name登录账号) department models.ForeignKey(Department, on_deletemodels.PROTECT, verbose_name所属科室) title models.CharField(职称, max_length20, choices( (resident, 住院医师), (attending, 主治医师), (chief, 主任医师) ), defaultattending) phone models.CharField(联系电话, max_length11, blankTrue) def __str__(self): return f{self.department.name}-{self.title}-{self.user.last_name or self.user.username} class Meta: db_table hospital_doctor verbose_name 医生 verbose_name_plural verbose_name class Schedule(models.Model): 医生排班生成号池的依据 doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE, verbose_name医生) date models.DateField(出诊日期) period models.CharField(时段, max_length10, choices( (am, 上午), (pm, 下午) )) total_slots models.PositiveIntegerField(号源总数, default30) start_time models.TimeField(开始时间, default08:00) end_time models.TimeField(结束时间, default12:00) class Meta: db_table hospital_schedule verbose_name 出诊排班 verbose_name_plural verbose_name constraints [ models.UniqueConstraint( fields[doctor, date, period], nameuniq_doctor_date_period ) ] property def remaining(self): 实时剩余号属性写法方便模板直接调用 return self.total_slots - self.registrations.filter(statuspaid).count() class Registration(models.Model): 挂号记录每次挂号落一条同时消耗一个号位 patient models.ForeignKey(User, on_deletemodels.CASCADE, related_nameregistrations, verbose_name患者) schedule models.ForeignKey(Schedule, on_deletemodels.CASCADE, related_nameregistrations, verbose_name排班) status models.CharField(状态, max_length10, choices( (pending, 待支付), (paid, 已挂号), (cancelled, 已取消) ), defaultpending) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table hospital_registration verbose_name 挂号记录 verbose_name_plural verbose_name ordering [-created_at]2.2.1 字段和约束的关键参数说明上述设计中几个参数不是随手写的每一处都在解决一个具体问题OneToOneField(User)医生和 Django 自带用户模型一对一绑定这样医生登录系统用的是同一套认证不用单独写医生登录页。on_deletemodels.CASCADE保证删除用户时医生资料一起清理不会产生脏数据。ForeignKey(Department, on_deletemodels.PROTECT)科室被医生引用后不可删除。PROTECT 会在数据库层面拦截删除操作避免出现“科室没了但医生还挂着该科室 ID”的孤儿记录。UniqueConstraint(fields[doctor, date, period])这是排班模型最重要的约束。它保证一个医生同一天同一时段只能有一条排班记录从数据库层面杜绝前端重复提交生成的重复排班。没有这个约束后续的号源统计会立刻产生偏差。related_nameregistrations反向查询时不再需要写registration_set直接用schedule.registrations.filter(...)或者patient.registrations.all()代码可读性高很多模板里调用也更顺手。PositiveIntegerField号源总数不可能为负数用这个字段类型比普通 IntergerField 多一层基础校验。2.3 患者模型不要另起炉灶直接扩展 User很多教材会单独设计 Patient 表但实际开发中这样做等于把用户认证拆成两套体系登录、密码重置、会话管理全得自己写。推荐的替代方案是创建 User 的 Profile 扩展模型通过OneToOneField绑定from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class PatientProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) id_card models.CharField(身份证号, max_length18, uniqueTrue) phone models.CharField(手机号, max_length11) address models.CharField(住址, max_length200, blankTrue) def __str__(self): return f{self.user.last_name or self.user.username} 的就诊档案 class Meta: db_table hospital_patient_profile verbose_name 患者档案 verbose_name_plural verbose_name receiver(post_save, senderUser) def create_patient_profile(sender, instance, created, **kwargs): 注册用户后自动创建空档案省去手动同步的步骤 if created: PatientProfile.objects.get_or_create(userinstance)逻辑说明post_save信号监听 User 的创建事件一旦有新用户注册自动生成一条关联 profile。这样在注册视图里不需要显式调用 profile 的 save 方法也不用担心因为表单漏填字段导致关联失败。参数uniqueTrue对身份证号做了唯一约束防止同一身份信息开多个账号。真实系统中患者和医生都可以挂在同一个 User 表下通过 profile 里加一个user_type字段区分角色。但在毕业设计维度上一个医生对应一个可登录用户一个普通前台患者对应一个可登录用户已经足够覆盖演示和答辩场景。答辩时能解释清楚“为什么患者不单独建表”本身就是加分项。3. 挂号及退号的核心视图用事务和 select_for_update 保障号源不超卖3.1 挂号业务中的并发隐患到底在哪普通的增删改查视图学会了不等于挂号功能能上线。挂号业务最微妙的地方在于“号源总量有限多人可能同时抢同一个号位”。假设某个排班剩余 1 个号两个患者同时点“挂号”按常规写法两个请求都会先查remaining 1然后各自生成一条挂号记录最终结果就是 1 个号位被卖出去 2 次。这个问题属于典型的资源竞争场景。Django 解决它不需要引入 Redis 锁或者消息队列靠数据库自身的行锁 事务就能处理。做法是在查询可用排班时使用select_for_update()让数据库对命中的排班行加排他锁直到事务提交或回滚才释放。第二个请求到达时会阻塞在锁上等第一个事务结束再继续此时它读到的剩余号数已经是更新后的数字。3.2 最小可运行的挂号视图代码下面是一个基于Transaction和select_for_update的挂号接口实现。它在逻辑上涵盖“锁排班 - 校验剩余号 - 创建挂号记录 - 返回结果”四个步骤可以平滑适配页面表单提交和 JSON 接口两种调用方式。from django.db import transaction from django.shortcuts import get_object_or_404 from django.http import JsonResponse from django.views.decorators.http import require_POST from django.contrib.auth.decorators import login_required from .models import Schedule, Registration login_required require_POST def create_registration(request): 患者挂号接口 请求参数: schedule_id 返回: JSON, 含挂号状态与提示信息 schedule_id request.POST.get(schedule_id) if not schedule_id: return JsonResponse({code: 400, msg: 缺少排班编号}) # 开启事务保证锁和后续写操作在同一个原子块内 try: with transaction.atomic(): # 锁定这一条排班记录直到事务结束 schedule ( Schedule.objects .select_for_update() .filter(idschedule_id, date__gtedatetime.date.today()) .select_related(doctor, doctor__department) .first() ) if schedule is None: return JsonResponse({code: 404, msg: 排班不存在或已过期}) # 统计当前已占用号位只算有效挂号 used_slots ( Registration.objects .filter(scheduleschedule, statuspaid) .count() ) if used_slots schedule.total_slots: return JsonResponse({code: 410, msg: 号源已满请选择其他时段}) # 检查是否已挂过同一排班避免重复挂号 if Registration.objects.filter( patientrequest.user, scheduleschedule, statuspaid ).exists(): return JsonResponse({code: 409, msg: 您已挂过该医生的号请勿重复操作}) reg Registration.objects.create( patientrequest.user, scheduleschedule, statuspaid ) # 事务结束锁释放 return JsonResponse({ code: 200, msg: 挂号成功, data: { registration_id: reg.id, doctor_name: schedule.doctor.user.last_name or schedule.doctor.user.username, department: schedule.doctor.department.name, date: schedule.date.strftime(%Y-%m-%d), period: schedule.get_period_display(), } }) except Exception as e: # 生产环境建议记录日志这里仅返回错误信息 return JsonResponse({code: 500, msg: f挂号失败: {str(e)}})3.2.1 这段代码的关键参数与逻辑点select_for_update()是这段代码的核心命脉。它只在事务内生效所以外层必须包裹transaction.atomic()。如果你把锁写在事务外面Django 会直接抛TransactionManagementError。另一个容易被忽略的点是select_for_update必须配合filter使用才有意义它锁定的不是“查询条件”而是查询结果对应的数据库行。used_slots的统计逻辑选择对statuspaid计数而不是用remaining total_slots - used原因在于退号操作只需要把状态改为cancelled不需要物理删除记录。这样设计后所有历史挂号痕迹都保留着对后续对账和审计非常有帮助。date__gtedatetime.date.today()是第二个重要过滤条件。它防止患者传入一个已经过期的排班 ID 去占号。日期过滤写在filter里而非 Python 层判断是为了避免“当前排班查到了但锁定了无用记录”的情况减少无畏的行锁开销。3.3 退号视图与状态机流转退号逻辑相对简单但容易踩一个坑把status改回pending而不是cancelled导致号位统计异常。正确做法是定义一个清晰的“状态机”pending待支付 -paid已挂号 -cancelled已取消只有paid可以进入cancelled只有pending可以进入paid。下面的退号代码同时做了属主和状态的两重校验from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_POST from django.contrib.auth.decorators import login_required from .models import Registration login_required require_POST def cancel_registration(request): 患者退号 请求参数: registration_id 校验挂号和状态后改为 cancelled reg_id request.POST.get(registration_id) if not reg_id: return JsonResponse({code: 400, msg: 缺少挂号记录编号}) try: with transaction.atomic(): # 锁挂号记录同时锁关联排班防止退号瞬间有人挂号造成错误计数 reg ( Registration.objects .select_for_update() .select_related(schedule) .get(idreg_id, patientrequest.user) ) if reg.status ! paid: return JsonResponse({code: 409, msg: 当前状态不能退号}) reg.status cancelled reg.save(update_fields[status]) return JsonResponse({code: 200, msg: 退号成功号源已释放}) except Registration.DoesNotExist: return JsonResponse({code: 404, msg: 挂号记录不存在或无权操作})有人说退号不需要select_for_update因为只是改个状态。但这里锁挂号记录的同时通过select_related把排班一并锁进事务为的是防止极端情况患者在医生下班前最后几分钟退号另一个人同时挂这个号如果这两步操作之间有间隙数据库层面的计数会出现瞬时不一致。在生产级代码里宁可多锁一行也不能留下竞态窗口。3.4 挂号失败时优先检查哪些东西代码写完不代表问题解决。实际运行中最常见的三类报错有清晰的排查路径TransactionManagementError: select_for_update cannot be used outside of a transaction这说明select_for_update写在了atomic块之外或者查询根本没进事务。把包含锁查询的代码整体挪进with transaction.atomic():内即可。号源没有减少但挂号记录创建成功几乎可以断定是status的默认值写错或者统计口径用了filter(statuspending)。统计号源的逻辑必须和挂号创建逻辑使用同一个状态常量。前端重复点击导致同一患者挂到两个号虽然视图里有重复挂号校验但从工程角度讲应该在按钮上做disabled状态控制同时在视图层用唯一约束如UniqueConstraint(fields[patient, schedule], conditionQ(statuspaid))做最后的兜底。4. 医生排班管理与门诊查询的实用查询写法4.1 排班列表页的查询优化策略排班列表是系统里访问频率最高的页面。患者进入挂号页先看“今天有哪些医生出诊”医生登录后也要看“我这个月排了哪些班”。如果直接在模板里循环schedule.registrations.count或调用schedule.remaining属性会产生 N1 次数据库查询每显示一条排班就多查一次号源统计。当排班记录达到几十条时页面加载时间会明显变慢。解决办法是用 Django ORM 的annotate在一次查询中完成统计。下面给出一个门诊排班查询视图的示例from django.db.models import Count, Q from django.utils import timezone from django.views.generic import ListView from .models import Schedule class ScheduleListView(ListView): model Schedule template_name hospital/schedule_list.html context_object_name schedules paginate_by 10 def get_queryset(self): # 只显示今天及之后的排班并按日期和时间排序 return ( Schedule.objects .filter(date__gtetimezone.localdate()) .select_related(doctor__user, doctor__department) .annotate( used_slotsCount( registrations, filterQ(registrations__statuspaid) ) ) .order_by(date, period) )查询逻辑说明annotate配合Count(registrations, filter...)是 Django 2.0 之后才支持的条件聚合写法内部会被翻译成 SQL 里的SUM(CASE WHEN ... THEN 1 ELSE 0 END)。它的好处是不产生额外查询全部统计在一条 SQL 内完成。模板里使用schedule.used_slots替代调用schedule.remaining因为后者每次访问都会单独查一次数据库。这是“能放进 QuerySet 的就不要放进 Model 属性”的典型例子。真正需要警惕的是不要在这个视图之外再调用schedule.remaining否则会破坏批量查询的优化效果。4.2 科室筛选与日期筛选的兼容处理列表页一般会提供“按科室筛选”“按日期筛选”两个入口。如果只有一个筛选条件生效代码很好写但两个条件同时存在时必须注意参数是否带默认值。下面是一段兼容多条件的get_queryset写法import datetime from django.db.models import Count, Q def get_filtered_schedules(request): queryset ( Schedule.objects .filter(date__gtedatetime.date.today()) .select_related(doctor__user, doctor__department) ) # 科室筛选没有传入时不做过滤 dept_id request.GET.get(dept_id) if dept_id: queryset queryset.filter(doctor__department_iddept_id) # 日期筛选支持 ?date2025-06-01 格式非法格式忽略 date_str request.GET.get(date, ).strip() if date_str: try: target datetime.date.fromisoformat(date_str) queryset queryset.filter(datetarget) except ValueError: pass # 日期格式不合法时忽略该筛选条件 # 按已挂号数排序号多的优先显示方便患者找热门医生 queryset queryset.annotate( used_slotsCount(registrations, filterQ(registrations__statuspaid)) ).order_by(-used_slots, date) return queryset这段代码在参数容错上做了两个细节处理。if dept_id判断过滤掉空字符串和None避免dept_id这种空参数导致查询异常日期转换使用fromisoformat遇到2025/06/01这类非标准格式时主动忽略而不是报 500 错误。排序用-used_slots把热门号源排前面既方便患者选择也减少了后半夜蹲守捡漏的体验压力。4.3 医生端查询我今天的挂号患者列表医生登录后最关心的是“今天哪些人挂了我的号”。这个查询相对简单但涉及跨表关联检索正确写法是直接用select_related连表查询避免在模板里通过reg.patient.profile.phone产生 N1 次查询。from django.contrib.auth.decorators import login_required from django.shortcuts import render from django.utils import timezone from .models import Registration, Doctor login_required def my_patients(request): 医生查看自己当日挂号患者 doctor Doctor.objects.filter(userrequest.user).first() if not doctor: return render(request, hospital/error.html, {msg: 当前账号不是医生账号}) today timezone.localdate() regs ( Registration.objects .filter(schedule__doctordoctor, schedule__datetoday, statuspaid) .select_related(patient, patient__profile, schedule) .order_by(schedule__period, created_at) ) context { doctor: doctor, regs: regs, reg_count: regs.count(), } return render(request, hospital/doctor_patients.html, context)filter(schedule__doctordoctor)是跨模型关联过滤的标准写法翻译成 SQL 就是JOIN hospital_schedule ON ... WHERE schedule.doctor_id ?。select_related在这里一口气带出patient、patient.profile和schedule三张关联表的数据后续模板中无论是显示患者姓名还是手机号都不会再产生额外 SQL。注意patient__profile的双下划线写法它表示嵌套关联的层级。这里有个容易忽略的细节regs.count()不会因为前面调用了select_related而受影响它仍然是独立的COUNT(*)查询。如果你发现 count 和列表数据量不一致优先检查是不是filter条件里有status遗漏。5. Django Admin 后台改造让“资料齐全”体现在可维护性上5.1 为什么毕业设计里的 Admin 后台不能直接用默认配置Django Admin 是这套系统最容易被低估的部分。默认生成的注册页面确实能增删改查但直接拿去做项目演示会有三个尴尬列表显示的全是Schedule object (1)这类对象名医生排班页里选医生时看到的是名字而不是“科室-职称-姓名”的可读格式更重要的是没有做数据过滤和搜索记录多了之后根本无法管理。“资料齐全”的源码工程通常会提供一套经过深度定制的 Admin 页面这样答辩时可以直接演示数据录入和权限分流比临时写前端表单省时间很多。下面给出一个实用的 Admin 配置覆盖列表、筛选、搜索、行内编辑四个维度的优化。from django.contrib import admin from .models import Department, Doctor, Schedule, Registration admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): list_display (id, name, intro) search_fields (name,) class RegistrationInline(admin.TabularInline): 排班详情页内联展示挂号记录 model Registration extra 0 fields (patient, status, created_at) readonly_fields (patient, status, created_at) can_delete False def has_add_permission(self, request, objNone): # 不允许在排班后台直接手工添加挂号避免绕过号源校验 return False admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): list_display (id, doctor, date, period, start_time, end_time, total_slots, used_count) list_filter (date, period, doctor__department) search_fields (doctor__user__username, doctor__user__last_name) date_hierarchy date autocomplete_fields (doctor,) inlines [RegistrationInline] def get_queryset(self, request): # 聚合统计已挂号数避免列表页 N1 查询 from django.db.models import Count, Q return ( super().get_queryset(request) .select_related(doctor__user, doctor__department) .annotate(used_countCount(registrations, filterQ(registrations__statuspaid))) ) def used_count(self, obj): return obj.used_count used_count.short_description 已挂号数 admin.register(Doctor) class DoctorAdmin(admin.ModelAdmin): list_display (id, name_display, department, title, phone) list_filter (department, title) search_fields (user__username, user__last_name, phone) def name_display(self, obj): return obj.user.last_name or obj.user.username name_display.short_description 姓名 admin.register(Registration) class RegistrationAdmin(admin.ModelAdmin): list_display (id, patient, schedule, status, created_at) list_filter (status, schedule__date, schedule__doctor__department) search_fields (patient__username, patient__profile__phone, schedule__doctor__user__last_name) date_hierarchy created_at def get_readonly_fields(self, request, objNone): # 已取消的挂号记录不允许再编辑 if obj and obj.status cancelled: return self.readonly_fields (status,) return self.readonly_fields5.1.1 Admin 配置里最值得 Debug 的几个点list_display中used_count是函数而不是模型字段Django Admin 无法自动排序。如果希望在列表页点“已挂号数”列头排序必须给函数加admin.display(orderingused_count)装饰器否则点击排序会报错。没有加这个装饰器时Django 会用used_count本身去数据库里查找列名结果必然FieldError。search_fields中使用doctor__user__username这种跨表路径是合法的但要注意search_fields的跨表搜索性能并不高因为 Django 会为每个路径生成单独的LIKE条件。对于毕业设计量级的数据几百条排班没有压力不必为此引入搜索引擎。autocomplete_fields依赖对应 ModelAdmin 里注册了search_fields如果你在DoctorAdmin里没有写search_fieldsScheduleAdmin的autocomplete_fields会在请求时抛异常。这个隐藏依赖关系非常容易踩到。RegistrationInline重写has_add_permission返回 False 是一个工程上的好习惯挂号必须走业务视图才能触发号源锁定逻辑如果允许从 Admin 直接添加挂号记录号源校验就被绕过了。同理can_deleteFalse防止误删操作因为退号应该走业务接口将状态改为cancelled。5.2 Admin 数据导出与备份在毕业设计文档中通常需要附上一两句话说明“系统数据如何备份”。Admin 后台本身不直接支持导出 Excel但可以基于 Django 的QuerySet写一个简单的 CSV 导出视图作为附加功能扩展到后台中import csv from django.http import HttpResponse from django.contrib.admin.views.decorators import staff_member_required from .models import Registration staff_member_required def export_registrations_csv(request): 导出挂号记录为 CSV用于离线对账 response HttpResponse(content_typetext/csv) response[Content-Disposition] attachment; filenameregistrations.csv writer csv.writer(response) writer.writerow([挂号单号, 患者, 科室, 医生, 日期, 时段, 状态, 创建时间]) regs ( Registration.objects .select_related(patient, schedule__doctor__department, schedule__doctor__user) .order_by(-created_at) ) for reg in regs: writer.writerow([ reg.id, reg.patient.last_name or reg.patient.username, reg.schedule.doctor.department.name, reg.schedule.doctor.user.last_name or reg.schedule.doctor.user.username, reg.schedule.date, reg.schedule.get_period_display(), reg.get_status_display(), reg.created_at.strftime(%Y-%m-%d %H:%M:%S), ]) return response这个导出视图有两个参数值得解释Content-Disposition头里的attachment告诉浏览器该响应是一个下载文件filename需要带.csv后缀否则 Windows 用户打开时会乱码。get_status_display和get_period_display是 Django 模型内置方法返回 Choices 字段中手动指定的中文展示值不需要自己维护一个状态映射字典。5.3 给排班管理增加批量生成功能手工逐条创建排班非常痛苦。通过action可以给 Admin 增加批量操作快速为一个指定日期生成所有医生的排班from django.contrib import admin, messages from django.shortcuts import redirect from django.urls import path from django import forms from .models import Schedule, Doctor from datetime import date, timedelta class BatchGenerateForm(forms.Form): start_date forms.DateField(label开始日期, widgetforms.DateInput(attrs{type: date})) days forms.IntegerField(label生成天数, min_value1, max_value30, initial7) admin.register(Schedule) class ScheduleAdminWithAction(admin.ModelAdmin): # 省略前面的字段配置... actions [batch_generate] def get_urls(self): urls super().get_urls() custom_urls [ path(batch-generate/, self.admin_site.admin_view(self.batch_generate_view), nameschedule-batch-generate), ] return custom_urls urls def batch_generate(self, request, queryset): return redirect(admin:schedule-batch-generate) batch_generate.short_description 批量生成排班 def batch_generate_view(self, request): if request.method POST: form BatchGenerateForm(request.POST) if form.is_valid(): start form.cleaned_data[start_date] days form.cleaned_data[days] created 0 skipped 0 for offset in range(days): day start timedelta(daysoffset) # 跳过周末演示科室正常门诊 if day.weekday() 5: continue for doctor in Doctor.objects.all(): _, was_created Schedule.objects.get_or_create( doctordoctor, dateday, periodam, defaults{total_slots: 30, start_time: 08:00, end_time: 12:00} ) if was_created: created 1 else: skipped 1 self.message_user(request, f排班生成完成新增 {created} 条跳过重复 {skipped} 条。) return redirect(admin:index) else: form BatchGenerateForm() context self.admin_site.each_context(request) context[form] form context[title] 批量生成排班 return render(request, admin/batch_generate.html, context)get_or_create是批量生成的最佳选择它以doctor date period作为匹配键存在则跳过不存在则创建。底层依赖的正是模型层定义的UniqueConstraint如果去掉这个约束get_or_create在高并发场景下可能仍然产生重复数据但在此处串行执行没有问题。日期筛选使用weekday() 5自动跳过周末这个规则可以根据医院实际门诊安排调整。6. 挂对账与门诊数据核验的终极技巧系统从“能跑”到“敢演示”最后一公里是对账。排班数据、挂号数据、退号数据之间是否一致直接决定系统能不能应付答辩老师“数据对不上怎么办”的追问。这里分享一个实战中打磨出来的门诊数据核验方法。约定对账口径某一天的医生排班号源总数减去该日已取消的挂号数必须等于当日有效挂号数。写成公式即sum(total_slots for schedule in day) - cancelled_count paid_count利用 Django ORM 的聚合查询可以写一个独立的管理命令来完成每日对账放到management/commands/audit_daily.py中from django.core.management.base import BaseCommand from django.db.models import Count, Q, Sum from django.utils import timezone from datetime import timedelta from hospital.models import Schedule, Registration class Command(BaseCommand): help 核对指定日期的号源、挂号与退号数据 def add_arguments(self, parser): parser.add_argument(--date, typestr, help日期格式 YYYY-MM-DD默认今天) def handle(self, *args, **options): date_str options.get(date) target timezone.localdate() if date_str: target timezone.datetime.fromisoformat(date_str).date() schedules ( Schedule.objects .filter(datetarget) .annotate( paid_countCount(registrations, filterQ(registrations__statuspaid)), cancelled_countCount(registrations, filterQ(registrations__statuscancelled)), ) ) total_slots schedules.aggregate(totalSum(total_slots))[total] or 0 paid_total sum(s.paid_count for s in schedules) cancelled_total sum(s.cancelled_count for s in schedules) expected total_slots - cancelled_total actual paid_total self.stdout.write(f日期: {target}) self.stdout.write(f排班总数: {schedules.count()}号源总量: {total_slots}) self.stdout.write(f有效挂号数: {paid_total}退号数: {cancelled_total}) self.stdout.write(f预期剩余号: {expected}实际剩余号: {total_slots - actual}) if expected actual: self.stdout.write(self.style.SUCCESS(对账通过数据一致)) else: self.stdout.write(self.style.ERROR(f对账失败差值为 {expected - actual}请检查挂号记录))执行方式很简单python manage.py audit_daily --date 2025-06-01没有传--date参数时默认对账当天。这个命令的原理是把对账规则固化到应用层每一天的数据跑一遍即完成校验。如果差值为负数说明存在没有排班记录的挂号数据需要检查是否有人绕过了排班校验直接写入数据如果差值为正数说明有排班号源被“吞掉”优先排查是否有人在关闭支付的情况下仍创建了paid状态的记录。对这个对账逻辑做一次增强扩展当一个排班的总号数等于已取消数说明该排班的所有号全部被退完此时可以把该排班标记为“无效”避免后续患者再挂。这是一个值得写进文档的边界场景处理。接口上加一个前置判断即可逻辑简单但对系统完整性而言意义很大。最后一条收尾技巧在任何涉及数量统计的界面都建议显示统计时间戳比如“数据截至 2025-06-01 14:30:00”。因为挂号数据实时变动患者看到的是一个带时间标记的静态快照可以避免“数据过期”的质疑。实现只要在模板 context 中传入timezone.now()并在页脚渲染即可一行代码的成本却能让整个系统在演示时显得严谨许多。本文还有配套的精品资源点击获取
返回列表