Flask + Vue 全栈实现医院预约挂号系统:从架构设计到并发控制
发布时间:2026/8/31 6:19:06 作者:尧图编辑部 阅读量:1,286

简介本资源是一套完整的基于Python Flask与Vue.js的医院预约挂号系统开发实践材料面向Web全栈初学者及医疗信息化项目开发者旨在解决传统挂号流程效率低、信息不透明等痛点提供可运行的前后端分离解决方案。压缩包共604个文件包含118个Vue组件文件支撑前端交互、63个JavaScript脚本实现业务逻辑、46个Python后端模块处理API与数据库操作、69张JPG/PNG素材与159个SVG图标完善界面呈现并附带SQL初始化脚本、多套批处理运行文件如init_sql.bat、run.bat及演示视频MP4整体大小为67.54MB。目前已有207人学习下载资源结构清晰涵盖从环境搭建、数据库初始化、前后端联调到功能演示全流程配套演示视频直观展示患者挂号、医生排班查看、预约管理等核心场景便于快速理解系统架构与调试要点。 搭过这套 Flask Vue 的医院预约挂号系统之后我最大的感受是这类题目看着像“课设标配”但真正动手做的时候难点全在表层功能之外的细节里。比如号源并发怎么处理、时间槽怎么划分、用户登录态怎么维持、前端路由刷新为什么会 404这些点如果没想明白代码能跑起来是一回事能经得起老师和面试官追问是另一回事。这篇文章我会从项目设计思路、后端核心实现、前端交互拆解到环境搭建和问题排查完整梳理一遍这套基于 Python 的 Flask-vue 医院预约挂号系统的实现路径。无论你是准备毕业设计、想练手全栈项目还是打算在简历上加一个完整的业务系统这篇文章都应该能帮上忙。1. 项目整体设计与需求拆解1.1 挂号系统到底在解决什么问题医院预约挂号系统的业务逻辑其实一点都不复杂核心就是让患者通过线上方式选择合适的科室、医生、时间完成预约。但它背后牵扯到的角色和状态转换比表面上看起来要复杂不少。先说角色。一个完整的预约挂号系统至少要包含三类用户患者普通用户、医生、管理员。患者需要注册登录、浏览科室医生信息、预约号源、查看或取消预约记录医生需要查看自己的排班和预约患者列表管理员则需要维护科室、医生信息、排班规则等基础数据。再梳理状态。一张预约单从创建到完成至少要经历“待就诊 / 已预约”和“已完成 / 已取消”等状态取消预约后对应号源必须释放回池子里否则会造成号源浪费。这个状态流转是整套系统数据设计的基础。从标题看这个项目用的是 Python Flask 做后端接口Vue 做前端页面属于典型的前后端分离架构。源码里通常还包括演示视频和数据库初始化脚本方便快速跑起来看效果。1.2 技术选型的逻辑为什么偏偏是 Flask Vue遇到类似项目很多人的第一反应是“为什么不用 Django为什么不用 Spring Boot”我做完之后认真想了一下这个组合其实是权衡了开发效率、学习成本和展示效果之后的合理选择。Flask 的定位是微框架。它不像 Django 那样自带 Admin 后台、ORM、表单、认证等全套设施而是把选择权留给开发者。对于预约挂号这类业务相对简单、表结构不算复杂的系统Flask 的灵活性反而成了优势——你可以用 SQLAlchemy 做 ORM用 JWT 做认证用蓝图(Blueprint)划分模块写起来比 Django 更轻代码结构也更清晰适合在答辩时把每个模块讲明白。Vue 的优势则在于渐进式框架的上手曲线。比起 React 的 JSX 和函数式编程思维Vue 的模板语法、响应式数据绑定对后端转前端的开发者更友好。而且 Vue 生态里的 Vue Router 和 Vuex / Pinia 都很成熟做页面路由和登录态管理几乎不需要额外的造轮子。还有一个很重要的现实因素网上可参考的资料多。Flask 和 Vue 各自的中文文档和示例项目都非常丰富遇到问题基本搜索一下就有答案这对需要独立完成项目的同学来说太重要了。1.3 项目功能模块的完整划分把需求拆解成功能模块通常可以分成三个端来看患者的操作流程是注册登录 → 首页浏览科室 → 选择科室查看医生列表 → 进入医生详情页查看排班 → 选择日期和号源时段 → 确认预约 → 在我的预约中查看或取消。医生的核心场景是查看自己的排班表、查看某个时间段的预约患者列表、标记患者为已完成。管理员的后台操作包括维护科室信息增删改查、维护医生信息所属科室、职称、简介、创建排班选定医生、日期、时间段、号源数量、查看所有预约记录。这三个端在数据库层面共用同一套用户表和科室表通过角色字段区分权限通过医生表、排班表、预约表关联业务数据。前端则通过路由权限控制让不同角色登录后看到不同的界面。2. 后端设计与核心实现细节2.1 数据库表设计六张表怎么建模最合理预约系统的数据模型是整个项目的根基。我自己建表时反复调整过几次最终沉淀下来的核心表结构大概是这样的用户表(user)id、username、password(哈希存储)、real_name、phone、role(patient / doctor / admin)、created_at。科室表(department)id、name、description、location。医生表(doctor)id、user_id(关联用户表)、department_id、title(职称)、intro、photo。排班表(schedule)id、doctor_id、work_date、start_time、end_time、total_count(总号源数)、booked_count(已预约数)、status。预约表(appointment)id、patient_id、doctor_id、schedule_id、appointment_date、time_slot、status(booked / completed / cancelled)、create_time。管理员表可以直接复用用户表用 role 字段区分不需要单独建表。为什么医生表要单独拆出来而不是直接把科室字段挂在用户表上因为一个医生可能同一天在不同科室出诊虽然业务上少见但数据建模时应该保留灵活性而且医生有职称、简介等用户表里没有的业务属性。拆出来之后后续扩展医生排班、医生评价等功能会容易得多。排班表里为什么要同时存 total_count 和 booked_count这是为了做预约时的可预约数量校验。每次预约请求进来后端要检查 booked_count 是否小于 total_count如果小于则允许预约并将 booked_count 加 1。这个字段设计是避免每次查询都去统计预约表提升性能的同时也简化了并发控制的逻辑。2.2 认证与权限JWT 方案的前因后果预约系统里有三类角色后端必须能识别当前登录用户是谁、有没有权限操作某个接口。常见方案有两种Session 和 JWT。Session 方案需要服务端存储会话适合传统服务端渲染的项目但前后端分离场景下处理跨域时需要额外配置凭证略显麻烦。JWT 方案把用户信息加密后签发一串 token前端每次请求带上 token 即可后端通过校验签名获取用户身份天然适合前后端分离。这个项目选 JWT 是合理的。核心实现可以分为三块登录接口收到用户名密码后校验用户是否存在、密码是否正确然后生成 token 返回前端。生成逻辑通常是对用户 ID、过期时间、密钥进行签名Python 里可以用 PyJWT 库import jwt import datetime def generate_token(user): payload { user_id: user.id, role: user.role, exp: datetime.datetime.utcnow() datetime.timedelta(days7) } token jwt.encode(payload, app.config[SECRET_KEY], algorithmHS256) return token前端拿到 token 后存到 localStorage 里后续每次请求在请求头加上Authorization: Bearer token。后端写一个装饰器统一校验from functools import wraps from flask import request, jsonify import jwt def login_required(f): wraps(f) def decorated(*args, **kwargs): token request.headers.get(Authorization, ).replace(Bearer , ) if not token: return jsonify({code: 401, msg: 未登录}), 401 try: payload jwt.decode(token, app.config[SECRET_KEY], algorithms[HS256]) request.user_id payload[user_id] request.user_role payload[role] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录已过期}), 401 except jwt.InvalidTokenError: return jsonify({code: 401, msg: 无效凭证}), 401 return f(*args, **kwargs) return decorated再写一个role_required装饰器包在login_required外层就可以实现医生接口只有医生角色能访问、管理员接口只有管理员能访问的权限控制。这里有一个容易踩的坑JWT 的密钥 SECRET_KEY 一定要放在配置项里不要硬编码在代码中。答辩时老师可能会问“如果 SECRET_KEY 泄露了怎么办”答出“可以通过环境变量或配置文件管理泄露后重新生成密钥让所有用户重新登录”会比较加分。2.3 号源并发处理怎么防止两个用户抢到同一个号这是预约系统里最有含金量的问题也是很多人容易忽略的。假设某个医生上午 10:00-10:30 的号源只剩 1 个患者 A 和患者 B 同时点预约。如果代码逻辑是“先查 booked_count再判断是否小于 total_count然后插入预约记录最后更新 booked_count”在高并发场景下可能出现两个请求同时查到 booked_count 9、total_count 10然后都判断“还可以预约”最终都把数据写进去导致超卖。解决思路有几个从简单到复杂排序第一数据库行锁。在创建预约记录之前用SELECT ... FOR UPDATE锁定排班表的这一行等事务提交后再释放。在 SQLAlchemy 中可以通过with_for_update()实现# 在事务中锁定排班记录 schedule Schedule.query.filter_by(idschedule_id).with_for_update().first() if schedule.booked_count schedule.total_count: return jsonify({code: 400, msg: 号源已满}) # 创建预约记录、更新 booked_count这样做能保证同时只有一个请求能进入“读取并修改 booked_count”的临界区避免超卖。第二乐观锁。不加锁但更新时带上版本号或条件更新result Schedule.query.filter( Schedule.id schedule_id, Schedule.booked_count Schedule.total_count ).update({ booked_count: Schedule.booked_count 1 }) if result 0: return jsonify({code: 400, msg: 号源已满})如果更新影响行数为 0说明此时号源已被占满预约失败。这种方式不需要显式加锁性能更好适合并发量不是特别大的场景。第三Redis 预扣库存。这是生产环境更常见的做法把所有号源提前写入 Redis预约时用 DECR 命令做原子扣减。但课设项目引入 Redis 会增加部署复杂度用数据库锁已经足够体现你对并发问题的理解了。我的建议是在项目里使用“事务 行锁”方案因为代码简单、好解释而且对 MySQL 来说实现很成熟。演示的时候可以写个并发脚本模拟两个请求同时抢最后一个号验证只有一个人能成功这一套配合起来讲会很出彩。2.4 核心接口清单与返回格式约定后端接口设计的好坏直接影响前端联调效率。建议统一返回格式{ code: 200, msg: success, data: {} }核心接口大约包括POST /api/auth/register注册POST /api/auth/login登录GET /api/departments科室列表GET /api/departments/id/doctors某科室下的医生列表GET /api/doctors/id医生详情GET /api/doctors/id/schedules?date2024-05-20某医生的某天排班POST /api/appointments创建预约GET /api/appointments/mine我的预约列表PUT /api/appointments/id/cancel取消预约GET /api/doctor/appointments医生查看自己的预约列表每个接口的参数校验、权限校验、异常处理都要做好。比如创建预约时除了校验号源是否充足还要校验预约日期是否在今天之后、同一患者是否已经预约过同一时段的号避免重复预约。3. 前端 Vue 部分页面结构与交互实现3.1 路由划分与权限控制Vue 前端第一件要做的事是划分路由。我的习惯是按照页面维度拆分然后通过路由守卫控制访问权限/login登录页/register注册页/首页科室列表/department/:id科室详情医生列表/doctor/:id医生详情排班与预约入口/appointments我的预约/admin管理后台科室/医生/排班管理权限控制通过 Vue Router 的全局前置守卫实现。在 router/index.js 中router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login || to.path /register) { next() } else if (!token) { next(/login) } else if (to.path.startsWith(/admin) role ! admin) { next(/) } else { next() } })这里需要注意 token 过期的情况。仅靠前端判断 localStorage 里有没有 token 是不够的因为 token 可能已过期。所以 axios 拦截器里要统一处理 401 响应发现过期就清除本地登录状态并跳转到登录页。3.2 Axios 封装与拦截器前端和后端是通过 HTTP 接口通信的封装 axios 是必备操作。我通常会在 src/utils/request.js 中统一封装import axios from axios import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) localStorage.removeItem(role) router.push(/login) } return Promise.reject(error) } )这样做的好处是前端所有页面代码里不需要重复写 token 拼接和错误提示逻辑。api 模块里只需写类似export const getDepartments () request.get(/departments)这样一行函数页面中直接调用即可。3.3 预约流程的交互设计页面交互是预约系统最容易做得好玩的部分。我实现的核心流程是首页展示科室卡片列表点击进入科室详情页。科室详情页以卡片或表格形式展示该科室的医生列表包含姓名、职称和简介。医生详情页展示医生完整信息下方是未来 7 天的排班日历或日期 tab。点击某一天的排班卡片会展开该医生当天剩余的时间段。每个时段显示剩余号源数无号则置灰。点击“预约”按钮弹出确认对话框展示医生、日期、时段信息确认后调用创建预约接口。成功后跳转到“我的预约”页面。整个链路看似简单但要注意几个交互细节号源为 0 时按钮必须 disable 或置灰避免用户点击后收到失败提示预约成功后要刷新排班数据把已预约时段标记为“已约满”取消预约后也要刷新释放号源。前端的“及时刷新”对数据一致性体验很重要。3.4 组件划分思路页面怎么拆组件Vue 开发中按组件拆分页面能显著提高代码复用性。我的做法是DoctorCard.vue医生卡片在科室详情页和医生列表都会用到。SchedulePicker.vue排班选择器展示某医生某天的排班时段。AppointmentModal.vue预约确认弹窗。StatusTag.vue预约状态标签自动区分“已预约 / 已完成 / 已取消”的颜色。组件拆分的原则是一个组件只干一件事组件通过 props 接收数据通过 emit 通知父组件状态变化。比如 SchedulePicker 接收 doctorId 和 date 作为 props内部自己去请求排班接口预约成功后 emit(booked) 让父组件刷新数据。这样各页面复用排班交互时只需引入组件不需要重复实现逻辑。4. 实操过程从零跑通这套系统4.1 环境准备与工具版本选择在动手之前先把环境准备好。我使用的一套稳定组合是Python 3.8 或 3.103.10 对 Flask 和 SQLAlchemy 支持良好Node.js 16 或以上MySQL 5.7 或 8.0Vue CLI 4/5或者 Vite 创建的项目也可以但要手动配置代理Flask 2.x Flask-SQLAlchemy Flask-CORS PyJWT这里不建议直接用最新版 Python 3.13因为部分依赖可能还没有对应的 wheel 包安装时容易踩坑。同样Vue 项目如果用 Vue CLI建议锁在 4.x/5.x生态最成熟。模拟数据要提前准备好。用 Faker 库可以快速生成一批患者和医生数据让演示效果看起来更真实。科室信息、医生的排班时间可以不写死在代码里而是通过 SQL 脚本或者后端初始化接口写入。4.2 后端启动步骤和一些配置细节拿到源码或自己创建项目之后后端启动流程一般是固定的创建虚拟环境python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate安装依赖pip install -r requirements.txt配置数据库连接。在 config.py 里设置 SQLALCHEMY_DATABASE_URI常见格式SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/hospital_db?charsetutf8mb4注意这里要带charsetutf8mb4否则存中文时在部分环境可能出现编码问题。初始化数据库flask db init flask db migrate flask db upgrade如果源码里提供了 init_db.sql也可以直接用source init_db.sql导入比手写迁移命令更快。不过我个人还是建议把 SQLAlchemy 的模型定义熟悉一下因为答辩时老师很可能会问迁移工具是怎么用的。启动服务flask run --host0.0.0.0 --port50004.3 前端启动与接口代理前端部分建议用 Vue CLI 创建项目后把源码中的 src 等目录替换进去。最重要的一个配置是 devServer 代理在 vue.config.js 中设置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }这样前端请求/api/xxx时会被代理到后端 5000 端口避免开发环境里跨域问题。有些项目前端直接请求http://localhost:5000并靠 Flask-CORS 解决跨域也能跑通但开发时用代理更干净接口地址也更灵活。启动命令npm install npm run serve浏览器访问http://localhost:8080看到登录页就说明前后端已经联通。4.4 演示视频里应该录什么源码附带的演示视频说白了就是项目的“门面”录得好不好直接影响验收。我的建议是在视频里覆盖以下 6 个环节用管理员账号登录展示后台管理界面。创建一个新的科室和医生账号。为医生添加某天的排班设置号源数量。退出管理员账号用患者账号登录。完整走一遍预约流程选择科室 → 选择医生 → 选择排班 → 确认预约。在我的预约里取消预约再回排班页面验证号源已释放。每个环节的操作速度不要太快关键数据变化比如号源从可约变为已满可以停留几秒方便答辩老师看清楚。5. 常见问题与排查技巧实录5.1 跨域问题接口联调时的头号障碍前后端分离开发中跨域问题几乎每个人都会遇到。表现是前端页面在浏览器里能打开但请求接口时报错Access to XMLHttpRequest at http://localhost:5000/api/login from origin http://localhost:8080 has been blocked by CORS policy。解决方案有两种。最省事的方式是后端加 Flask-CORSfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})但我在实际开发中更推荐用前端的 devServer 代理方案原因在上面已经提到代理方案在后端不用开放所有跨域来源部署到生产环境后更不容易出安全问题。5.2 数据库连接、中文乱码问题数据库连接失败是常见问题现象如下Access denied for user rootlocalhost用户名/密码错误或 MySQL 用户没有远程访问权限。Cant connect to MySQL server on localhostMySQL 服务没启动或者 3306 端口没监听。Table hospital_db.user doesnt exist数据库表未迁移成功需要重新执行迁移命令或导入 SQL 文件。中文乱码问题主要是表或字段的字符集不是 utf8mb4。解决方式是在创建数据库时指定CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接字符串里也带着charsetutf8mb4基本就能杜绝乱码。5.3 时间处理时区和格式的坑预约系统里时间字段特别多稍微处理不当就会出现“存进去的时间比显示的时间少了 8 小时”这类问题。原因通常是 MySQL 的时区与后端不一致或者 Python 的datetime.now()与datetime.utcnow()混用。我的做法是所有时间字段统一存本地时间Asia/Shanghai后端使用datetime.now()获取当前时间前端显示时用 JavaScript 的 Date 对象或 day.js 库格式化。如果使用 UTC 存储则前后端都要做时区转换徒增复杂度。前端提交预约日期时一定要保证传的日期格式是YYYY-MM-DD后端在接收时用datetime.strptime显式解析避免因为格式不一致导致查询不到排班。5.4 前端打包部署后的 404 问题很多同学开发环境跑得好好的一打包部署到 Nginx 上访问某个子路由就 404。这是因为 Vue Router 默认使用 history 模式刷新页面时浏览器会向服务器请求真实的路径而后端没有匹配到对应的路由。解决方案是 Nginx 配置里加上location / { try_files $uri $uri/ /index.html; }这样所有请求都会尝试回退到前端入口。如果项目是部署在一台服务器上的还可以用 Nginx 把/api反向代理到 Flask 服务实现单端口部署。5.5 源码文件常见异常提示速查表提示信息原因解决方案No module named flask依赖没装运行 pip install -r requirements.txtsqlalchemy.exc.OperationalError连不上数据库检查 MySQL 服务与账号密码jwt.exceptions.ExpiredSignatureErrortoken 过期重新登录SyntaxError: Non-ASCII character源码里存在中文编码问题在文件头部加# -*- coding: utf-8 -*-并检查文件编码Module Error: Cant resolve element-ui前端依赖没装齐重新执行 npm install6. 关于源码项目学习与二次开发的扩展建议这套系统跑通之后如果你不想止步于课设有几个方向值得继续打磨。第一个方向是增强业务完整度。目前的挂号系统偏“预约”本身但实际医院场景里还有取号、缴费、病历、问诊记录等环节。你可以试着加一个“取号”功能让患者预约后到院凭预约码取号或者加一个简单的支付模块预约时收 1 元挂号费模拟整个交易链路。这些功能能明显提升项目的业务成熟度。第二个方向是技术架构升级。当并发量变大时可以把排班的号源库存放到 Redis 中配合 RabbitMQ 或 Celery 做后端的异步短信通知登录认证可以从单点 JWT 升级为 Refresh Token Access Token 双令牌机制前后端通信可以增加 WebSocket 实时推送排队叫号信息。每一个点单独拿出来都可以作为答辩时的加分项。第三个方向是部署与运维优化。把 Flask 后端用 Gunicorn 启动前端打包后用 Nginx 托管数据库独立配置甚至可以写一套 Docker Compose 文件把 MySQL、Flask、Nginx 三个服务编排在一起做到docker-compose up -d一键启动。这个能力在真实工作中非常实用。我在实际开发中还有一个体会项目里写的每一个接口、每一张表都要能说清楚设计理由。比如“为什么 schedule 表要单独存 booked_count 而不直接 count 预约表”——“因为减少查询压力配合行锁做并发控制。”这种深度思考的小细节往往比堆功能更容易打动别人。如果你现在正准备启动自己的预约挂号项目建议先把表结构和核心流程吃透再动手写代码。代码写错了可以改但前期设计想不清楚后面改起来才是真的痛苦。希望这篇分享能帮你把这条路走得顺一点。本文还有配套的精品资源点击获取