“乡镇居民诊疗挂号信息系统”,听起来好像是个挺大的工程,但本质上就是用Python Web那一套成熟技术,把一个线下排队的场景搬到线上。最近我在PyCharm里用Django完整做了一遍,从需求梳理、数据库建模到挂号下单、后台维护,把整个流程跑通了。这篇博文就把我做这个项目时的思路、代码、踩过的坑全部摊开来讲,如果你也准备用Django或Flask做类似的信息管理系统,或者正在憋课程设计、毕业设计,这篇应该能帮你少走不少弯路。
先说清楚这东西到底解决什么问题。乡镇卫生院和社区卫生服务中心的就医流程,跟三甲医院不太一样,病人数量没那么恐怖,但科室设置、医生排班、号源管理这些环节一个都不能少。以前靠护士拿纸质本子登记,高峰期容易排错队、挂错科,月底统计也费劲。换成Web挂号系统之后,居民自己用手机或电脑就能预约,窗口护士在后台一键排班,医生也能看到当天有几个号、都是谁。整个流程透明了,工作量反而降下来了。
1. 项目画像:先搞清楚系统到底要管哪些事
1.1 核心需求拆解
做这种管理系统,最忌讳一上来就写代码。我习惯先列一张需求清单,把“谁在用、用来干什么、管什么数据”这三件事理清楚。对于乡镇居民诊疗挂号系统,主要角色其实就三类:
- 居民/患者:注册登录、浏览科室和医生、选择时间段挂号、查看自己的挂号记录、取消预约。
- 医院工作人员(护士/前台):维护科室信息、录入医生资料、配置每周排班、处理退号、查看当天挂号统计。
- 系统管理员:管理用户权限、初始化基础数据、数据库备份维护。
对应这三类角色,核心功能就能拆成几个模块:用户认证模块、基础数据管理(科室、医生、排班)、挂号业务模块(预约、退号、号源扣减)、后台管理模块(Admin界面或自定义管理页面)。
我见过不少类似的课程设计,把系统做得很重,什么在线问诊、电子病历、药品库存全都塞进去,结果每个功能都只做了个壳。我这个项目的边界切得很明确——看病历、收费、医保这类短期内支撑不了的业务先不做,把“挂得上号、退得了号、统计得出数据”这条主链路做扎实,才是这个系统真正的价值所在。
1.2 为什么用Python Web来落地
选择技术栈之前,我先问了自己一个问题:这个系统最看重什么?答案是开发效率和维护成本。乡镇卫生院不会有专职程序员坐镇,系统交付之后很可能由信息科的人兼职维护,代码的可读性和框架的稳定性必须排在最前面。
Python的两大Web框架,Django和Flask,都满足这个条件。Django的核心理念是“全家桶”,ORM、Admin后台、认证系统、表单处理全都有现成的,非常适合业务逻辑标准的信息管理系统;Flask走的是“微框架”路线,只保留最核心的路由和模板引擎,其他组件自由选配。我在下一节会详细对比这两个方案,但先剧透一下结论:这个项目我最终选型是Django,原因就一句话——挂号系统的业务逻辑本身就是标准的CRUD加少量事务处理,Django的ORM和Admin能把这部分工作量压缩掉至少三分之一。
2. 技术选型:Django和Flask到底选哪个
2.1 两个框架的定位差异
很多人纠结Django还是Flask,其实它们解决的是不同层面的问题。打个比方,Django像是一套精装修交付的房子,拎包入住,厨房、卫生间、电路全给你规划好了,你只需要按自己的风格摆家具;Flask像loft毛坯房,承重墙和基本管线有,剩下的空间怎么隔断、走线、装修全看你自己。
具体到功能对比,我用一张表说清楚:
| 对比维度 | Django | Flask |
|---|---|---|
| 项目结构 | 自带project/app分层,约定优于配置 | 默认只有一个app入口,结构自由但需自己规划 |
| ORM | 内置强大ORM,支持迁移、关联查询 | 默认无ORM,常配SQLAlchemy |
| Admin后台 | 自带完整后台,注册模型即可用 | 需要扩展flask-admin |
| 用户认证 | 内置User模型与认证视图 | 需要flask-login等扩展 |
| 表单处理 | 内置Form和CSRF防护 | 需要Flask-WTF |
| 学习曲线 | 框架概念多,初期有点陡 | 上手快,但后期组件选型靠自己 |
| 适合场景 | 信息管理系统、内容管理、后台类项目 | API服务、微服务、快速原型 |
说句实在话,如果你只学过Flask、没碰过Django,用Flask做这个挂号系统也完全可以。你需要额外安装Flask-SQLAlchemy做ORM,Flask-Login做登录态,Flask-WTF做表单和CSRF防护,Flask-Admin做后台管理。加起来也是一套组合拳,只是每一步都要自己接。我这次之所以选Django,就是想把精力集中到挂号业务本身,而不是去组装基础设施。
2.2 开发工具:PyCharm为什么顺手
开发环境我用的是PyCharm,不是因为它多高大上,而是它跟Python项目配合得最自然。PyCharm有几个功能对这个项目帮助特别大:
- 新建项目时直接创建虚拟环境(venv),依赖隔离得很干净,不会把试验项目里的包弄乱。
- 自带Django支持,识别manage.py和settings.py之后,可以直接通过IDE运行manage.py命令,不用每次切到终端敲。
- Debug模式非常好用,在视图函数里打一个断点,就能看到request对象、表单数据、数据库查询结果,排查问题比print大法高效太多。
社区版免费版就够用了,Django项目的调试、代码提示这些核心功能都不缺。后面所有实操步骤我都会基于PyCharm社区版来说明。
3. 开发环境准备:从Python到Django项目骨架
3.1 Python解释器与虚拟环境
开始写代码之前,先把基础环境准备好。我用的Python版本是3.10,Django版本是4.2 LTS。如果你自己装Python,安装时记得勾选“Add Python to PATH”那个选项,不然命令行里打python会提示找不到命令,这是新手最容易踩的第一个坑。
装完Python之后,我习惯先做一个配置:把pip源换成国内镜像。不换的话,pip install django下载速度可能让你怀疑人生。用清华源一行命令搞定:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple然后在PyCharm里新建项目。项目名我建议用英文,比如registration_system,不要用中文。PyCharm创建项目时会默认创建一个虚拟环境,路径在项目目录下的venv文件夹里。虚拟环境的作用是隔离依赖,让这个项目用到的Django版本不会影响你其他项目。
接下来安装Django:
pip install django==4.2装完后用python -m django --version验证一下,能看到版本号就说明环境OK了。
3.2 创建项目和应用
Django的项目结构有个约定,一个“项目”相当于整个网站,一个“应用”相当于网站里的一个功能模块。挂号系统我规划了两个应用——accounts负责用户认证,hospital负责科室、医生、排班、挂号这些核心业务。
进入项目根目录后,执行:
django-admin startproject config . python manage.py startapp accounts python manage.py startapp hospital注意第一条命令后面有个点,意思是把项目配置文件生成在当前目录,而不是再包一层文件夹。生成完之后,目录结构大概是这样的:
registration_system/ ├── config/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── accounts/ ├── hospital/ ├── manage.py └── venv/接下来要把两个应用注册到配置里。打开config/settings.py,找到INSTALLED_APPS列表,加上accounts和hospital。顺便把语言和时区改成中国习惯:
LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True改完这两个配置,后台界面会显示中文,时间也不会差8个小时了。然后运行迁移命令,把Django内置的数据表建好:
python manage.py migrate python manage.py createsuperusercreatesuperuser是创建一个管理员账号,一会登录后台用。到这一步,项目骨架已经跑起来了,在PyCharm里点运行,浏览器访问http://127.0.0.1:8000,能看到Django默认的火箭页面就说明一切正常。
4. 数据模型设计:四张核心表撑起挂号业务
4.1 模型定义与字段说明
挂号系统的业务数据,说穿了就是四个实体:科室、医生、排班、挂号记录。我用Django的models模块把它们定义出来,代码在hospital/models.py里。
Department(科室表)字段最简单,就是名字和简介;Doctor(医生表)通过外键关联科室;Schedule(排班表)是连接医生和号源的关键,每天每个医生可以有好几个时段,每个时段有一个总号数和剩余号数;Appointment(挂号记录表)记录哪个居民挂了哪个排班的号。
from django.db import models from django.contrib.auth.models import User class Department(models.Model): name = models.CharField('科室名称', max_length=50, unique=True) description = models.TextField('科室简介', blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class Meta: verbose_name = '科室' verbose_name_plural = '科室' class Doctor(models.Model): name = models.CharField('医生姓名', max_length=30) department = models.ForeignKey( Department, on_delete=models.CASCADE, verbose_name='所属科室', related_name='doctors' ) title = models.CharField('职称', max_length=30, blank=True) introduction = models.TextField('医生简介', blank=True) def __str__(self): return f'{self.name}({self.department.name})' class Meta: verbose_name = '医生' verbose_name_plural = '医生' class Schedule(models.Model): doctor = models.ForeignKey( Doctor, on_delete=models.CASCADE, verbose_name='医生', related_name='schedules' ) date = models.DateField('出诊日期') start_time = models.TimeField('开始时间') end_time = models.TimeField('结束时间') total = models.PositiveIntegerField('总号数', default=20) remaining = models.PositiveIntegerField('剩余号数', default=20) def __str__(self): return f'{self.doctor.name} {self.date} {self.start_time}-{self.end_time}' class Meta: verbose_name = '出诊排班' verbose_name_plural = '出诊排班' unique_together = ('doctor', 'date', 'start_time') class Appointment(models.Model): STATUS_CHOICES = ( ('booked', '已预约'), ('completed', '已完成'), ('cancelled', '已取消'), ) user = models.ForeignKey( User, on_delete=models.CASCADE, verbose_name='预约用户', related_name='appointments' ) schedule = models.ForeignKey( Schedule, on_delete=models.CASCADE, verbose_name='排班', related_name='appointments' ) created_at = models.DateTimeField('预约时间', auto_now_add=True) status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='booked') class Meta: verbose_name = '挂号记录' verbose_name_plural = '挂号记录'几个设计上的考虑说明一下。Schedule表里的remaining字段是核心,挂号时判断号源还剩多少全靠它。unique_together保证同一个医生在同一天的同一个开始时间只能有一条排班记录,防止重复录入。Appointment.status用字符串存状态而不是直接删除记录,是为了保留历史数据,方便日后统计。
4.2 数据库迁移与ORM操作技巧
模型定义好之后,需要通过迁移把表建到数据库里。这一串命令要记住:
python manage.py makemigrations python manage.py migratemakemigrations会扫描模型的变更,在应用目录下生成迁移文件;migrate才是真正执行SQL、把表建出来。每次改了models.py里的字段,都要走一遍这两个命令。
项目开发阶段我默认用的是SQLite数据库,零配置文件、单文件存储,对学习和小规模部署足够友好。后续要切换MySQL也很简单,改一下settings.py里的DATABASES配置,再安装mysqlclient驱动就行。正式的乡镇卫生院如果挂号量不大,SQLite其实也顶得住;当然生产环境我更推荐PostgreSQL或MySQL,看运维条件。
ORM的操作平时用到最多的几种写法也列一下,给不熟Django的人做个参考:
from hospital.models import Department, Doctor, Schedule, Appointment from django.contrib.auth.models import User # 查询内科下所有医生 doctors = Doctor.objects.filter(department__name='内科') # 查询2024-06-01当天还有号的排班 available_schedules = Schedule.objects.filter(date='2024-06-01', remaining__gt=0) # 创建挂号记录 appt = Appointment.objects.create(user=user, schedule=schedule, status='booked') # 取消挂号:更新状态而不是物理删除 Appointment.objects.filter(id=appt_id).update(status='cancelled') # 删除对象(确实需要删除时) schedule = Schedule.objects.get(id=schedule_id) schedule.delete()filter()返回的是QuerySet,可以继续链式调用;get()返回单个对象,查不到会抛DoesNotExist异常,多个结果会抛MultipleObjectsReturned。update()走的是SQL UPDATE语句,适合批量修改;而先get再.delete()会先加载对象再删,两种方式各有适用场景。这些细节不用死记,踩过几次坑自然就记住了。
5. 核心功能模块开发与防坑实录
5.1 用户注册登录:Django自带的认证最省事
用户认证这块,Django自带的auth系统已经提供了User模型和登录会话管理,没必要自己造轮子。我只需要写两个视图和一个登录表单。
在accounts/views.py里实现注册和登录:
from django.shortcuts import render, redirect from django.contrib.auth.models import User from django.contrib.auth import login, authenticate, logout from django.contrib.auth.decorators import login_required def register(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') confirm = request.POST.get('confirm_password') if password != confirm: return render(request, 'accounts/register.html', {'error': '两次密码不一致'}) if User.objects.filter(username=username).exists(): return render(request, 'accounts/register.html', {'error': '用户名已存在'}) user = User.objects.create_user(username=username, password=password) login(request, user) return redirect('home') return render(request, 'accounts/register.html') def user_login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('home') return render(request, 'accounts/login.html', {'error': '用户名或密码错误'}) return render(request, 'accounts/login.html') @login_required def user_logout(request): logout(request) return redirect('login')这里的关键是create_user,它会对密码做哈希加密,千万不要直接用User.objects.create(username=..., password=...),那样会把明文密码存进数据库,一旦泄露就是严重的安全事故。authenticate加login的组合负责校验密码并写入登录态,后面的视图只要加上@login_required装饰器,未登录用户就会被自动重定向到登录页。
Django默认的登录态机制是基于session的,对浏览器非常友好,完全不需要自己管理token。
5.2 预约挂号下单:防止“超号”的两种姿势
挂号下单是整个系统里最有技术含量的一个环节。场景是这样的:每个排班有固定的号源总数,比如上午放20个号,第20个人挂号的时候剩余号数还剩1个,第21个人如果同时操作就可能撞车。如果代码写得糙,就会出现“挂号成功但号源是负的”这种事故。
我见过很多课程设计里直接这么写:
# 错误示范 schedule = Schedule.objects.get(id=schedule_id) if schedule.remaining > 0: schedule.remaining -= 1 schedule.save() Appointment.objects.create(...)这段代码在单线程、单用户测试时没问题,但只要并发稍微上来一点,两个请求同时读到remaining=1,判断都通过,然后各自减1,最后remaining变成-1,号池超卖了。解决思路是加锁或原子操作。
方案一:select_for_update行级锁,把判断和扣减放进一个事务里:
from django.db import transaction from django.shortcuts import get_object_or_404 @transaction.atomic def book_appointment(request, schedule_id): schedule = get_object_or_404( Schedule.objects.select_for_update(), id=schedule_id ) if schedule.remaining <= 0: return render(request, 'hospital/full.html', {'message': '该时段已挂满'}) schedule.remaining -= 1 schedule.save(update_fields=['remaining']) Appointment.objects.create( user=request.user, schedule=schedule, status='booked' ) return redirect('my_appointments')select_for_update会在数据库层面把这一行锁住,其他事务要等当前事务提交后才能操作这行,这样就能保证判断剩余号数和扣减号数的原子性。注意它必须放在transaction.atomic()里才会生效,而且要确保查的是排班表这行数据所在的查询集,Django对JOIN查询加锁有对锁的警告,测试环境用直接查表的方式最稳妥。
方案二:用F()表达式做原子更新,配合filter(remaining__gt=0):
updated = Schedule.objects.filter(id=schedule_id, remaining__gt=0).update(remaining=F('remaining') - 1) if updated == 0: return render(request, 'hospital/full.html', {'message': '该时段已挂满'}) Appointment.objects.create(user=request.user, schedule=schedule, status='booked')F('remaining') - 1这个操作是在数据库端完成的,不经过Python读改写,天然避免并发问题;filter(remaining__gt=0).update(...)只有满足条件才会更新成功,返回值updated为0就说明号已经被抢完了。方案二比方案一更轻量,也是我最后采用的方案。我写这段代码时被“超号”问题折腾了很久,用Django shell模拟多线程并发下单试过,方案二实测下来确实靠谱。
5.3 退号与号源释放
取消预约的逻辑,很多人会忘记同时把号源放回去。如果只把Appointment.status改成cancelled,而不恢复Schedule.remaining,那么被取消的号就永远变成“死号”,实际有余量但患者挂不上。正确做法是两个操作放在同一个事务里:
@transaction.atomic def cancel_appointment(request, appointment_id): appt = get_object_or_404( Appointment.objects.select_for_update(), id=appointment_id, user=request.user ) if appt.status == 'cancelled': return redirect('my_appointments') appt.status = 'cancelled' appt.save(update_fields=['status']) Schedule.objects.filter(id=appt.schedule_id).update(remaining=F('remaining') + 1) return redirect('my_appointments')这里我特意加了select_for_update锁住挂号记录,防止同一个用户连续发送两次取消请求导致号源被恢复两次。真实场景中用户手抖点两下取消按钮很常见,不锁的话号源数据就会漂移。这种细节看起来不起眼,但生产环境特别容易出问题。
5.4 后台管理与Admin界面
Django的Admin后台是这个项目最划算的“白嫖”功能。只要把模型注册到hospital/admin.py里,护士维护科室、医生、排班的工作就能在后台直接完成,根本不用另外写管理页面。我的注册代码长这样:
from django.contrib import admin from .models import Department, Doctor, Schedule, Appointment @admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): list_display = ('name', 'created_at') search_fields = ('name',) @admin.register(Doctor) class DoctorAdmin(admin.ModelAdmin): list_display = ('name', 'department', 'title') list_filter = ('department',) search_fields = ('name',) @admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): list_display = ('doctor', 'date', 'start_time', 'end_time', 'total', 'remaining') list_filter = ('date', 'doctor__department') date_hierarchy = 'date' @admin.register(Appointment) class AppointmentAdmin(admin.ModelAdmin): list_display = ('user', 'schedule', 'status', 'created_at') list_filter = ('status', 'schedule__date') search_fields = ('user__username',)list_display设置后台列表页显示哪些列,list_filter在右侧生成筛选器,search_fields提供搜索框。Appointment的筛选器里我用了schedule__date,双下划线是Django跨表查询的语法,在Admin里照样能用。这套配置加起来不到30行,就把后台管理页面全部搞定了。
5.5 一个容易忽略的问题:CSRF校验与请求拦截
给模板里的表单加上{% csrf_token %}是Django玩家的基本素养,但我在开发时还是经常遇到Forbidden报错。尤其用Ajax提交POST请求的时候,必须从cookie里取出csrf token放进请求头。新手最常见的错误是忘记在模板里加{% csrf_token %},或者把Django的CSRF和浏览器的安全拦截搞混。看到类似“请求被拦截”“Forbidden (CSRF token missing or incorrect)”这类红色报错页时,先检查表单有没有加token,而不是急着改后端逻辑。
如果确实需要做纯API接口给小程序用,可以给视图加@csrf_exempt跳过校验,但所有用它来调用POST接口都会面临CSRF攻击风险,不太建议在正式业务里滥用。我的原则是:能走Django表单就尽量走表单,让框架替你处理安全问题。
6. 常见报错与排查速查表
6.1 开发期高频问题汇总
这段时间我整理了开发过程中遇到的典型报错和解决方案,直接做成一张速查表,方便你对照排查:
| 报错信息 | 原因分析 | 解决方法 |
|---|---|---|
| ModuleNotFoundError: No module named 'django' | 当前Python环境没装Django,或PyCharm解释器选错 | 检查PyCharm Project Interpreter是否为项目虚拟环境;在终端重新pip install django |
| DisallowedHost at / | ALLOWED_HOSTS没配置,访问域名/IP不在白名单 | 在settings.py里加ALLOWED_HOSTS = ['*'](开发)或填具体域名(生产) |
| OperationalError: no such table: hospital_appointment | 模型建好了但没迁移(migrate) | 执行python manage.py makemigrations再python manage.py migrate |
| Forbidden (CSRF token missing or incorrect) | 表单没加{% csrf_token %},或Ajax请求头没带token | 模板表单加{% csrf_token %};Ajax用cookie读取token放到header |
| TypeError: 'NoneType' object is not callable | 视图函数名写错、或URL路由对应的视图不存在 | 检查urls.py的path第二参数是否指向正确视图函数 |
| AttributeError: 'Schedule' object has no attribute 'doctor_name' | 模板里访问了模型不存在属性 | 在模型定义@property或在模板用schedule.doctor.name |
| RuntimeWarning: DateTimeField received a naive datetime | 代码里用了不含时区的datetime.now() | 改用django.utils.timezone.now() |
| Bad Request (400) 页面能打开但总是400 | settings里ALLOWED_HOSTS为空且访问host不在列表 | 检查配置文件,开发期设置ALLOWED_HOSTS = ['*'] |
6.2 排查问题的三板斧
遇到Bug先别慌,我的排查顺序是固定的:先看浏览器开发者工具里的Network面板,确认请求地址、请求方式、状态码;再看PyCharm的控制台输出,Django会把完整的异常堆栈打出来,最后一行通常会告诉你错在哪个文件的哪一行;最后一招才是打断点跑代码,在视图函数里点一下行号旁边的空白处就能设断点,然后按Debug按钮以调试模式启动。
我最想提醒的是:不要靠猜来改代码。我在调试过程中学到一个教训,改成print()慢慢打出中间变量,虽然笨,但最可靠。等把报错路径摸清楚了再动手,往往只需要改一行代码就能让功能恢复正常。
7. 上线前的收尾检查与优化建议
7.1 部署前必须处理的几个配置
如果这个系统要真正给卫生院用,有几个Django默认配置是必须改的,不然上线就会出安全问题或者性能问题。我列一个清单:
DEBUG = False。调试模式会把服务器源代码、配置信息直接暴露在错误页面上,这是最危险的一个开关。ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com'],限制哪些域名和IP可以访问系统。- 收集静态文件,
python manage.py collectstatic,把Admin和相关静态资源统一放到STATIC_ROOT目录里,交给Nginx等Web服务器处理。 - 数据库迁移到生产库,注意先在本地
python manage.py makemigrations备份迁移文件,再在生产环境执行migrate。 - 设置数据库备份任务,SQLite文件直接定时复制,MySQL用
mysqldump,建议一天至少备份一次。
7.2 从“能跑”到“好用”的一点扩展想法
核心挂号链路稳定了,这个系统其实还能扩展出很多东西。乡镇卫生院如果要进一步提升线上服务能力,以下几个方向都值得考虑:
- 微信预约入口:把挂号流程包装成H5小页面,通过微信公众号菜单跳转,对不熟悉电脑的老人也更友好。
- 短信通知:预约成功、退号确认、出诊提醒用短信模板推送给居民,降低爽约率。
- 号源策略优化:部分热门科室支持分时段放号,比如早上放10个、下午放10个,避免一下子被抢光。
- 数据统计看板:按科室、按医生维度统计月挂号量,用图表展示高峰时段,辅助排班决策。
我个人做这个项目最大的体会是,技术本身并没有多难,难的是把业务逻辑想透。挂号系统的每一个状态、每一次号源变化都对应着真实的就诊流程,写代码之前先把自己代入到挂号的人、排班的护士、出诊的医生这三个角色里,很多设计决策自然就有了答案。如果你也正在做类似的Web管理系统,希望这篇手记能给你一点参考,少踩几个我踩过的坑。