
“python基于vue的康复医院挂号管理系统的设计与实现”这个标题细看就是一套标准的全栈开发活Python做后端、Vue做前端、Django/Flask二选一当Web框架、PyCharm当开发环境。但真正把它当成一个能跑通的系统来搭难点并不在代码量而在业务流程怎么拆。康复医院的挂号不是普通门诊那种“挂个号去诊室门口等”的简单模型它牵扯到疗程制预约、治疗师排班、号源控制、取消释放这些逻辑而这些恰好是面试官或者答辩老师最会追问的地方。这篇文章就围绕这个项目从需求拆解到数据库设计、后端接口、前端页面、联调部署一步步讲清楚顺便把我实际开发中踩过的坑和绕过的弯都记录下来。无论你是做毕业设计还是想完整走一遍 Python 全栈项目的新手都可以直接对着这份思路往下做。后端我会以 Django 为主线来讲同时也会说清楚 Flask 在这个项目里到底能扮演什么角色。1. 项目到底在做什么需求拆解与流程设计1.1 康复医院挂号和其他挂号有什么不一样先聊一个很多人会忽略的问题为什么题目里要强调“康复医院”如果把康复医院理解成普通综合医院的简化版后面做出来的系统大概率只是套模板答辩时经不起问。康复科的就诊模式和内科、外科差得很远。康复医疗的核心不是“开药”而是连续性的治疗过程。一个脑卒中患者可能需要连续六周、每周五次的物理治疗每次治疗由同一位治疗师按计划执行。也就是说患者挂号的目的是预约“治疗师的时间段”而不是简单约一个医生的问诊。这个差异直接决定了系统设计。普通门诊挂号医生是资源一个医生对应一个科室患者选医生、选日期就行。康复医院挂号核心资源变成了“排班时段”。同一治疗师一天内可以有多个治疗时段每个时段有严格的接诊上限而且一段治疗周期内患者大概率要重复约同一个治疗师。数据模型如果按普通挂号那一套来建后面做排班和预约时一定会卡壳。另外康复医院普遍存在多院区、多治疗区的情况。神经康复、骨伤康复、儿童康复、心肺康复这些亚专科往往分散在不同楼层甚至不同楼栋患者第一次来根本不知道挂哪个科。所以科室分类和医生擅长方向的展示比普通医院挂号更需要重视。1.2 角色、模块与业务流程一套完整的挂号系统最少要拆出三个角色患者、医生、管理员。如果项目还想要点亮点可以再加一个“治疗师”本质上和医生是同一类角色但在康复场景里可以细分为物理治疗师、作业治疗师、言语治疗师等。角色核心权限关注点患者注册登录、查询科室/医生、预约挂号、取消挂号、查看挂号记录操作简单、时间段直观医生/治疗师查看个人排班、查看预约患者列表、更新就诊状态排班与患者列表清晰管理员科室管理、医生信息管理、排班管理、挂号统计数据维护效率业务流程可以画成一条线患者登录 → 选择科室 → 选择医生 → 选择可预约的排班时段 → 提交挂号 → 系统锁定号源 → 生成待就诊记录 → 患者按时到院 → 医生确认就诊或爽约取消。在实现时预约状态我建议做成一个清晰的状态字段待就诊、已完成、已取消、爽约。不要只用一个布尔值后续统计和展示都会用到多状态。特别是“取消”这个动作一定要设计成可追溯的记录方便管理员处理纠纷。1.3 为什么最终选了 Django Vueflask 放在哪里这个问题几乎每次都会被问到。毕竟标题里同时出现了 Django 和 Flask很多人纠结到底用哪个。我的建议很直接核心业务用 Django。原因是挂号系统不是一个“单接口”项目它包含用户认证、后台管理、ORM 模型、数据迁移、定时统计这些恰恰是 Django 的强项。Django 自带的 Admin 后台几乎是白送的管理端管理员维护科室、排班可以直接用现成界面不用额外写一遍前端页面。ORM 的迁移机制也能在数据库结构变化时救命这在项目开发过程中非常常见。Flask 不是不好它是那种“轻量灵活”的框架适合做 API 原型、微服务或者几个接口就能搞定的小工具。但放到这个项目里用户系统、权限、跨域、分页、序列化这些都要自己拼工作量会多出不少。当然如果你的任务书明确写了 Flask那也完全可以做主体逻辑是一样的只是把 Django 的 ModelForm、DRF 换成 Flask-SQLAlchemy 和 Flask-RESTful。前端选 Vue 就没什么悬念了。Vue 的组件化开发非常适合挂号这种多步骤页面而且 Element Plus 组件库把表格、表单、日期选择器都封装好了能省大量样式时间。前后端分离开发后后端只要专心输出 JSON前端只管渲染页面联调效率很高。2. 数据库设计与后端 Django 实现2.1 环境准备用 PyCharm 快速创建项目环境部分不复杂但容易在版本兼容上翻车我直接给一套实践过的组合。我的开发环境是 Python 3.10、Django 4.2、djangorestframework 3.14、django-cors-headers 4.3前端用 Vue 3 Element Plus。在 PyCharm 里创建一个新项目目录然后配置虚拟环境python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install django djangorestframework django-cors-headers mysqlclient数据库我建议直接用 MySQL虽然开发时用 SQLite 能省事但交作业或者演示时老师们常问“你用的什么数据库”说 SQLite 显得太单薄。连接 MySQL 前记得建好数据库实例CREATE DATABASE rehab_hospital CHARACTER SET utf8mb4;接着创建 Django 项目和 appdjango-admin startproject rehab python manage.py startapp appointment把 appointment 注册到 settings.py 的 INSTALLED_APPS 里然后顺便把 REST_FRAMEWORK 的配置加上比如分页和认证方式。这里我多提一句用 DRF 默认的 SessionAuthentication 在前后端分离场景下会不好用建议直接上 JWT用 simplejwt 库接口保护省心很多。2.2 核心模型设计六张表打通挂号链路数据模型是整个项目的核心底盘。康复医院挂号系统我建议设计六张核心表用户表、科室表、医生表、排班表、挂号单表、就诊记录表。直接复用 Django 自带的 User 表来承载用户信息再建一个 Profile 扩展患者的手机号和病历号。医生表要单独建因为一个医生属于一个科室且医生信息里有职称、擅长方向、治疗师类型等字段和 User 一对一关联。排班表是最关键的一张表设计思路如下class Schedule(models.Model): PERIOD_CHOICES ( (morning, 上午), (afternoon, 下午), (evening, 晚间), ) doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE, verbose_name医生) work_date models.DateField(verbose_name出诊日期) period models.CharField(max_length20, choicesPERIOD_CHOICES, verbose_name时段) total_slots models.IntegerField(default20, verbose_name总号源数) booked_slots models.IntegerField(default0, verbose_name已预约数) class Meta: unique_together (doctor, work_date, period) verbose_name 医生排班注意这个unique_together(doctor, work_date, period)它保证了同一个医生在同一天同一时段只有一条排班记录这是防止数据重复的第一层保险。排班的日期和时段确定后号源总数和已预约数就构成了挂号系统的核心状态。挂号单表是第二核心class Appointment(models.Model): STATUS_CHOICES ( (pending, 待就诊), (completed, 已完成), (cancelled, 已取消), (missed, 爽约), ) patient models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name患者) schedule models.ForeignKey(Schedule, on_deletemodels.CASCADE, verbose_name排班) appointment_no models.CharField(max_length32, uniqueTrue, verbose_name挂号单号) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending, verbose_name状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name预约时间)appointment_no生成时可以用日期加随机数比如20250415001这样既保证了唯一性也方便按天统计。关于查询与删除要特别提醒一个点取消挂号时不要物理删除 Appointment 对象而是把状态改为cancelled否则后续做统计报表时数据会越来越少说不清来源。Django 的 ORM 虽然提供了delete()但在这个场景里“软取消”才是正确做法。整套模型跑迁移python manage.py makemigrations python manage.py migrate2.3 预约挂号的核心逻辑并发下的号源控制这是我整个项目里最有技术含量的一个部分也是真正值得放进“设计与实现”标题下的内容。先想一个并发场景某治疗师周五上午的号源还剩最后 1 个两个患者同时点击预约。如果代码按“先查剩余号源再判断是否充足最后写入预约记录”这个顺序写两个请求会同时查到剩余 1 个号接着同时通过判断最后两个人都挂号成功号源超卖了。要解决这个问题本质上是给查询加锁让两个并发请求排队执行。Django ORM 提供了select_for_update()配合事务可以锁定行数据from django.db import transaction from django.db.models import F transaction.atomic def create_appointment(request, schedule_id): schedule Schedule.objects.select_for_update().get(idschedule_id) if schedule.booked_slots schedule.total_slots: return JsonResponse({error: 号源已满}) appointment Appointment.objects.create( patientrequest.user, scheduleschedule, appointment_nogenerate_no(), statuspending ) schedule.booked_slots F(booked_slots) 1 schedule.save() return JsonResponse({appointment_no: appointment.appointment_no})select_for_update()的作用是在事务内锁住这条排班记录直到事务提交或回滚。第二个请求进来时必须等第一个事务结束再读取到已经更新过的booked_slots这样就不会出现超卖。这里还有一个细节schedule.booked_slots F(booked_slots) 1用了数据库层的原子操作而不是先取到 Python 变量再赋值。用F()表达式更新可以避免“读出来-算完-写回去”之间再次产生竞态属于 Django 里处理计数的标准做法。取消挂号时反过来要释放号源transaction.atomic def cancel_appointment(request, appointment_id): appointment Appointment.objects.select_for_update().get(idappointment_id) if appointment.status cancelled: return JsonResponse({error: 订单已取消}) appointment.status cancelled appointment.save() schedule Schedule.objects.select_for_update().get(idappointment.schedule_id) schedule.booked_slots F(booked_slots) - 1 schedule.save() return JsonResponse({message: 取消成功})如果不释放号源用户取消后这个时段就再也挂不进去了属于真实项目中很容易遗漏的 bug。我一开始就没做释放逻辑测试时发现取消后号源计数不对才补上这一层。2.4 接口与序列化DRF 的快速落地REST API 的设计口径要统一我全部按/api/前缀来组织。DRF 里只要写一个序列化器再套上视图集输出 JSON 的速度会非常快。这里以医生列表接口为例from rest_framework import serializers, viewsets class DoctorSerializer(serializers.ModelSerializer): department_name serializers.CharField(sourcedepartment.name, read_onlyTrue) class Meta: model Doctor fields [id, name, title, specialty, department_name, avatar] class DoctorViewSet(viewsets.ModelViewSet): queryset Doctor.objects.select_related(department).all() serializer_class DoctorSerializer配合select_related能在一开始就把关联科室查出来避免 N1 查询。这是面试或者性能优化常被问到的一个点我当时就因为没加这一步列表页打开时数据库查询数量翻了十几倍。核心接口清单整理如下接口方法作用/api/registerPOST患者注册/api/token/POST登录获取 JWT/api/departmentsGET科室列表/api/doctorsGET按科室过滤医生/api/schedulesGET查看排班与剩余号/api/appointmentsPOST预约挂号/api/appointments/myGET我的挂号记录/api/appointments/{id}/cancelPOST取消预约排班查询接口有一点要注意前端需要知道每个时段还剩多少个号。所以序列化时我会额外输出一个remaining字段等于total_slots - booked_slots并且过滤掉剩余的0的排班避免前端把没号的时段展示出来误导用户。3. 前端 Vue 页面开发从搭建到功能完成3.1 初始化 Vue 3 项目环境与依赖前端我用 Vite 脚手架从 0 开始创建项目npm create vuelatest创建过程中选择 Vue Router、Pinia 这些选项需要交互式选择按提示走就行。项目创建后安装额外依赖npm install element-plus axios piniaVue 项目初始化过程中最常遇到的坑就是 Node 版本太老Vite 4 起要求 Node 18 以上。如果你在npm run dev时看到报错提示版本不支持先确认node -v的输出版本而不是急着卸载重装。前端项目我推荐放在和后端平级的目录比如根目录下分backend/和frontend/方便管理也方便后期打包。目录结构如下rehab-hospital/ ├── backend/ # Django 项目 │ ├── rehab/ │ ├── appointment/ │ └── manage.py └── frontend/ # Vue 项目 ├── src/ ├── package.json └── vite.config.js3.2 页面骨架与路由规划挂号系统的页面不算多但路由设计要照顾不同角色的进入路径。我的路由表大致如下/login登录页/register注册页/首页科室选择/doctors医生列表/schedule排班时段选择/appointments我的挂号记录/admin管理员后台可选Vue Router 建议开启动态路由稍显复杂初学者直接用静态路由表加导航守卫完全够了。导航守卫里做一件核心事判断有没有 token没有就重定向到登录页否则放行。页面布局用 Element Plus 的el-container搭配侧边栏整体分成左侧菜单栏和右侧内容区。首页不要用太复杂的表格挂号的用户里有不少是老年人按钮要够大字号尽量放大流程越短越好。我就是在这个项目里突然理解了为什么很多医疗系统的界面要设计成大字号卡片操作门槛降低是有实际意义的。3.3 请求封装与登录态管理前后端分离后所有接口请求要统一走一个 Axios 实例方便统一处理 token 注入和错误提示。自定义src/utils/request.jsimport axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.error || 请求失败) return Promise.reject(error) } ) export default requesttoken 存localStorage是最简单的做法。虽然安全性不如 HttpOnly Cookie 方案但在课程设计或者中小型项目中是完全可以接受的。重点在于拦截器统一处理 401用户登录过期时能自动踢回登录页而不是在每个请求里重复判断。Pinia 里建一个 user store维护用户名、角色这些基础信息登录成功后写入刷新页面时再从 localStorage 恢复。这样侧边栏和页面顶部可以根据角色显示不同的入口。3.4 挂号三步走的页面实现挂号页我推荐做成“分步”形式每步对应一个组件通过一个currentStep变量控制显示。第一步选科室、第二步选医生、第三步选排班时段最后弹出确认框。科室列表从/api/departments拉取用卡片网格展示。医生列表根据选中的科室请求/api/doctors?department_idxxx。排班时段则是整个流程的核心请求返回的数据里要包含日期、上下午、剩余号数剩余号数为 0 的时段直接禁用不让用户点击。这里我顺便提一下 Vue 的插槽slot机制。科室卡片和医生卡片的结构高度相似可以把它们抽成一个DoctorCard组件在卡片底部用插槽插入不同的操作按钮比如“查看排班”“立即预约”这样代码复用度更高也符合组件化开发的思路。选中排班后展示一条确认信息医生名、职称、科室、出诊时间、就诊地点、挂号费。用户点击“确认挂号”后端返回挂号单号前端再跳转到“我的挂号”页面展示结果。整个流程不要让用户做多余选择能默认的尽量默认。4. 前后端联调、打包与上线部署4.1 跨域问题与开发调试前后端分离开发时Vue 开发服务器跑在 5173 端口Django 跑在 8000 端口两者之间天然存在跨域问题。处理方式有两种第一种是后端配置 CORSpip install django-cors-headers在 settings.py 中加入INSTALLED_APPS [ ..., corsheaders, ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]第二种是前端配置 Vite 代理这也是我推荐的方式。在vite.config.js里server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } }前端请求/api/...时Vite 开发服务器会把它转发给 Django浏览器端看着是同源的不再触发 CORS 机制。这种方式的好处是将来部署时直接把 Nginx 反代配上就行前端代码里不用改任何地址。4.2 前端打包并交给 Django开发联调没问题后把前端打包成静态文件npm run build构建产物会生成在frontend/dist/目录下。把dist目录里的文件复制到 Django 项目的静态目录中然后在 Django settings 里配置STATICFILES_DIRS [ BASE_DIR / static, ]再写一个视图把未匹配路由都指向index.html或者简单点直接让首页视图渲染index.html。这样 Django 就成了既能提供 API、又能托管前端静态文件的单一服务部署时只需要跑一个后端服务省掉一层配置。这种做法的好处是部署成本低一台服务器就够了。缺点是前后端耦合了不太利于后续把前端单独扩容但对毕业设计和个人项目来说完全够用。4.3 部署的最小方案与服务器准备如果你想把项目真正部署到云服务器上我给出一个最小可行方案。第一步服务器上装好 Python 3、MySQL 和 Nginx。第二步把后端代码传到服务器用虚拟环境安装依赖然后跑数据库迁移python manage.py migrate python manage.py collectstatic第三步用 gunicorn 启动 Djangogunicorn rehab.wsgi:application -b 0.0.0.0:8000第四步配置 Nginx 反向代理把所有请求转发给 gunicorn。注意location /api时要加上proxy_set_header Host $host;否则 Django 的 CSRF 校验可能出问题。这一步比很多人以为的要重要我部署时第一次访问接口一直报 403排查了半天才发现是代理头没传透。数据库记得定期备份用crontab定时执行mysqldump就行这个小习惯能避免很多后期数据丢失的麻烦。5. 实战中的常见问题与避坑清单5.1 我踩过的五个坑这个表格里的问题是我在实际开发中真实遇到并解决过的也是很多人做完项目最想分享的东西。问题现象根本原因解决办法取消挂号后号源没恢复只改了预约状态没减少排班已约数事务内同时更新排班表的 booked_slots两个用户同时抢最后一个号都成功查询和写入之间没有加锁select_for_update 行级锁前端拿到的时间比实际早8小时Django 时区设置导致时间偏移settings 里 TIME_ZONE、USE_TZ 保持一致排班时段重复出现没加唯一约束模型加 unique_together迁移建索引刷新页面 404Vue Router history 模式部署后路由无法匹配Nginx 配置 fallback 到 index.html时区那个问题尤其要拎出来说。Django 默认的TIME_ZONEUTC而中国用户用的是东八区时间前端展示时如果不做转换用户看到的就诊时间会差出 8 个小时直接影响挂号准确度。我建议在 settings.py 里设置TIME_ZONEAsia/Shanghai同时USE_TZFalse这样数据库存的就是本地时间省去前端转换的麻烦。如果你的项目要求国际多时区那是另一个复杂的话题但这个系统用本地时间最简单。5.2 调试效率提升技巧这个项目调试起来有两个工具我觉得特别值。第一个是 DRF 自带的 Browsable API。在浏览器直接访问http://localhost:8000/api/doctors会看到一个可视化接口页面上面可以直接填入参数发送 GET、POST 请求比在 Postman 里手动拼请求方便得多。很多刚上手 Django 的同学不知道这个功能其实是白捡的调试利器。第二个是 PyCharm 的断点调试。在接口视图函数里打上断点点击 Debug 按钮启动服务前端一次请求过来代码会停在断点处可以逐行查看变量。这种调试方式对理解请求生命周期非常有帮助比print()大法高到不知道哪里去了。另外提醒一点DRF 的接口报错信息默认是 HTML 格式不影响使用但是如果你希望前端能拿到结构化的错误信息可以配置统一异常处理或者直接看error.response.data里的内容。刚开始联调时前端弹出“请求失败”的 ElMessage 但不知道后端为什么失败我建议先打开浏览器开发者工具里的 Network 标签查看响应体内容很多时候一眼就能看出是字段名不一致还是序列化器校验失败。5.3 这套系统还能怎么扩展项目做到能跑通只是第一步真正让这套系统显得完整往往还要看扩展性。我个人觉得以下几个方向最值得加。第一种是支付消息闭环。给挂号单接入微信支付或者支付宝支付挂号完成后发送微信服务通知或者短信提醒。这一块可以调用第三方聚合支付接口或者云服务短信平台代码量不大但整个项目的“完整感”立刻提升一个层次。第二种是叫号屏。康复治疗区域需要排队叫号大屏幕上显示当前候诊和治疗进度。技术上很简单用一个 websocket 连接后端推送当前预约号状态前端大屏展示。这个功能对康复医院尤其适用因为治疗过程不是“问诊几分钟就结束”每个患者实际耗时较长叫号能有效减少大厅滞留。第三种是数据大屏。管理员后台加一个统计看板按科室展示挂号量、医生工作量、取消率、爽约率这些指标用 ECharts 做一个图表组件。这类功能对答辩演示的加分效果非常明显总比给老师看一张数据表格要直观得多。6. 写在最后做这个项目的过程中我最大的感受是技术本身并不难难点在于把业务流程搞清楚。把康复医院这件事从头到尾想明白数据表怎么建、字段怎么设、接口怎么出逻辑自己会浮现出来。反过来如果一上来就急着写代码到最后一定会反复改模型、改接口返工成本反而更高。如果你正打算做类似的挂号系统我建议你在写第一行代码之前先花一整天把下面几个问题想清楚患者是怎么挂号的医生怎么安排出诊时间如果两个人同时抢号怎么办取消预约之后号源怎么处理这些问题想清楚了剩下的就是按部就班往代码里填。项目做完之后记得保留一份操作手册和部署文档答辩演示时用得上后续自己接手维护也会有帮助。祝你的项目顺利跑通早日从“设计”走到“实现”。