ARTICLE DETAIL

资讯详情

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

基于Flask+Vue的医院挂号床位预约住院管理系统设计与实践

基于Flask+Vue的医院挂号床位预约住院管理系统设计与实践 1. 项目整体设计与技术选型先聊点实在的。医院挂号、床位预约、住院管理这类系统放在十年前多半是医院内部用JavaEE或者.NET那套在做工期长、交付重改个需求还要层层走流程。但现在中小型医院、私立诊所、甚至社区卫生服务中心都想要一套能快速上线、预算友好、既能管门诊又能管住院的系统。我这次用Python Flask Vue.js 来搭这套“医院挂号床位预约住院管理系统”核心思路就是后端轻量但够用前端交互现代整个项目跑起来不重维护起来也不至于让人头秃。为什么选 Flask 而不是 FastAPI不少朋友最近都在问这个问题。FastAPI 确实性能更好、自带自动生成OpenAPI文档在异步场景下有天然优势。但医院管理这类系统核心是事务处理和复杂业务逻辑不是高并发接口压测。Flask 的生态成熟、插件丰富、资料多团队上手成本低尤其对于手里只有一个Python基础、还没深入异步编程的开发者来说Flask 的同步请求模型更容易理解和调试。用 Flask 搭后台配合 SQLAlchemy 做ORM处理挂号、床位、住院这些关联数据代码写起来清晰也能解释得通。当然如果你预测未来有大量并发挂号抢号场景FastAPI 可能更合适但那就得接受团队学习成本和生态适配的代价。这个项目我选择 Flask也是对“够用、稳定、易维护”做个平衡。前端用 Vue 而不是传统模板或者 jQuery理由很简单挂号、床位选择、住院办理这些页面交互状态非常多。比如医生排班表、床位地图、住院进度条用模板字符串拼HTML会写吐而 Vue 的数据绑定和组件化能让状态管理清爽很多。Vue 3 的 Composition API 配合script setup写起来更紧凑项目里我用了 Vite 作为构建工具开发时热更新快得飞起打包后丢给 Nginx 也没有问题。这套系统到底能做什么一句话概括病人在小程序或网页端挂号医生端排班和接诊住院部管理床位预约和入院出院流程管理员维护科室、医生、床位的基础数据。三个角色对应三套界面后端同一套接口。项目的价值不在炫技而在于把医院线下那套“挂号—候诊—开单—办住院—分床”的流程原原本本搬到线上并且把床位这种最紧缺资源的状态管理清楚。1.1 系统功能需求拆分先别急着写代码做这类业务系统第一件事是跟真实业务场景的人聊清楚流程。我梳理下来核心模块大概分四块会员/患者管理患者注册、登录、基本信息维护、就诊历史查询。这里不需要做太复杂的账号体系手机号验证码登录就够了实在不行用户名密码也行关键是身份证号和手机号不能错。挂号管理支持按科室、医生、日期查排班选择时间段挂号支持取消挂号和退号。挂号费可以设计成在系统里记流水也可以对接真实支付网关但那个适合再接上先留好扩展字段。床位预约与住院管理病房和病床的基础数据维护支持按楼层/病区/床位状态空闲、占用、清洁中筛选预约床位后生成住院预登记记录。入院办理时要能关联挂号记录和就诊医生分配床位后状态要联动变迁。出院时释放床位缴费信息单独记账。后台管理账户权限管理患者、医生、护士、管理员、科室排班设置、床位管理、报表统计日挂号量、住院周转率等。管理员界面上要有基本的 CRUD不用花哨但数据关联不能乱。以上需求点拆出来后数据库的表结构也就跟着出来了。这里有个心得别一开始就设计一大堆表先按业务流程画画草图把主表患者、医生、科室、病床、挂号单、住院单之间的一对多、多对多关系理清楚再补充字典表比如性别、状态、医生职称也不迟。1.2 技术栈脉络与前后端交互设计这个项目技术栈很简单后端 Flask SQLAlchemy MySQLSQLite 开发时也行前端 Vue 3 Vite Element Plus Axios。前后端通过 RESTful API 通信JSON 格式传数据身份认证用 JWT。我选择 JWT 而不是 Flask-Login 的 Session 方案原因在于这个系统大概率会做小程序或 App 端JWT 天然支持跨域、无状态扩展。JWT 的 token 过期时间设置短一点比如2小时刷新 token 单独接口下发加上注意用户禁用列表安全性在内部系统里是够用的。另外配合 Flask-CORS 处理跨域开发时前端地址在localhost:5173后端接口在localhost:5000不处理 CORS 根本联调不起来。后端架构上我用了蓝图Blueprint做模块划分这也算是 Flask 项目的惯例了。比如auth蓝图管登录注册appointment蓝图管挂号bed蓝图管床位hospitalization蓝图管住院。每个蓝图有独立的url_prefix比如/api/auth、/api/appointment。这样文件多了以后不至于乱成一锅粥。配合应用工厂模式创建一个create_app()函数把扩展注册、蓝图注册、配置加载都放进去既方便测试也方便部署时切换环境。前端这边路由设计遵循页面角色区分。患者端挂在/patient下医生端挂在/doctor下管理端挂在/admin下。使用 Vue Router 的懒加载按需加载各模块页面组件避免首屏加载一堆无用代码。Axios 封装了一个请求实例统一处理 token 注入、错误码拦截、HTTP 401 跳登录。Element Plus 作为组件库直接省去了玩 CSS 基础组件的功夫表格、表单、弹窗、日期选择器开箱即用对业务后台开发效率提升非常明显。2. 数据库设计与后端核心接口实现既然要做医院管理系统数据设计的质量基本上决定了后续开发的体验。我见过太多项目后期改得焦头烂额原因就是前期表结构没想清楚关联关系混乱状态字段用字符串存得五花八门。所以这块我打算多花点篇幅把核心表结构和接口逻辑讲透方便大家直接拿去用。2.1 核心表结构设计思路与SQL语句先给一个核心表的清单再逐个说明字段设计意图。为了叙述方便我以 MySQL 为例用 SQLAlchemy 的 model 定义代码来演示。最核心的表有user用户表主键 idusername登录名password_hash密码哈希phone手机号id_card身份证号role角色patient/doctor/admin/nursestatus启用禁用状态。department科室表idnamedescription科室介绍parent_id用于二级科室可不要。doctor医生信息表iduser_id关联用户表name真实姓名title职称department_id所属科室intro简介。schedule排班表iddoctor_iddepartment_idwork_date出诊日期period上午/下午/晚上total_slots号源总数remain_slots剩余号源。appointment挂号记录表idpatient_iddoctor_idschedule_idappointment_datetime_slotstatus待就诊/已就诊/已取消/已退号order_number排队序号created_at。ward病区/病房表idnamefloor楼层department_id可选归属科室。bed病床表idward_idbed_no床位号bed_type普通床/ ICU床/ 抢救床status空闲/占用/预约/清洁price日床位费。hospitalization住院记录表idpatient_iddoctor_idbed_idadmission_datedischarge_datestatus预约/入院/住院中/出院diagnosis诊断pre_deposit预交金。registration_flow流水表iduser_idamounttype充值与消费created_atrelated_id关联单据id。观察这些表的关系医生属于科室是外键排班是医生和时间维度的交叉表挂号记录是患者与排班的关联住院记录一端关联患者、医生、床位。这样设计至少解决了以下问题一个患者多次住院记录都在一张表里床位一旦关联了住院记录便能从住院记录中反查历史不会出现床位被并发分配的问题。用 SQLAlchemy 定义模型时外键约束一定不要省。虽然写代码时可能觉得约束碍事但在业务层面能有效阻止脏数据进入。比如床位分配同一时间一张床只能有一条未出院或未取消的住院记录这可以在数据库层用UniqueConstraint对bed_id statusstate in active statuses做部分约束或者最简单的方式是在业务逻辑里加锁后续我会讲到。2.2 挂号与排班接口的实现细节排班和挂号是整套系统最精细的部分。排班接口设计时我先考虑两种使用情境管理员配置排班需要查询、新增、修改、删除患者端查询排班只看可预约的号源不能看到内部备注。所以接口拆分如下GET /api/schedule?department_id1date2025-06-01返回该科室当天所有医生的排班前端按上午/下午分组展示。POST /api/schedule管理员创建排班入参包括 doctor_id、date、period、total_slots。需要做校验同一个人同一天同一时段不能重复创建排班。这就是典型的在插入前先查询是否存在同条件的记录。还可以加一个检查日期不能早于今天避免补录历史的错误。POST /api/appointment患者挂号。这个接口需要做三个关键校验排班记录存在且 remain_slots 大于0同一个患者同一天同一时段不能重复挂号患者状态正常未处于黑名单或被封禁状态。挂号成功后的核心动作是扣减剩余号源并且生成挂号记录。为了保住并发情况下的正确性我直接在扣减操作上用了条件更新UPDATE schedule SET remain_slots remain_slots - 1 WHERE id ? AND remain_slots 0。在 SQLAlchemy 中执行这种条件更新如果返回影响行数为0说明号源已经没了就直接提示“该时段已满”。这一步简单但非常关键否则两个请求同时进来都读到 remain_slots 1最终两个都挂号成功但只剩1个号数据库层面就会出现超卖。2.3 床位预约与住院流程的闭环设计床位管理是整个系统里业务语义最重的模块。先定义状态机空闲 → 预约 → 占用 → 清洁中 → 空闲。由护士或医生操作床位管理员统一管理病区和床位信息。住院流程大致如下患者在门诊被医生建议住院医生端发起住院申请填写初步诊断和建议病房类型。住院部护士在系统里看到待入院的申请列表手动或自动选床。选床时只有status 空闲的床才可选。选择后立即把床位状态改为预约同时生成一条住院记录状态为预约。患者办理入院护士确认入住床位状态改为占用住院记录状态改为住院中同时更新患者状态。出院结算后护士操作出院床位状态改为清洁中住院记录状态改为出院清理工作完成后床位状态改为空闲。这里最需要注意的坑是床位状态和住院记录状态必须保持同步。一旦出现床位在“占用”状态而住院记录显示“出院”后面统计和调度全部乱套。所以我在代码层面尽量把状态变更的操作放进同一个事务函数里要么都成功要么都回滚。举个例子办理入院接口内部会同时更新bed.status和hospitalization.status中间任何一步出错都直接抛异常回滚不留给数据半成品的可能。床位选择页面在前端是一张“床位图”每个病区一个列表床号卡片展示状态色块点击空闲床会弹出预约弹窗。这块交互不难难的是数据实时性。因为可能存在多个护士同时操作同一病区光靠刷新页面不够。我在后端每次分配床位的接口里先使用SELECT ... FOR UPDATE锁住该床位记录再进行状态修改和住院记录插入。虽然 MySQL 的 InnoDB 支持行锁但在 SQLAlchemy 中使用with_for_update()可以确保事务内锁行有效避免并发冲突。3. Vue前端开发与核心模块实现前端这块是很多做后端的朋友最容易忽略却最容易翻车的地方。我在这里踩过不少坑后面也会单独列一节常见问题但基本实现思路还是得讲清楚。3.1 项目初始化与开发环境配置开发环境准备我默认是 Windows 或 macOS 主机上已经装了 Node 16。Vue 3 项目我通常用 Vite 初始化命令也很简单npm create vitelatest hospital-frontend -- --template vue cd hospital-frontend npm install npm run dev装依赖时有个细节Element Plus 和 Axios 要单独装npm install element-plus axios vue-router4Element Plus 在 Vue 3 项目里可以直接全量引入也可以按需引入。对于内部管理系统我更建议全量引入省去按需配置的时间毕竟打包体积不是首要矛盾。如果你实在在意首屏加载再按需引入也不迟。Vite 开发环境的代理配置非常重要。前后端联调时前端请求直接写相对路径/api然后让 Vite 代理到后端地址。在vite.config.js里配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这样前端代码里用axios.get(/api/schedule)开发环境会自动转发到 5000 端口不需要处理 CORS。等到打包后可以直接让 Nginx 把/api转发到 Flask 服务整体链路非常顺。3.2 登录认证与权限控制的落地写法前端登录页我没什么好讲的无非是表单、校验、调登录接口。真正需要设计的是 token 存储和路由守卫。先写一个工具模块utils/request.js封装 Axiosimport 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(hospital_token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(hospital_token) localStorage.removeItem(hospital_user) router.push(/login) } ElMessage.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } ) export default request在路由的beforeEach守卫里做角色权限控制router.beforeEach((to) { const token localStorage.getItem(hospital_token) const user JSON.parse(localStorage.getItem(hospital_user) || {}) if (to.meta.requiresAuth !token) { return { path: /login } } if (to.meta.role user.role ! to.meta.role) { return { path: /403 } } return true })这里to.meta.role和requiresAuth定义在路由配置里就行。比如管理端路由表这样写{ path: /admin, component: () import(../views/admin/AdminLayout.vue), meta: { role: admin }, children: [ { path: departments, component: () import(../views/admin/DepartmentManage.vue) }, { path: beds, component: () import(../views/admin/BedManage.vue) } ] }这样一个患者角色就算手动改 URL 也进不了管理端页面体验上完善很多。当然真正的安全还得靠后端的权限校验前端路由守卫只是涂了层“防君子不防小人”的皮。3.3 挂号页面与医生排班看板的实现患者端挂号页我要做到“三步走”第一步选科室第二步选医生/时间第三步确认挂号信息并提交。背后对应的组件分别是DepartmentList.vue、ScheduleList.vue、AppointmentConfirm.vue。状态管理我直接用 Vuex 或 Pinia 都行但简单场景其实组件间传参就够了。为了不引入额外复杂度我这里直接用了ref和props。ScheduleList.vue的核心逻辑是根据科室 ID 和日期调用接口拿到当天排班列表。每个排班长这样{ id: 12, doctor_name: 王医生, title: 副主任医师, department_name: 心内科, work_date: 2025-06-01, period: 上午, total_slots: 30, remain_slots: 5, consult_fee: 20 }展示时如果remain_slots为 0按钮要禁用并且显示“已约满”。这是一个最简单的用户体验细节但很多人一开始没做。别小看这一点线下的患者看不到实时号源线上系统要是约满还能点击提交后才报错那就非常消耗信任。候选人组件源码我简单贴一下核心模板部分template div classschedule-card v-foritem in schedules :keyitem.id div classdoc-info span classname{{ item.doctor_name }}/span span classtitle{{ item.title }}/span /div div classmeta{{ item.department_name }} · {{ item.period }}/div div classremain剩余号源: {{ item.remain_slots }}/div el-button typeprimary :disableditem.remain_slots 0 clickhandleAppointment(item) 预约/el-button /div /templatehandleAppointment里会打开一个确认弹窗显示患者信息、医生信息、挂号费然后调POST /api/appointment提交。成功之后提示“挂号成功”并跳转个人挂号列表页。3.4 床位预约与住院流程的前端状态联动管理端的床位管理页我采用“左侧病区列表 右侧床位卡片墙”的布局。病区列表用树形或普通列表点击病区后右侧刷新当前病区下所有床位。每个床位卡片展示床号、类型、价格、状态状态用不同颜色区分绿色空闲显示“预约”按钮黄色预约显示预约人、预约时间红色占用显示患者姓名和入院日期灰色清洁中显示清理开始时间点击空闲床的“预约”按钮弹窗需要填写患者 ID 或选择已登记的患者再填预计入院日期。提交后刷新床位列表就可以看到黄色状态。住院流程的办理页面我分成两步操作。第一步输入或选择患者 ID系统自动带出该患者的挂号记录和门诊诊断。第二步选择床位和入院日期确认后提交。所有数据校验都在后端做前端只做基本表单规则。在 Vue 开发中还有个小技巧对于床位状态的更新可以做一个轮询请求比如每 30 秒刷新一次当前病区床位数据或者利用 WebSocket 推送但内部系统用轮询足够了。轮询请求要注意不能在页面离开后还在跑我用onUnmounted清理定时器否则容易内存泄漏。4. 常见问题与排查技巧实录写代码最难得不是把功能跑通而是遇到问题时能有条不紊地定位和解决。我把自己在这个项目中踩过的坑、被同事问过的问题整理成一个小清单希望能帮大家节省排查时间。4.1 前端跨域与接口联调常见坑联调时最经典的问题就是浏览器报 CORS 错误。开发环境我建议直接用 Vite 代理前端所有请求走相对路径让代理把请求转发到后端这个问题就不会出现。但如果是正式部署后前后端域名不同就必须在后端开启 CORS。Flask 后端开启 CORS 的方法我用的是 Flask-CORS 库from flask_cors import CORS def create_app(): app Flask(__name__) CORS(app, supports_credentialsTrue, resources{r/api/*: {origins: *}})注意前后端分离场景下如果 Ajax 请求里启用了withCredentials传递 Cookie那origins就不能是*必须指定明确的域名否则浏览器会拒绝。用 JWT 的话其实不太依赖 Cookie把 token 放在请求头里就行。即便如此CORS 配置里我把supports_credentials设为True也让跨域请求携带自定义头变得顺滑一些。还有人在前端明明能打开页面请求却一直 404。先别怀疑后端大概率是 Vite 代理没生效或者 Flask 的url_prefix写错。你可以先打开浏览器开发者工具看看网络请求实际发到了哪个地址。如果请求地址是localhost:5173/api/schedule代理配置正确后端有响应如果显示localhost:5173/schedule那多半是前端 Axios baseURL 没设置成/api导致请求路径不对。4.2 数据库并发与状态不一致的实战处理医院系统中数字和状态错掉可不是闹着玩的。我遇到过这么一件事某个科室排班剩余号源白天还是 30晚上一看变成了 -1。原因很简单两个患者几乎同时提交了最后一个号的挂号请求。由于代码里先查了一下remain_slots 0第二次查询时看到的是旧值然后两个请求都执行了 update 操作减号源时没有带上remain_slots 0这个条件就变成负数了。修正的方法前文提过用条件更新schedule_update Schedule.query.filter_by( idschedule_id, remain_slots 0 ).update({ remain_slots: Schedule.remain_slots - 1 }) db.session.commit()如果schedule_update返回 0说明更新失败直接回滚并提示号源不足。这种方式比锁行更优雅也能避免事务持有时间过长。床位并发是这类系统最容易出事故的地方。两个护士同时给两个患者分配同一张空闲床如果没有任何保护就会有两个患者住进同一张床这一看就是重大医疗事故苗头。所以在办理入院接口里我明确使用了锁with db.session.begin(): bed Bed.query.filter_by(idbed_id).with_for_update().first() if bed.status ! 空闲: raise BusinessError(床位已不可用) bed.status 预约 hospitalization Hospitalization(...) db.session.add(hospitalization)这里注意with_for_update()必须放在一个事务内执行否则锁是无效的。SQLAlchemy 里通过db.session.begin()或干脆把整个操作写进函数并加上db.session.autocommit的协调方式处理。最终我选择了显式事务块逻辑清晰出了问题也好排查。还有一个状态不一致的坑预约床位后患者没来办理入院床位一直停在“预约”状态。后来我加了预约超时自动释放的功能在预约床位时记录expected_admission_date每天早上跑一个定时任务把已过期且状态为“预约”的床位清空并把对应住院记录置为“已取消”。这个功能虽小但对床位周转率帮助非常直接。4.3 Python 与 Vue 环境安装的踩坑记录这个项目所涉及的 Python 和 Vue 环境安装也算新人最容易卡住的点。我在这块给几个经验Python 建议直接去官网下载安装包别用系统自带的老版本。安装时记得勾选Add Python to PATH不然后续命令行执行python会提示找不到命令。装虚拟环境我用虚拟环境管理python -m venv venv source venv/bin/activate # mac/linux venv\Scripts\activate # windows pip install flask flask-sqlalchemy flask-cors flask-migrate pymysql如果你发现pip install网络很慢或失败可以换用国内镜像源但这不是什么隐蔽技巧就不展开说了。Vue 这边Node.js 版本建议装 LTS 版本不要用太新的奇数大版本避免兼容性问题。项目依赖安装失败时检查一下npm用的仓库源。另外把包管理器的 cache 清理一下再重新npm install很多小毛病都能解决。有一个比较常见的错误是Failed to load tsconfig vue/tsconfig/tsconfig.web.json这种情况下多半是 vue-tsc 和 vue/tsconfig 版本不匹配升级统一到最新版本再安装一次就好。还有一个容易被忽视的点如果想把 Vue 项目打包好后集成到 Spring Boot 或其他后端框架里建议把 Vue 的base路径改成相对路径。在 Vite 的vite.config.js里设置base: ./这样打包后的 JS、CSS 资源路径都是相对路径放进任何静态资源目录都能跑。我用 Nginx 部署时直接把dist目录指向 root再配置一个location /api的反向代理整个项目就能快速上线。部署 Flask 我用的是 gunicorn Nginx生产环境不会直接跑flask run那只是开发调试用的。先安装 gunicorn然后在项目根目录运行gunicorn -w 4 -b 127.0.0.1:5000 wsgi:app其中wsgi.py里写一句from app import create_app app create_app()Nginx 配置关键部分server { listen 80; server_name your_domain; location / { root /var/www/hospital-frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样整个系统就完整跑起来了。不过生产部署时还有几个点要补数据库连接池配置、日志输出、静态文件的缓存策略以及定期备份数据库毕竟医院数据丢不起。我个人在实际开发这套系统中最大的体会是技术本身都不是最难的事难的是把“业务约束”翻译成“代码约束”。挂号超卖、床位重复分配、状态不同步这些问题一旦在真实环境爆发轻则退款投诉重则影响医疗秩序。所以开发这种管理系统别急着把界面做得多么炫先把后端的事务边界、状态迁移、并发处理想清楚把数据库约束加扎实才是真正的竞争力。前端只要保证交互顺手、数据实时刷新就已经成功了一大半。如果你准备照着这个思路做一个类似系统建议优先从“挂号床位预约”这两个核心链路入手先跑通再扩展遇到状态错乱时也别慌理清事务和锁问题一定会浮出水面而且能被解决。
返回列表