☰
Python+Django+MySQL医院挂号系统源码:毕设完整业务闭环与并发避坑指南
2026/10/2 14:13:21 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的医院挂号诊疗管理系统完整项目源码,采用Python+Django+MySql技术栈开发,适合用作毕业设计、期末大作业或课程设计参考。项目代码附带详细注释,新手也能快速理解整体架构与业务逻辑,下载后简单部署即可运行使用。资源包共包含2000个文件,以1633个JavaScript脚本、249个HTML页面、53个CSS样式表及33个HTM页面为主体,另有JSON配置、Python后端文件、XML与Markdown说明等,压缩包大小约6.25MB,前端页面与后端逻辑分层清晰。目前已有331人学习下载,属于经过实际答辩检验的高分项目,导师认可度较高。读者可获得完整的挂号、诊疗、用户管理等模块实现方案,借助注释快速掌握Django项目结构与数据库设计思路,并在此基础上进行二次开发或功能扩展,为毕业设计提供可靠参考。

1. 从一份能跑通的挂号系统源码说起:它到底解决了什么问题

很多计算机专业的同学在毕设选题阶段会卡在同一个地方:想做业务系统,又怕功能太单薄被答辩老师追问;想堆技术栈,又担心自己搭不起来环境。这份基于 Python + Django + MySQL 的医院挂号诊疗管理系统,恰好落在了一个比较舒服的区间——业务闭环完整,技术栈主流,代码结构清晰,数据库脚本齐全。它覆盖了患者注册登录、科室与医生管理、排班放号、在线挂号、就诊记录、处方与缴费这条主线,后台还有管理员对医生、科室、药品的维护入口。换句话说,这不是一个只有增删改查的玩具项目,而是一个能讲清楚「谁在什么场景下做了什么操作」的完整业务系统。适合正在做毕设的本科生、需要快速搭一个医疗类管理系统原型的开发者,以及想拿一个真实 Django 项目练手的人。下面我会按「先跑起来、再看结构、最后避坑」的顺序,把这份资源拆开讲透。

2. 环境搭建与项目启动:从 Python 到 MySQL 的完整链路

2.1 技术栈选型为什么是 Django 而不是 Flask

拿到一份源码,第一件事不是急着pip install,而是先判断它的技术选型是否合理,这决定了你后面改代码时会不会被框架本身绊住。这份项目用的是 Django,而不是 Flask 或 FastAPI,原因很实际:医院挂号系统天然包含大量后台管理需求——医生信息维护、科室分类、药品库存、排班表,这些用 Django 自带的 admin 后台几乎可以零成本搭出来。如果用 Flask,你得自己写一套 CRUD 页面和权限控制,工作量翻倍不说,还容易在权限校验上出漏洞。

Django 的 ORM 也是关键。挂号系统里「一个患者对应多条挂号记录,一条挂号记录对应一个医生和一个排班」这种多表关联非常密集,用 Django 的ForeignKey和related_name能直接把关系映射到代码层,查询时用select_related就能减少数据库往返。MySQL 作为关系型数据库,配合 Django 的 migration 机制,建表和改字段都不需要手写 SQL。这套组合在毕设场景下属于「稳」的选择,答辩时也能讲清楚为什么不用 NoSQL——医疗数据强一致性要求高,关系模型更合适。

2.2 环境准备:Python、MySQL 与依赖安装

在动手之前,先把三个东西装好:Python 3.8 以上、MySQL 5.7 或 8.0、以及一个趁手的编辑器(VS Code 或 PyCharm 都行)。Python 安装时记得勾选「Add to PATH」,否则后面命令行里python会提示找不到命令。MySQL 安装过程中会让你设置 root 密码,这个密码后面要写进 Django 的配置文件,别设得太随意。

# 查看 Python 版本,确认在 3.8 以上 python --version # 查看 MySQL 是否安装成功并启动 mysql --version # Windows 下检查服务状态 net start | findstr MySQL # macOS/Linux 下检查 systemctl status mysql

确认环境没问题后,进入项目根目录安装依赖。这类毕设项目通常会带一个requirements.txt,里面列了 Django、mysqlclient、Pillow 等包。mysqlclient是 Python 连接 MySQL 的驱动,安装时在 Windows 上偶尔会报编译错误,遇到的话换成pymysql也能用,但需要在__init__.py里加一行pymysql.install_as_MySQLdb()。

# 创建虚拟环境,避免污染全局包 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装项目依赖 pip install -r requirements.txt # 如果 mysqlclient 安装失败,改用 pymysql pip install pymysql

虚拟环境这一步很多人会跳过,觉得麻烦。但毕设项目往往依赖版本比较旧,比如 Django 3.2 和 Django 4.0 在某些 API 上不兼容,如果你全局环境里已经装了新版本,直接跑就会报一堆ImportError。用虚拟环境隔离,是省后悔药的做法。

2.3 数据库配置与初始化:settings.py 里改什么

Django 的数据库配置集中在settings.py的DATABASES字段。这份项目用的是 MySQL,你需要把NAME、USER、PASSWORD、HOST、PORT改成自己本地的实际值。NAME是数据库名,建议提前在 MySQL 里建好,字符集用utf8mb4,否则存中文姓名或地址时会出现乱码。

# settings.py 数据库配置片段 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'hospital_registration', # 数据库名,需提前创建 'USER': 'root', # MySQL 用户名 'PASSWORD': 'your_password', # 改成你自己的密码 'HOST': '127.0.0.1', # 本地用 127.0.0.1,不要用 localhost 'PORT': '3306', # MySQL 默认端口 'OPTIONS': { 'charset': 'utf8mb4', }, } }

这里有个细节值得说:HOST写127.0.0.1而不是localhost。在部分系统上,localhost会走 Unix socket 连接,而 MySQL 的 socket 路径配置不一致时会报Error 2002 (HY000): Can't connect to local MySQL server through socket。用127.0.0.1强制走 TCP,能绕开这个玄学问题。

数据库建好后,执行迁移和初始化:

# 创建数据库(在 MySQL 命令行里执行) CREATE DATABASE hospital_registration CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 生成迁移文件并建表 python manage.py makemigrations python manage.py migrate # 导入项目自带的初始数据(如果有 .sql 文件) mysql -u root -p hospital_registration < init_data.sql # 创建后台管理员账号 python manage.py createsuperuser # 启动开发服务器 python manage.py runserver

makemigrations是根据模型文件生成建表语句,migrate才是真正在数据库里执行。如果项目自带init_data.sql,里面通常包含科室、医生、药品的基础数据,导入后前台才有内容可展示。createsuperuser创建的账号用来登录/admin后台,管理医生和排班。

2.4 项目目录结构与核心模块定位

启动成功后,别急着点页面,先花十分钟把目录结构摸清楚。Django 项目的组织方式比较固定,但每个毕设的 app 划分不一样。常见做法是按业务拆成user(患者)、doctor(医生)、registration(挂号)、admin_app(后台管理)几个 app。每个 app 下有models.py(数据模型)、views.py(业务逻辑)、urls.py(路由)、templates(页面模板)。

目录/文件作用改动频率
manage.py命令行入口,跑服务、迁移都靠它不改
settings.py全局配置,数据库、静态文件、中间件改数据库和静态路径
urls.py(项目级)总路由,分发到各 app加新页面时改
models.py定义表结构和关联关系改字段时改
views.py处理请求、查数据、返回页面改业务逻辑时改
templates/HTML 模板,Django 模板语法改页面时改
static/CSS、JS、图片基本不改
media/用户上传的文件,如医生头像自动生成

搞清楚哪个功能在哪个 app 里,后面改需求时才能快速定位。比如要加一个「患者取消挂号」功能,你需要在registration/views.py里加视图函数,在registration/urls.py里加路由,在模板里加按钮。这三步缺一不可,漏了路由就会 404。

3. 核心业务逻辑拆解:挂号、排班与权限控制怎么实现

3.1 数据模型设计:患者、医生、排班、挂号四张表的关联

挂号系统的核心在于表关系设计。这份项目里,最关键的几张表是:患者表、医生表、排班表、挂号记录表。它们之间的关系可以用一句话概括:一个医生有多个排班,一个排班对应多个挂号名额,一个患者可以有多条挂号记录。

# models.py 核心模型示例(简化版) from django.db import models from django.contrib.auth.models import User class Department(models.Model): name = models.CharField(max_length=50, verbose_name='科室名称') description = models.TextField(blank=True, verbose_name='科室描述') def __str__(self): return self.name class Doctor(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) department = models.ForeignKey(Department, on_delete=models.CASCADE, verbose_name='所属科室') title = models.CharField(max_length=20, verbose_name='职称') specialty = models.CharField(max_length=100, verbose_name='擅长领域') photo = models.ImageField(upload_to='doctor_photos/', blank=True) def __str__(self): return f"{self.user.last_name}{self.title}" class Schedule(models.Model): doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE, verbose_name='医生') date = models.DateField(verbose_name='出诊日期') time_slot = models.CharField(max_length=20, choices=[ ('morning', '上午'), ('afternoon', '下午'), ], verbose_name='时段') total_slots = models.IntegerField(default=20, verbose_name='总号源') remaining_slots = models.IntegerField(default=20, verbose_name='剩余号源') class Meta: unique_together = ('doctor', 'date', 'time_slot') # 防止重复排班 class Registration(models.Model): patient = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='患者') schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, verbose_name='排班') status = models.CharField(max_length=10, choices=[ ('pending', '待就诊'), ('completed', '已完成'), ('cancelled', '已取消'), ], default='pending', verbose_name='状态') created_at = models.DateTimeField(auto_now_add=True, verbose_name='挂号时间') class Meta: unique_together = ('patient', 'schedule') # 同一患者同一排班不能重复挂号

这段代码里有几个设计点值得注意。Schedule表里的unique_together约束防止同一个医生在同一天同一时段被重复排班,这是业务上的硬性要求。Registration表里的unique_together防止患者重复挂号,但实际业务中患者可能取消后重新挂号,所以更严谨的做法是用status字段配合查询逻辑来判断,而不是单纯靠数据库约束。remaining_slots字段是冗余设计,每次挂号成功后减一,这样查询余号时不用实时统计挂号记录数,性能更好,但代价是需要在取消挂号时加回去,逻辑要写对。

3.2 挂号流程的视图函数:从选科室到确认挂号

挂号流程通常分三步:选科室 → 选医生和排班 → 确认挂号。对应的视图函数要处理 GET 请求展示页面和 POST 请求提交数据。下面是一个典型的挂号视图:

# views.py 挂号核心逻辑 from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.db import transaction from .models import Department, Doctor, Schedule, Registration @login_required def department_list(request): """展示所有科室""" departments = Department.objects.all() return render(request, 'registration/department_list.html', {'departments': departments}) @login_required def doctor_list(request, department_id): """展示某科室下的医生及其排班""" department = get_object_or_404(Department, id=department_id) doctors = Doctor.objects.filter(department=department).select_related('user') # 只展示今天及以后的排班 schedules = Schedule.objects.filter( doctor__in=doctors, date__gte=timezone.now().date(), remaining_slots__gt=0 ).select_related('doctor__user') return render(request, 'registration/doctor_list.html', { 'department': department, 'doctors': doctors, 'schedules': schedules, }) @login_required @transaction.atomic def register(request, schedule_id): """确认挂号,使用事务保证号源扣减和挂号记录同时成功""" schedule = get_object_or_404(Schedule, id=schedule_id) # 检查是否重复挂号 if Registration.objects.filter(patient=request.user, schedule=schedule).exists(): return render(request, 'registration/error.html', {'msg': '您已挂过该号,请勿重复操作'}) # 检查余号 if schedule.remaining_slots <= 0: return render(request, 'registration/error.html', {'msg': '该时段号源已满'}) # 扣减号源并创建挂号记录 schedule.remaining_slots -= 1 schedule.save() Registration.objects.create(patient=request.user, schedule=schedule) return redirect('registration:my_registrations')

@transaction.atomic装饰器是这段代码的关键。挂号涉及两个写操作:扣减remaining_slots和创建Registration记录。如果扣减成功但创建记录失败,号源就白白少了一个,患者却没挂上号。用事务包起来,两个操作要么都成功,要么都回滚。select_related用于减少查询次数,Doctor.objects.filter(...).select_related('user')会在一次查询里把关联的 User 数据也取出来,避免在模板里循环访问doctor.user.last_name时反复查库。

3.3 权限控制:患者、医生、管理员三种角色的隔离

系统里有三类人:患者、医生、管理员。Django 自带的User模型和is_staff、is_superuser字段可以区分管理员,但医生和患者需要额外标记。常见做法是给User加一个role字段,或者用Doctor模型的存在与否来判断——如果某个 User 关联了 Doctor 记录,他就是医生。

# 用装饰器限制角色访问 from django.contrib.auth.decorators import user_passes_test def is_doctor(user): return hasattr(user, 'doctor') def is_patient(user): return not hasattr(user, 'doctor') and not user.is_staff @user_passes_test(is_doctor) def doctor_schedule_manage(request): """医生管理自己的排班""" schedules = Schedule.objects.filter(doctor=request.user.doctor) return render(request, 'doctor/schedule_manage.html', {'schedules': schedules}) @user_passes_test(is_patient) def my_registrations(request): """患者查看自己的挂号记录""" registrations = Registration.objects.filter( patient=request.user ).select_related('schedule__doctor__user', 'schedule__doctor__department') return render(request, 'registration/my_registrations.html', {'registrations': registrations})

hasattr(user, 'doctor')利用了 Django 反向关联的特性:如果Doctor模型里用OneToOneField(User),那么 User 实例会自动获得一个.doctor属性。这种判断方式简洁,但要注意如果医生账号被删除,hasattr会返回 False,需要处理边界情况。更稳妥的做法是在User上显式加role字段,用choices限定取值范围。

3.4 后台管理:用 Django admin 快速搭建医生和科室维护

Django admin 是这个项目的一大优势。你不需要写任何前端代码,只要在admin.py里注册模型,就能获得一套功能完整的增删改查后台。

# admin.py 注册模型到后台 from django.contrib import admin from .models import Department, Doctor, Schedule, Registration @admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): list_display = ('name', 'description') search_fields = ('name',) @admin.register(Doctor) class DoctorAdmin(admin.ModelAdmin): list_display = ('user', 'department', 'title', 'specialty') list_filter = ('department', 'title') search_fields = ('user__username', 'user__last_name') @admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): list_display = ('doctor', 'date', 'time_slot', 'total_slots', 'remaining_slots') list_filter = ('date', 'time_slot', 'doctor__department') date_hierarchy = 'date' @admin.register(Registration) class RegistrationAdmin(admin.ModelAdmin): list_display = ('patient', 'schedule', 'status', 'created_at') list_filter = ('status', 'created_at') search_fields = ('patient__username',)

list_display控制列表页显示哪些字段,list_filter提供右侧筛选器,search_fields开启搜索框,date_hierarchy在排班管理里加一个按日期钻取的导航。这些配置加起来不到二十行代码,但省掉了大量后台页面的开发时间。答辩时演示后台管理功能,直接用 admin 就能撑住场面。

4. 避坑与排查:跑这份源码时最容易翻车的五个地方

4.1 静态文件 404:CSS 和 JS 加载不出来

现象:页面能打开,但样式全乱,浏览器控制台报GET /static/css/style.css 404。

原因:Django 在DEBUG=True时会自动处理静态文件,但前提是INSTALLED_APPS里有django.contrib.staticfiles,且STATIC_URL配置正确。如果项目把静态文件放在 app 下的static/目录里,还需要确保模板里用的是{% static 'css/style.css' %}而不是硬编码路径。

解决:检查settings.py里STATIC_URL = '/static/'是否存在,INSTALLED_APPS是否包含django.contrib.staticfiles。如果静态文件在项目根目录的static/下,还要加STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]。模板开头记得{% load static %}。

4.2 数据库迁移报错:Table already exists

现象:执行python manage.py migrate时提示某张表已存在,或者django.db.utils.OperationalError。

原因:通常是之前跑过一次迁移,中途失败,或者手动导入过.sql文件建了表,但 Django 的迁移记录表django_migrations里没有对应记录。Django 以为没建过表,又去建一次,就冲突了。

解决:先确认数据库里到底有哪些表。如果表结构已经和模型一致,可以用python manage.py migrate --fake跳过已存在的迁移。如果表结构不对,最干净的做法是删库重建:DROP DATABASE hospital_registration; CREATE DATABASE hospital_registration CHARACTER SET utf8mb4;,然后重新migrate。别在迁移状态混乱的情况下硬改,越改越乱。

4.3 中文乱码:数据库字符集没设对

现象:页面显示的中文变成????或乱码,或者存入数据库的中文查出来是问号。

原因:MySQL 建库时没指定utf8mb4,默认可能是latin1。或者 Django 连接配置里没写OPTIONS: {'charset': 'utf8mb4'}。

解决:建库时强制指定字符集:CREATE DATABASE hospital_registration CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。settings.py的DATABASES里加上'OPTIONS': {'charset': 'utf8mb4'}。如果表已经建好了,可以用ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4;逐张改,但不如重建省事。

4.4 挂号并发问题:两个患者同时抢最后一个号

现象:测试时发现某个排班的余号变成负数,或者两个患者都挂上了同一个号。

原因:视图函数里先查询余号,再扣减,这中间有时间窗口。如果两个请求同时进来,都查到余号为 1,都判断可以挂号,然后都扣减,就超卖了。

解决:用数据库层面的锁。Django 里可以用select_for_update()配合事务:

@transaction.atomic def register(request, schedule_id): # 加行锁,其他请求会等待 schedule = Schedule.objects.select_for_update().get(id=schedule_id) if schedule.remaining_slots <= 0: return render(request, 'error.html', {'msg': '号源已满'}) schedule.remaining_slots -= 1 schedule.save() Registration.objects.create(patient=request.user, schedule=schedule) return redirect('registration:my_registrations')

select_for_update()会在查询时对行加锁,直到事务结束才释放。这样第二个请求会等第一个事务完成后才读到最新的余号。毕设答辩时如果老师问并发问题,这就是加分项。

4.5 时间字段时区错误:挂号时间差八小时

现象:created_at存的时间比实际时间少八小时,或者排班日期判断出错。

原因:Django 默认用 UTC 时间,settings.py里USE_TZ = True时,auto_now_add存的是 UTC。如果TIME_ZONE没设成Asia/Shanghai,显示和存储都会偏。

解决:settings.py里设TIME_ZONE = 'Asia/Shanghai'和USE_TZ = True。模板里显示时间时用{{ registration.created_at|date:"Y-m-d H:i" }},Django 会自动转成当前时区。如果数据库里已经存了错误的时间,需要手动修正或重新导入数据。

5. 二次开发与毕设加分项:从能跑到能讲

5.1 加一个「取消挂号」功能:完整的前后端改动

原始项目如果只有挂号没有取消,答辩时容易被问「患者临时有事怎么办」。加这个功能不复杂,但能体现你对业务闭环的理解。改动分三处:视图、路由、模板。

# views.py 取消挂号 @login_required @transaction.atomic def cancel_registration(request, registration_id): registration = get_object_or_404( Registration, id=registration_id, patient=request.user ) if registration.status != 'pending': return render(request, 'error.html', {'msg': '该挂号已处理,无法取消'}) # 回滚号源 schedule = Schedule.objects.select_for_update().get(id=registration.schedule.id) schedule.remaining_slots += 1 schedule.save() registration.status = 'cancelled' registration.save() return redirect('registration:my_registrations')
# urls.py 加路由 path('cancel/<int:registration_id>/', views.cancel_registration, name='cancel_registration'),
<!-- 模板里加按钮 --> {% if reg.status == 'pending' %} <a href="{% url 'registration:cancel_registration' reg.id %}" onclick="return confirm('确认取消挂号?')">取消挂号</a> {% endif %}

逻辑说明:取消时先判断状态是否为pending,已完成的不能取消。然后用select_for_update锁住排班行,把remaining_slots加回去。最后把挂号记录状态改成cancelled,而不是直接删除记录,这样保留操作痕迹,方便后台统计。模板里的onclick加一个确认弹窗,防止误操作。

5.2 用 Django ORM 做统计报表:给答辩加一个数据看板

答辩时如果只演示增删改查,老师可能会觉得工作量不够。加一个简单的统计页面,用 ORM 的聚合查询展示「各科室挂号量」「医生接诊排行」,代码量不大但视觉效果明显。

# views.py 统计看板 from django.db.models import Count, Q from django.utils import timezone @user_passes_test(lambda u: u.is_staff) def dashboard(request): # 各科室挂号量 dept_stats = Department.objects.annotate( reg_count=Count('doctor__schedule__registration', filter=Q(doctor__schedule__registration__status='completed')) ).values('name', 'reg_count').order_by('-reg_count') # 今日挂号总数 today = timezone.now().date() today_count = Registration.objects.filter( created_at__date=today ).count() # 医生接诊排行前五 doctor_rank = Doctor.objects.annotate( completed=Count('schedule__registration', filter=Q(schedule__registration__status='completed')) ).select_related('user').order_by('-completed')[:5] return render(request, 'admin/dashboard.html', { 'dept_stats': dept_stats, 'today_count': today_count, 'doctor_rank': doctor_rank, })

annotate配合Count和filter可以在一次查询里完成分组统计,不用写原生 SQL。Q对象用于在聚合时加条件,比如只统计状态为completed的挂号。values指定返回哪些字段,order_by排序。这个看板页面用 Chart.js 或 ECharts 渲染成柱状图,答辩演示效果会好很多。

5.3 部署上线:从 runserver 到生产环境的最小改动

python manage.py runserver只适合开发,性能差且不安全。如果毕设要求部署演示,可以用gunicorn+nginx或者更简单的waitress。最小改动方案是改settings.py里的DEBUG = False和ALLOWED_HOSTS,然后用waitress跑起来。

# 安装 waitress(Windows 友好) pip install waitress # 启动服务,监听 8000 端口 waitress-serve --listen=0.0.0.0:8000 hospital_registration.wsgi:application

DEBUG = False后,Django 不再自动处理静态文件,需要用python manage.py collectstatic把所有静态文件收集到STATIC_ROOT目录,然后由 nginx 或 waitress 托管。ALLOWED_HOSTS要填服务器 IP 或域名,否则会报DisallowedHost。如果只是本地演示,ALLOWED_HOSTS = ['*']也能用,但生产环境别这么写。

5.4 答辩时怎么讲这份代码:三个技术亮点

最后说一个实在的:代码跑通了,还得能讲清楚。这份项目有三个点可以在答辩时展开。第一,用transaction.atomic和select_for_update处理挂号并发,体现对数据一致性的理解。第二,用 Django ORM 的annotate和Q对象做统计,展示对框架的熟练度。第三,用user_passes_test装饰器做角色隔离,说明权限控制的设计思路。这三个点都不复杂,但能把你和「只会 CRUD」的同学区分开。

我自己的习惯是,每次拿到一份新源码,先跑通主流程,然后挑一个核心功能把代码读一遍,最后试着加一个小功能。这套流程走下来,这份代码就真正变成你的了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询