资讯动态

Flask+Vue前后端分离医院管理系统开发实战:从数据库到接口联调

发布时间:2026/10/23 13:40:29 来源:尧图企业网站定制
最近刚把一个基于 Flask Vue 的前后端分离医院综合管理系统完整跑通从数据库建模到后端接口、前端页面、最后本地联调整个过程踩了不少坑。趁热把这次的实操过程整理出来给正在做同类项目、或者准备用 Python 方向做管理系统开发的朋友一个可直接参考的完整记录。说明一下文中的“安康医院”是我搭建这套演示系统时用的项目代号不指代任何特定机构功能设计上参考了通用医院业务流程即挂号、门诊、收费、药房、住院、统计这些主线模块。整个系统使用 Python 的 Flask 作为后端框架前端使用 Vue通过接口完成前后端数据交互数据库用的是 MySQL开发工具选了 PyCharm。1. 项目概况与功能拆解这套系统最初的需求来源很直白需要一套能覆盖医院日常运转主要环节的管理平台。医院业务不像普通的后台管理它天然有一条完整的业务闭环——患者先挂号挂号后进入门诊就诊医生问诊后开处方处方经过收费再到药房取药如果病情严重就办理住院住院期间产生医嘱和护理记录。任何一个环节断开整个系统就没法用。所以我在设计功能清单的时候没有贪多贪全而是紧紧围绕这条主线去展开。最终落地的模块包括这样几块系统管理用户、角色、权限、基础档案科室、医生、药品、患者服务挂号、病历、门诊业务接诊、处方、收费管理挂号费、药品费、住院费、药房管理库存、发药、住院管理入区、医嘱、出院、统计报表门诊量、收入、科室排行。1.1 核心需求为什么这么多很多第一次做管理系统的人会问为什么医院系统看起来功能特别多这不是为了凑数而是业务流程本身决定的。用一个最简单的场景来说。患者去挂号这个动作背后至少牵涉三张表患者表、挂号表、号源表。医生开了处方这个处方又牵涉到处方主表、处方明细表、药品表、库存表。药房发药要扣减库存同时产生一条发药记录。收费员收费又要关联挂号或处方生成收费流水。每一步都是一个实体每一步都是一组接口。功能多只是表象数据之间的关联才是核心。我整理需求时把它划分成三个层次基础数据层、业务操作层、统计分析层。基础数据层负责科室、医生、药品等静态数据的维护业务操作层承载挂号、就诊、收费这些动态流程统计分析层对业务数据做聚合查询。这样划分后即使模块很多开发顺序和接口设计也能保持清晰。1.2 角色权限设计医院管理系统有一个绕不开的点就是角色权限。不同身份的人能操作的功能完全不同。我的权限设计采用了基于角色的访问控制模型。系统里预置了这样几类角色系统管理员、挂号收费员、门诊医生、药房药师、住院部护士、药剂科主任以及数据查看用的院长只读账号。每个角色对应一组菜单权限和接口权限登录时后端根据角色信息动态返回该用户可访问的模块前端再根据这个结果渲染菜单。这个设计在实际开发中带来的好处是权限判断收敛在后端接口层前端只负责展示安全性可控。比如药房药师调库存接口没问题但如果他调收费接口后端会直接拦截并返回无权限提示。通过装饰器对每个接口做权限标识后续加功能、加角色都只是配置层面的事不会牵动全局逻辑。2. 技术选型解析很多人在做Python后端项目时会纠结一个点用Flask还是Django。标题里这两个词都出现过说明一开始做技术调研时对这两套框架都有过考虑。这里把我的选择和思考过程展开讲清楚。2.1 为什么选择Flask而不是DjangoFlask和Django都是Python Web开发的主流框架但设计哲学完全不一样。Django走的是“全家桶”路线自带ORM、Admin后台、表单校验、模板引擎、用户认证一个命令就能生成项目骨架。Flask则是微框架路线核心只保留路由和请求响应其他功能通过扩展库按需添加。我的选型逻辑是这样的这套系统采用前后端分离架构前端Vue负责页面渲染后端只提供JSON接口这种情况下Django自带的模板引擎和Admin后台基本用不上反而显得笨重。Flask足够轻量路由定义直观扩展库生态也完善需要ORM就装Flask-SQLAlchemy需要认证就装Flask-JWT-Extended需要跨域就装Flask-CORS按需组合灵活度高。当然这不是说Django不好。如果项目需要快速搭建后台内容管理又不想单独做前端那Django的Admin是极好的。但这次场景是前后端分离的系统管理平台我选择了更贴合需求的Flask。2.2 前端框架选择Vue的原因前端部分可选方案不少原生JavaScript、jQuery、Vue、React都能做。之所以选Vue核心原因是它的渐进式设计对中小型管理系统非常友好。Vue的核心优势在于组件化和响应式数据绑定。管理系统的页面大量重复出现表格、表单、弹窗、下拉选择这类交互模式组件化可以把它们封装成可复用的模块。比如我封装了一个通用的分页表格组件不同页面传入不同的列配置和请求方法整个列表页的开发时间能缩短一半以上。配合组件库使用开发效率还能再上一个台阶。我用的是当前社区比较活跃的UI组件库表格、表单、日期选择器、消息提示这些都有现成组件不用自己从零写样式。再加上Vue Router做路由管理、Pinia或Vuex做状态管理整个前端工程的组织结构是清晰明确的。2.3 PyCharm开发环境搭建开发工具我选了PyCharm主要是看中它对Python项目的完整支持。如果是学生或刚入门用社区版也能跑Flask项目但做这类完整系统遇到调试接口和数据库模型时专业版会更顺手。环境搭建有几个关键步骤。第一步是创建虚拟环境我习惯在PyCharm里新建项目时选择虚拟环境避免系统Python环境中安装的包互相污染。第二步是安装后端依赖用pip install把Flask、Flask-SQLAlchemy、Flask-JWT-Extended、Flask-CORS、PyMySQL这些包一次性装好。第三步是配置数据库连接在PyCharm的Database面板里连接到本地的MySQL实例方便直接查看表结构和调试SQL。这里有一个很重要的经验先把项目骨架跑通再开始写业务代码。我先是创建了一个最简单的Flask应用返回一个“hello world”接口然后在Vue前端调用这个接口确认前后端通路正常再开始填充各个业务模块。这个初始调试步骤虽然简单但能避免后期接口联调时搞不清楚是前端问题还是后端问题。3. 数据库设计与核心模型医院管理系统的复杂度从数据库表结构就能看出来。一个规范合理的数据库设计等于给整个项目打下了坚实的地基。谈需求时画业务流程图做的第一步却是把数据库表结构定下来因为所有接口的输入输出都围绕数据模型展开。3.1 表结构整体设计我把数据库表分成几个域来设计这样不至于一团乱麻。系统管理域包含用户表、角色表、用户角色关联表。基础档案域包含科室表、医生表、药品分类表、药品表。患者服务域包含患者表、挂号表。门诊业务域包含就诊记录表、处方主表、处方明细表。收费域包含收费项目表、收费记录表。药房域包含药品库存表、发药记录表。住院域包含住院登记表、医嘱表。表与表之间的关系需要重点理清。科室与医生是一对多一个科室可以有多个医生。患者与挂号是一对多一个患者可以有多次挂号记录。挂号与就诊是一对一一次挂号对应一次就诊记录。就诊与处方是一对多一次就诊可以开出多张处方。处方与处方明细是一对多一张处方包含若干条药品明细。药品与库存是一对一每个药品对应一条库存记录。画设计图的时候建议用ER图工具把主外键关系画出来哪怕是用简单的表格列出字段也行。这一步不要跳否则后续写模型和接口时会反复改表结构。3.2 核心表的字段定义与实现我用Flask-SQLAlchemy来定义数据库模型。以用户和科室两个表为例from datetime import datetime from werkzeug.security import generate_password_hash from extensions import db class User(db.Model): __tablename__ sys_user id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse, comment登录账号) password_hash db.Column(db.String(255), nullableFalse, comment密码哈希) real_name db.Column(db.String(50), nullableFalse, comment真实姓名) role_id db.Column(db.Integer, db.ForeignKey(sys_role.id), comment角色ID) status db.Column(db.SmallInteger, default1, comment状态1启用 0禁用) create_time db.Column(db.DateTime, defaultdatetime.now, comment创建时间) class Department(db.Model): __tablename__ base_department id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) dept_name db.Column(db.String(100), nullableFalse, comment科室名称) dept_code db.Column(db.String(20), uniqueTrue, nullableFalse, comment科室编码) intro db.Column(db.String(255), comment科室简介) status db.Column(db.SmallInteger, default1, comment状态)密码字段单独提一下这里存的是哈希值而不是明文。我用werkzeug.security里提供的哈希函数来处理密码存储即使数据库被泄露攻击者也拿不到原始密码。用户表里还有个细节就是create_time设置默认值为当前时间新用户创建时不需要在代码里手动赋值ORM会在插入数据时自动填充。3.3 模型关系配置与设计心得表之间的关联关系在ORM模型里通过关系属性体现。以医生表和挂号表为例做一个OneToMany关系的标准写法class Doctor(db.Model): __tablename__ base_doctor id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) doctor_name db.Column(db.String(50), nullableFalse) dept_id db.Column(db.Integer, db.ForeignKey(base_department.id)) title db.Column(db.String(50), comment职称) regist_fee db.Column(db.Numeric(10, 2), comment挂号费) class Registration(db.Model): __tablename__ reg_registration id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) patient_id db.Column(db.Integer, db.ForeignKey(pat_patient.id)) doctor_id db.Column(db.Integer, db.ForeignKey(base_doctor.id)) reg_time db.Column(db.DateTime, defaultdatetime.now) status db.Column(db.SmallInteger, default0, comment0待就诊 1就诊中 2已完成 3已退号)关于外键这里有一个实践心得ORM层面使用ForeignKey来建模但建表时一般不要被数据库级联删除之类的外键约束绑死。原因在于Web系统在删除数据时通常采用逻辑删除比如在表中加一个status字段标记失效而不是真正执行DELETE语句。如果数据库层面的外键约束设置得太严格反而会给后续的软删除带来麻烦。金额字段用一个重点经验数据库中用Numeric小数类型不要用FLOAT。金额计算如果涉及小数精度浮点数会出现0.1加0.2不等于0.3这种问题存钱、收费这种场景绝不能出现精度丢失。4. 后端API实现细节后端是整个系统的大脑所有业务规则、数据校验、权限控制都在这一层完成。Flask提供路由和请求处理我通过蓝图把不同业务模块拆开再配合JWT认证保护需要登录才能访问的接口。4.1 虚拟环境与依赖管理项目依赖并不是越多越好够用就行。我的requirements.txt内容如下Flask2.3.3 Flask-SQLAlchemy3.1.1 Flask-JWT-Extended4.5.3 Flask-CORS4.0.1 PyMySQL1.1.0 Werkzeug2.3.7 python-dotenv1.0.0环境管理强调一个细节一定要把Flask版本固定下来。同一个代码在Flask 2.x和Flask 3.x下部分API会有细微差异。比如从flask扩展库导入某些方法在版本升级后位置可能有变化不固定版本会导致换一台电脑就运行不起来。创建虚拟环境并安装依赖我习惯用一行命令python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install -r requirements.txt4.2 核心应用与蓝图目录结构Flask项目要做到可维护目录结构一定要清晰。我把应用工厂、扩展实例、蓝图模块、配置文件分层管理hospital/ ├── app.py # 应用入口 ├── config.py # 配置信息 ├── extensions.py # 扩展实例如db、jwt ├── models/ # 数据库模型 | ├── __init__.py | ├── system.py | ├── base.py | └── business.py ├── blueprints/ # 蓝图模块 | ├── __init__.py | ├── auth.py # 认证模块 | ├── doctor.py # 医生模块 | ├── registration.py # 挂号模块 | ├── pharmacy.py # 药房模块 | └── stats.py # 统计模块 ├── utils/ # 工具函数 └── requirements.txt应用入口的初始化代码大致这样from flask import Flask from flask_cors import CORS from extensions import db, jwt from blueprints.auth import auth_bp from blueprints.registration import reg_bp def create_app(): app Flask(__name__) app.config.from_pyfile(config.py) CORS(app, resources{r/api/*: {origins: *}}) db.init_app(app) jwt.init_app(app) app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(reg_bp, url_prefix/api/reg) return app使用蓝图直接粗暴地把所有路由堆在同一个文件里几百行代码会挤在一起排查一个接口问题要从头翻到尾既累又容易出错。按模块拆开之后找挂号接口我就只翻registration.py找药房接口就翻pharmacy.py定位问题的效率大幅提高。4.3 JWT登录认证实战身份认证采用的是JWT方案就是登录成功后后端签发一个带有效期的令牌前端在请求头中携带这个令牌访问受保护接口。这样做的好处是后端无需保存会话状态天然适合前后端分离架构。登录接口的核心逻辑auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) user User.query.filter_by(usernameusername).first() if user and check_password_hash(user.password_hash, password): token create_access_token(identityuser.id, expires_deltatimedelta(hours8)) return jsonify({ code: 200, msg: 登录成功, data: { token: token, username: user.username, realName: user.real_name, roleId: user.role_id } }) return jsonify({code: 401, msg: 账号或密码错误})受保护接口统一用装饰器控制from flask_jwt_extended import jwt_required, get_jwt_identity reg_bp.route(/list, methods[GET]) jwt_required() def registration_list(): current_user_id get_jwt_identity() # 继续业务逻辑有一个天然的坑需要提醒JWT默认的过期时间可能只有十几分钟实际使用中一定记得调长或者做刷新机制。我设置为8小时过期基本覆盖一个工作日的使用周期对管理系统来说是比较合理的配置。4.4 核心业务接口实现以挂号为例挂号接口是整个系统最微妙的业务之一。它不只是往挂号表插一条记录还涉及号源数量的校验与扣减。实现逻辑分三步走第一步根据医生ID查询该医生号源剩余量第二步判断剩余量是否大于0如果等于0则提示号源已满第三步创建挂号记录并扣减号源同时把操作结果返回前端。from sqlalchemy.exc import IntegrityError reg_bp.route(/create, methods[POST]) jwt_required() def create_registration(): data request.get_json() patient_id data.get(patientId) doctor_id data.get(doctorId) reg_date data.get(regDate) # 校验号源 doctor Doctor.query.get(doctor_id) if not doctor or doctor.remain 0: return jsonify({code: 400, msg: 号源已满}) reg Registration( patient_idpatient_id, doctor_iddoctor_id, reg_timedatetime.now() ) doctor.remain - 1 try: db.session.add(reg) db.session.commit() return jsonify({code: 200, msg: 挂号成功, data: {regId: reg.id}}) except IntegrityError: db.session.rollback() return jsonify({code: 500, msg: 挂号失败})挂号过程涉及新增记录和更新号源两个操作它们要么一起成功要么一起失败本质上属于事务操作。程序里用db.session.commit()统一提交当第二个操作出错时就会回滚保证数据不会出现挂号成功但号源没扣的脏数据情况。5. 前端Vue实现与接口联调后端接口准备妥当之后开始搭建Vue前端。这一层负责用户看到的页面、交互方式以及与后端的数据通信。生产环境里前端和后端往往部署在不同端口开发阶段则通过代理解决跨域问题。5.1 Vue项目结构与路由设计我用Vite创建了Vue项目结构如下frontend/ ├── src/ | ├── api/ # 接口请求封装 | ├── assets/ # 静态资源 | ├── components/ # 公共组件 | ├── router/ # 路由配置 | ├── stores/ # 全局状态 | ├── views/ # 页面组件 | ├── App.vue | └── main.js ├── vite.config.js └── package.json路由定义中最核心的是加入路由守卫import { createRouter, createWebHistory } from vue-router import { getToken } from ../utils/auth const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/MainLayout.vue), meta: { requiresAuth: true }, children: [ { path: dashboard, component: () import(../views/Dashboard.vue) }, { path: registration, component: () import(../views/Registration.vue) } ] } ] router.beforeEach((to, from, next) { if (to.meta.requiresAuth !getToken()) { next(/login) } else { next() } })前端路由守卫的意义在于用户如果没登录直接输入某个后端管理页面的地址会在进入页面渲染之前被拦截并跳转到登录页。它不承担安全责任真正的安全校验还是后端接口的JWT认证但这个体验层保护很有必要否则未登录用户会看到空白页面或接口报错。5.2 Axios请求封装与Token管理前后端交互我用Axios做HTTP请求同时做了一层封装。拦截器是这里的核心请求拦截器在发出请求前把Token加到请求头中响应拦截器统一处理返回的状态码比如401未登录就跳转登录页。import axios from axios import { getToken, removeToken } from ../utils/auth import router from ../router const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token getToken() if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { removeToken() router.push(/login) } return Promise.reject(error) } ) export default service这里有个细节后端JWT要的标准格式是“Bearer 空格 token”很多初学者只会把token字符串直接放进请求头导致每次都返回401。封装成拦截器后项目里所有接口调用自动携带Token不需要每个请求单独处理。5.3 核心页面实现挂号工作台挂号工作台是整个系统中操作频率最高的页面。页面功能不复杂但交互细节多左侧选择一个医生右侧填写患者信息提交时调用挂号接口。页面模板核心部分省略样式细节template div classregistration-page el-card el-form :modelform label-width100px el-form-item label患者姓名 el-input v-modelform.patientName placeholder请输入患者姓名 / /el-form-item el-form-item label联系电话 el-input v-modelform.phone placeholder请输入联系方式 / /el-form-item el-form-item label预约医生 el-select v-modelform.doctorId placeholder请选择医生 el-option v-foritem in doctorList :keyitem.id :labelitem.doctorName item.deptName :valueitem.id / /el-select /el-form-item el-form-item el-button typeprimary clicksubmitReg确认挂号/el-button /el-form-item /el-form /el-card /div /template接口调用部分async function submitReg() { const res await createRegistration({ patientName: form.patientName, phone: form.phone, doctorId: form.doctorId, regDate: form.regDate }) if (res.code 200) { ElMessage.success(挂号成功) } else { ElMessage.error(res.msg) } }表单校验是页面上值得注意的点。患者姓名和电话都不能为空电话号码还要做一个简单的格式校验否则脏数据进入挂号表后面在病历查询和统计报表里会暴露一堆问题。我用的做法是Element Plus自带的表单校验规则在el-form-item上添加rules属性。5.4 跨域问题与开发代理配置前后端联调时跨域是最常遇到的第一个拦路虎。浏览器同源策略限制下前端跑在5173端口后端跑在5000端口直接通过Axios调用后端接口必然报跨域错误。解决跨域有两条路一条是后端启用Flask-CORS允许指定来源访问接口另一条是前端在开发环境配置代理把/api路径的请求转发到后端地址绕开浏览器同源策略。我使用后端CORS加前端代理的组合方式。后端CORS配置代码如下CORS(app, resources{r/api/*: {origins: *}})前端vite.config.js配置代理export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这种配置下前端代码里请求路径写/api/auth/login实际上浏览器看到的是访问自身地址由开发服务器转发到localhost:5000。开发期非常方便等部署到生产环境再通过Nginx把同一个域名下的/api路径转发到后端服务即可前后端同源也不会触发跨域限制。6. 常见问题与排查技巧实录开发过程中踩过的坑可以整理出一张问题速查表其中不少问题非常典型很多做类似项目的人都会遇到。6.1 高频问题速查表问题现象可能原因解决办法前端请求接口报CORS错误后端未配置跨域或配置错误检查Flask-CORS配置明确允许的来源和路径后端返回的时间比实际晚8小时MySQL连接时区问题数据库连接串加上serverTimezone或统一使用Asia/Shanghai前端传入中文乱码请求未设置UTF-8编码后端解析JSON时确保Content-Type为application/json; charsetutf-8JWT登录后接口仍报401Token过期或格式不对在响应拦截器里抓取具体状态码检查请求头格式是否为Bearer开头数据库连接报Authentication错误MySQL 8.0默认认证插件问题使用caching_sha2_password时检查连接URL中的配置端口被占用多个Flask实例未关闭用命令行查看进程或启动时指定其他端口6.2 排错过程实录号源扣减异常挂号模块排错过程中有一次数据不一致问题印象很深。现象是一条挂号记录创建后查询该医生的号源余量却显示没有减少。排查过程让我收获很大。先检查挂号接口日志确认请求已经发出doctor.remain - 1这句代码确实执行了。再检查事务提交是否成功发现挂号记录写入后引起了唯一约束冲突触发IntegrityError导致回滚号源扣减也被一并回滚了。也就是说每次失败回滚后号源不减看起来像没扣减成功实际上是因为整个事务中止了。最终的解决办法是把号源扣减和挂号记录创建放在一个事务里失败时整体回滚保证两个操作的一致性。这也再次验证了一个老经验不要凭直觉判断数据不一致的原因要看事务边界看是否有回滚发生。6.3 开发过程中值得避开的坑第一个坑是密码明文存储。曾有小型系统开发阶段图省事直接在数据库里存明文密码后来为了演示安全性不得不返工。还不如一开始就使用哈希存储成本并不高写登录逻辑时多调用一个校验函数即可。第二个坑是药品库存扣减的并发问题。两个收费窗口同时给同一个药品发药时如果分别读取库存、分别扣减会出现超发。解决办法是给药品表加行级锁或者通过带有条件的UPDATE语句更新库存确保更新前后库存值匹配。第三个坑是过度设计。有人只用Flask做几个简单接口却把Redis缓存、Celery异步任务、RabbitMQ消息队列全堆上去结果项目维护成本直线上升。管理系统的主流场景是同步请求、数据清洗和报表查询先把基础功能老老实实做扎实比盲目引入一堆组件实用得多。这套系统还可以怎么扩展系统跑通之后我重新复盘了整个项目发现它还可以沿着以下几个方向继续扩展给有后续计划的朋友一点参考。一是接入预约挂号与患者报到机制。目前挂号记录创建后直接进入就诊状态实际医院场景中患者可能提前预约、到医院再取号这就需要额外的状态流转设计对服务端而言其实是把状态机做得更完整。二是增加药品效期管理和发药校验。药品表里已经有生产批号、有效期字段后续可以在发药接口加入临期药品拦截功能避免患者拿到即将过期的药。三是增加报表看板的图表呈现。目前统计接口返回的是数字数组前端直接渲染表格。再接一个图表组件库就能把门诊量趋势、科室收入占比等数据做成可视化卡片管理体验会提升很多。在我个人实际开发这套系统的体会中收益最大的不是某一个技术的深度学习而是对整个业务流程和数据关系的理解。医院管理系统功能多本质上是因为业务链长数据环环相扣把关系模型理清楚后后端接口规格自然就出来了。开发过程中养成的一个好习惯是每写一个模块前先在纸上画出它的数据流和异常分支比如挂号失败、库存不足、退号清退等写代码时大量问题被前置规避掉。最后再分享一个小技巧业务函数里所有涉及金额、库存、状态的操作都在提交前打印一条日志或者在前端页面上固定展示一次确认信息线上出问题时排查速度会快很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑