十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于Django的乡镇挂号系统开发:数据模型、并发控制与后台管理实战

基于Django的乡镇挂号系统开发:数据模型、并发控制与后台管理实战 “乡镇居民诊疗挂号信息系统”听起来好像是个挺大的工程但本质上就是用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毛坯房承重墙和基本管线有剩下的空间怎么隔断、走线、装修全看你自己。具体到功能对比我用一张表说清楚对比维度DjangoFlask项目结构自带project/app分层约定优于配置默认只有一个app入口结构自由但需自己规划ORM内置强大ORM支持迁移、关联查询默认无ORM常配SQLAlchemyAdmin后台自带完整后台注册模型即可用需要扩展flask-admin用户认证内置User模型与认证视图需要flask-login等扩展表单处理内置Form和CSRF防护需要Flask-WTF学习曲线框架概念多初期有点陡上手快但后期组件选型靠自己适合场景信息管理系统、内容管理、后台类项目API服务、微服务、快速原型说句实在话如果你只学过Flask、没碰过Django用Flask做这个挂号系统也完全可以。你需要额外安装Flask-SQLAlchemy做ORMFlask-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.10Django版本是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版本不会影响你其他项目。接下来安装Djangopip install django4.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_length50, uniqueTrue) description models.TextField(科室简介, blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class Meta: verbose_name 科室 verbose_name_plural 科室 class Doctor(models.Model): name models.CharField(医生姓名, max_length30) department models.ForeignKey( Department, on_deletemodels.CASCADE, verbose_name所属科室, related_namedoctors ) title models.CharField(职称, max_length30, blankTrue) introduction models.TextField(医生简介, blankTrue) 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_deletemodels.CASCADE, verbose_name医生, related_nameschedules ) date models.DateField(出诊日期) start_time models.TimeField(开始时间) end_time models.TimeField(结束时间) total models.PositiveIntegerField(总号数, default20) remaining models.PositiveIntegerField(剩余号数, default20) 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_deletemodels.CASCADE, verbose_name预约用户, related_nameappointments ) schedule models.ForeignKey( Schedule, on_deletemodels.CASCADE, verbose_name排班, related_nameappointments ) created_at models.DateTimeField(预约时间, auto_now_addTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultbooked) 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(date2024-06-01, remaining__gt0) # 创建挂号记录 appt Appointment.objects.create(useruser, scheduleschedule, statusbooked) # 取消挂号更新状态而不是物理删除 Appointment.objects.filter(idappt_id).update(statuscancelled) # 删除对象确实需要删除时 schedule Schedule.objects.get(idschedule_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(usernameusername).exists(): return render(request, accounts/register.html, {error: 用户名已存在}) user User.objects.create_user(usernameusername, passwordpassword) 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, usernameusername, passwordpassword) 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(idschedule_id) if schedule.remaining 0: schedule.remaining - 1 schedule.save() Appointment.objects.create(...)这段代码在单线程、单用户测试时没问题但只要并发稍微上来一点两个请求同时读到remaining1判断都通过然后各自减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(), idschedule_id ) if schedule.remaining 0: return render(request, hospital/full.html, {message: 该时段已挂满}) schedule.remaining - 1 schedule.save(update_fields[remaining]) Appointment.objects.create( userrequest.user, scheduleschedule, statusbooked ) return redirect(my_appointments)select_for_update会在数据库层面把这一行锁住其他事务要等当前事务提交后才能操作这行这样就能保证判断剩余号数和扣减号数的原子性。注意它必须放在transaction.atomic()里才会生效而且要确保查的是排班表这行数据所在的查询集Django对JOIN查询加锁有对锁的警告测试环境用直接查表的方式最稳妥。方案二用F()表达式做原子更新配合filter(remaining__gt0)updated Schedule.objects.filter(idschedule_id, remaining__gt0).update(remainingF(remaining) - 1) if updated 0: return render(request, hospital/full.html, {message: 该时段已挂满}) Appointment.objects.create(userrequest.user, scheduleschedule, statusbooked)F(remaining) - 1这个操作是在数据库端完成的不经过Python读改写天然避免并发问题filter(remaining__gt0).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(), idappointment_id, userrequest.user ) if appt.status cancelled: return redirect(my_appointments) appt.status cancelled appt.save(update_fields[status]) Schedule.objects.filter(idappt.schedule_id).update(remainingF(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 djangoDisallowedHost at /ALLOWED_HOSTS没配置访问域名/IP不在白名单在settings.py里加ALLOWED_HOSTS [*]开发或填具体域名生产OperationalError: no such table: hospital_appointment模型建好了但没迁移migrate执行python manage.py makemigrations再python manage.py migrateForbidden (CSRF token missing or incorrect)表单没加{% csrf_token %}或Ajax请求头没带token模板表单加{% csrf_token %}Ajax用cookie读取token放到headerTypeError: NoneType object is not callable视图函数名写错、或URL路由对应的视图不存在检查urls.py的path第二参数是否指向正确视图函数AttributeError: Schedule object has no attribute doctor_name模板里访问了模型不存在属性在模型定义property或在模板用schedule.doctor.nameRuntimeWarning: DateTimeField received a naive datetime代码里用了不含时区的datetime.now()改用django.utils.timezone.now()Bad Request (400) 页面能打开但总是400settings里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管理系统希望这篇手记能给你一点参考少踩几个我踩过的坑。
返回列表