ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+Vue医院后台管理系统设计与全栈实现

2026/9/30 9:07:21 拓冰建站 浏览量
SpringBoot+Vue医院后台管理系统设计与全栈实现 前阵子帮人从头搭了一版医院后台管理系统从数据库建模、后端接口、前端页面到最终部署全程走了一遍。做这类系统的最大感受是它看起来就是个“信息管理系统”但真把挂号、门诊、收费、药房、床位这些环节串起来之后涉及到的并发控制、事务一致性、权限分级、报表统计全都是后端开发里最实在的硬功夫。这套系统的技术选型是SpringBoot Vue前后端分离持久层用MyBatis数据库MySQL完整跑通后非常适合当成毕设、实习项目或者小团队内部自用系统的底子。这篇文章我把整个系统的设计思路、数据库表结构、核心代码逻辑、前后端联调流程和踩过的坑都拆开讲清楚需要完整源码做参考的可以直接按文里的步骤复现。1. 项目整体设计与思路拆解1.1 为什么是SpringBoot Vue而不是服务端渲染医院管理系统里有一种非常典型的业务特征角色多、页面多、操作流程长。同一个页面上可能既要显示挂号队列又要实时更新病床状态还要弹出收费窗口。这种交互密度下前后端分离的体验优势很明显——前端专注交互后端专注数据和业务规则两边各自演进互不拖累。SpringBoot在这套系统里承担的是标准的三层职责Controller层接收请求、Service层处理业务、Mapper层访问数据库。用SpringBoot最大的好处不是它“自动配置”多华丽而是团队协作时约定俗成的东西多任何人接手都能快速找到入口。MyBatis作为持久层方案在这类业务系统里尤其合适因为医院的查询场景特别复杂百名患者列表、按科室聚合统计、多表关联查询用XML手写SQL比JPA的自动生成的查询灵活得多。Vue这边的定位就是纯展示和交互。通过Vue Router做路由分发Vuex或Pinia管理登录态、角色信息、全局字典数据Axios负责和后端接口通信。项目分成两个独立工程前端打包成静态文件扔进Nginx后端打包成Jar包独立运行部署时互不干扰。这种选型在真实项目里还有一个隐形优势招人容易。SpringBoot和Vue几乎是国内Java后端和前端开发的标配就算团队里来了新人看代码的难度也远低于那些自研框架。1.2 角色权限模型怎么设计医院后台管理系统最忌讳的就是所有用户一把梭。医生能开医嘱但不应能改药品价格护士能录入体温但不应能看到财务流水管理员什么都能干但不应去操作挂号单。所以权限模型要提前设计好不然后面每一次接口上线都变成一场灾难。系统里我设计了四种核心角色管理员、医生、护士、收费员。后端权限控制采用RBAC模型就是“用户表—角色表—权限表”三层关系用户身上挂角色角色身上挂权限接口上通过拦截器校验当前用户具备哪些权限点。具体落地时登录接口会返回一个JSON里面带JWT令牌和用户基本信息、角色列表、权限列表。前端拿到权限列表后配合Vue Router的动态路由把当前用户没权限访问的菜单直接过滤掉。后端每个需要鉴权的接口都会写一个RequirePermission(patient:add)这样的自定义注解由拦截器统一处理而不是在Controller代码里到处写if判断。这样权限逻辑集中在一个地方维护增删权限点也方便。注意前端隐藏菜单只做体验优化真正的安全控制必须放在后端接口上。我见过不少项目只在前端做了菜单过滤结果有人绕过前端直接调接口数据就全漏了。安全底线一定留在服务端。1.3 功能模块边界划分系统按医院后台的日常业务拆成六个模块用户登录与权限管理、挂号管理、门诊医生工作站、收费管理、药房库存管理、系统统计报表。用户登录与权限模块管的是登录、验证码、修改密码、用户信息维护。挂号模块处理窗口挂号和退号记录挂号科室、医生、号别、就诊状态。门诊医生工作站是最核心的业务页面医生在这里查看候诊列表、书写病历、开检查单、开处方。收费模块对接处方和检查单处理收费和退费。药房库存关注药品入库、出库、库存预警。统计报表则从多个维度汇总数据比如科室接诊量、药品消耗排行、每日收入汇总。这些模块的边界在设计初期就要用接口文档和数据表划分清楚。模块之间尽量不要有隐形的数据库依赖比如收费模块需要知道处方信息那就定义prescription_id字段做外键关联而不是直接去查医生工作站的表。边界清晰了后面改需求就不会牵一发动全身。2. 数据库设计医院场景下的MySQL建模2.1 核心表结构怎么拆数据库是整个系统最不能偷懒的部分。表结构一旦建错后面写接口时就会发现各种别扭要么字段缺了要加列要么表之间关系不对要重跑数据。系统里最核心的表我列一下用户表、角色表、权限表、患者信息表、挂号表、病历表、处方表、处方明细表、药品表、收费记录表、退费记录表、科室表、病床表。以挂号表为例我最终定下的字段包括主键、患者ID、科室ID、医生ID、挂号类型、挂号费、就诊状态、挂号时间、操作员ID。这里的就诊状态是状态机WAITING待就诊、VISITING就诊中、FINISHED已完成、CANCELLED已退号。状态字段全部用字符串常量并且在后端Service层做状态流转校验避免出现“已完成还能退号”这种逻辑漏洞。处方表和处方明细表是一对多的关系。处方主表记患者、医生、开单时间、总金额、状态明细表记每一种药品的用量、频次、数量和单价。这么拆的好处是日后的统计报表好写也更贴近药房发药的业务流程——药房人员只关心某个处方需要发哪些药。2.2 索引设计给慢查询留好退路医院业务有一个特点就是时间维度的查询特别多。我今天接了多少个患者、本月每个科室收入多少这类查询都绕不开时间范围。如果不在时间字段上做索引数据量大了之后统计报表接口能把数据库CPU直接打满。我在三张表上做了重点索引挂号表的doctor_id create_time联合索引支撑医生工作台查今日待诊列表收费记录表的payment_status pay_time联合索引支撑财务统计药品表的stock单列索引支撑库存预警查询。建索引这事要克制不是每个字段都加就完事。索引越多写入性能越差。核心原则是先看业务查询条件再决定在哪些列上加索引一般单表索引不超过五六个宁缺毋滥。2.3 数据一致性挂号、收费、退费的事务处理医院系统里的钱和药容不得半点差错。最典型的是收费模块一次收费操作要更新收费记录表、更新处方状态、还要扣减药品库存。这三步只要有一个失败就会出现“钱收了但药没发”或者“药出了但钱没收”的脏数据。这里靠的是Spring的Transactional注解。入口方法是收费操作方法体内部做三件事插入收费记录、把处方状态改成已收费、扣减库存。一旦中间环节抛出运行时异常整个事务回滚数据库恢复到操作前的状态。需要注意的是Transactional默认只在RuntimeException下回滚如果是checked异常必须显式配置rollbackFor Exception.class。退费逻辑也一样。退费要校验原收费记录是否存在且已支付恢复药品库存并把收费记录状态更新为已退费。这个校验和状态更新必须在同一个事务里完成否则高并发下可能出现重复退费。3. 后端实现SpringBoot分层与MyBatis细节3.1 分层的标准姿势后端工程我按常见的三层结构组织controller、service、mapper。很多新手喜欢把业务逻辑全部写在Controller里图省事但系统一复杂就完了。打个比方Controller就是前台接待只负责收材料和递结果不应该具备“决定业务能不能办”的权限。我这里的代码习惯是Controller只做参数接收、参数校验的第一道关口、调用ServiceService层写所有业务规则和事务控制Mapper层只管SQL的增删改查。举个例子挂号这个操作看起来只是Insert一条挂号记录但实际业务是患者ID查不到要报错、选择的医生当天排班已满要报错、患者挂号次数超限要拦截。这一堆判断全在Service层完成Controller层的代码就变得特别干净。实体类用JavaBean风格定义字段和数据库列一一对应。VO视图对象和DTO数据传输对象按需创建比如患者列表页需要的PatientListVO是多个表拼出来的结果就不会去硬套单表实体。3.2 MyBatis映射文件里的实战技巧MyBatis在这套系统里最重要的部分是XML映射文件的SQL编写。我用三个技巧来节省开发时间。第一个是结果映射通用化。有些多表联查的结果集被多个接口复用我会抽出BaseResultMap然后通过extends扩展出各自需要的字段避免每个查询都重复写一遍列名映射。第二个是动态SQL的使用。患者列表页的筛选条件是医生传什么就查什么比如只看某个科室的、只看今天挂号的、还要模糊搜索患者姓名。这种场景用where加if标签组合实现动态条件拼接。注意不要在if里拼出WHERE 11MyBatis的where会自动处理首条条件前的AND写出来的SQL美观也安全。第三个是批处理操作。药房入库时可能是几十种药品一起录入如果逐条Insert会产生大量数据库往返。我直接用foreach标签拼成批量插入SQL一次请求把所有数据写入。实测导入500条药品数据从原来的几十秒降到一两秒。3.3 核心业务代码的要点实现挂号接口是系统里最容易被并发打穿的地方。同一时刻可能多个窗口在给同一个医生挂第50个号而号源上限是100。如果在Service层里写成“查出当前已挂号数判断小于100插入挂号记录”高并发下必然超号。解决方式是在SQL层做原子判断UPDATE doctor_schedule SET current_number current_number 1 WHERE id ? AND current_number total_number。这行SQL返回受影响行数如果为0就是号源满了直接向用户提示挂号失败。门诊医生的处方录入又是另一种逻辑。一张处方包含多条明细前端传来的是一个处方对象里嵌套着明细列表。前端会整整传入一个结构化的JSON后端在Service层通过Transactional把主表和明细表分多次插入。注意的是回滚逻辑同样要覆盖明细插入失败的情况。接口返回格式我统一封装了一个ApiResponse对象code、message、data三段式。这样前端拿到响应之后不用每个接口都单独判断自己的返回格式Axios拦截器里统一处理即可。4. 前端实现Vue工程与页面交互4.1 工程结构和路由设计前端工程我用Vue CLI初始化之后按模块拆分为views目录下的二级目录/views/register放挂号相关页面/views/doctor放医生工作台/views/pharmacy放药房页面/views/finance放收费和统计页面/views/system放用户和角色管理。路由设计上做了一层动态路由。静态路由只保留登录页和404页其余全部根据用户权限动态注册。登录之后前端从后端拿到的权限列表里过滤出当前角色能访问的路由然后通过router.addRoutes注入。这样做的好处是页面层级干净不同角色登录进去看到的侧边栏菜单天然不同。路由守卫必须要有。我在beforeEach里判断未登录直接跳登录页已登录但访问了无权限路由就提示并跳回首页。不过要再次强调前端的路由守卫只负责体验真正的数据安全还是后端说了算。4.2 Axios封装和请求拦截Axios在系统里要封装两个核心能力。第一是请求拦截每次请求发出前把存储在localStorage里的JWT令牌自动加到请求头。第二是响应拦截当后端返回code 401时说明令牌失效这时清理本地登录态、跳转登录页并提醒用户重新登录。响应拦截里同时要处理业务错误。后端返回的code不是200时直接在拦截器里弹出错误提示页面代码里就不需要每个接口都去处理失败分支代码量能少很多。注意区分HTTP状态码和业务状态码我这边HTTP请求返回一定都是200具体业务是否成功看body.code。4.3 核心页面的交互实现细节挂号页面上有个很常见的交互选了科室之后要联动加载该科室下的医生列表再选医生之后加载出排班号源。这里的联动逻辑如果每次都在组件内部写死fetch调用代码会很散。我习惯把所有关联数据请求收敛到一个store模块里在dispatch动作里串行发起依赖请求组件只拿结果渲染。医生工作台页面是整个系统最复杂的页面左侧是候诊患者列表中间是大病历编辑区右侧是常用药品选择区和检查单添加区。三个区域的数据是动态关联的点击左侧某个患者中间和右侧的内容全部切换。这个场景用Vue的computed结合vuex getter当前选中患者ID存store中间的病历内容和右侧的操作面板都通过患者ID计算渲染状态管理起来不会乱。药房的库存预警页加了简单的数字标记低于安全库存的药品行高亮显示。这类交互本身不复杂但是对使用体验提升很明显值得做。5. 完整实操从数据库初始化到前后端联调5.1 环境准备与版本选择环境这块是最容易踩坑的地方。后端我用的开发环境是JDK 1.8 SpringBoot 2.7.x这是国内大量生产系统的实际组合资料多、兼容性好第三方依赖也好找。MySQL用8.0版本注意MyBatis引入连接驱动时要选com.mysql.cj.jdbc.DriverMySQL 5.x的驱动配置已经不适用了。前端用Node.js 14版本和Vue CLI 4.xVue版本是2.6系生态成熟稳定。再说一下新版本的一个坑SpringBoot 2.7以后项目结构上没有大的变化但有些starter坐标的内部实现有调整网上搜到的老教程可能对不上。所以建议直接用主流稳定版本别追新。遇到版本问题先去官方迁移文档查不要凭记忆改配置。5.2 数据库初始化项目根目录放了一个sql/init.sql文件里面包含了建库、建表和插入测试数据。建表语句要保证可重复执行我的做法是在每个建表语句前加DROP TABLE IF EXISTS这样初始化时不容易报错。测试数据里造了十个科室、二十个医生和一百个模拟患者方便直接体验流程。初始化完成后需要修改application.yml里的数据库账号密码。如果本地MySQL的账号不是root或者有密码直接连不上这是最常见的启动失败原因。建议在application.yml中把密码用环境变量引用比如${DB_PASSWORD:root}这样换环境部署时不需要改代码只改环境变量。5.3 前后端联调的关键步骤联调阶段要先把后端接口跑起来启动类运行后访问http://localhost:8080/api/v1/health确认服务正常。前端开发服务器默认端口是8080但后端占用了8080所以要改Vue项目的vue.config.js端口为8081同时配置代理转发开发环境下所有/api前缀的请求转发到http://localhost:8080这样前端请求天然和后端同源绕开了开发环境的跨域问题。登录功能的联调优先级最高因为几乎所有页面都要先拿到Token才能请求数据。拿登录接口举例前端提交用户名和密码后端返回的响应体结构是{code:0, data:{token:xxx, userInfo:{...}}}。在src/utils/auth.js里封装好Token存取方法后马上在Axios拦截器里接入Token携带逻辑然后手动调试一次登录请求看到响应头带上了Authorization字段才算过。联调过程中我习惯用Postman先测后端接口测通了再用页面调。这样能把问题定位在代码还是网络层节省大量来回沟通时间。如果后端接口在Postman里返回正常前端页面请求却报错九成是代理配置、请求头或参数序列化的问题。5.4 部署发布部署我用了最经典的两段式方案。前端执行npm run build生成的dist目录拷到服务器的Nginx静态目录。后端执行mvn package -DskipTests生成的Jar包放到服务器目录通过nohup java -jar hospital-system.jar log.out 21 启动。Nginx配置上做了两个反向代理规则静态资源直接指向dist目录/api前缀的请求反向代理到http://127.0.0.1:8080。这种部署方式在前端页面刷新后路由不失效比打开history路由模式要配置更多的Nginx规则。前端的Vue Router我用了hash模式URL里带#不好看但省心刷新页面不会出现404适合小团队自用。6. 常见问题与排查技巧实录6.1 问题速查表把我在开发过程中遇到的高频问题整理成表格方便大家照着排查现象可能原因解决方案前端访问接口报跨域开发环境代理没配置或生产环境Nginx没转发检查vue.config.js的proxy和生产环境Nginx的proxy_pass登录请求返回401Token过期或请求头没携带Token检查Axios请求拦截器的Header设置和登录过期逻辑数据库连接失败application.yml账号密码不对或驱动配置错误确认MySQL连接串useSSLfalse驱动类用com.mysql.cj.jdbc.Driver批量插入报SQL语法错误foreach标签拼接时缺少分隔符或语句块不对检查foreach的separator和value括号是否完整事务没回滚方法内抛的是checked异常在Transactional上显式指定rollbackFor Exception.class前端页面404Vue Router模式选了historyNginx未做fallback改用hash模式或在Nginx加try_files $uri $uri/ /index.html新增字段后查询不到MyBatis结果映射没加对应字段XML的ResultMap和select的列名要同时补上6.2 排查思路遇到Bug先分清是前端还是后端问题。最有效的定位方式是看浏览器开发者工具的Network面板。请求如果都没发出去那是前端代码的问题看看是否有JS报错路由是否正确Axios拦截器哪里把请求拦了。请求发出去了但返回的状态码是500那直接看后端日志。SpringBoot的启动终端和项目里的logs/目录都是查堆栈异常的好地方。日志排查时重点看Caused by字段那才是异常的根本原因。比如一条日志里前面一堆MyBatisSystemException最后Caused by写着Unknown column xxx那就是SQL或者表结构不匹配直接去对应Mapper文件里修正即可。还有一个实战经验排查MyBatis相关问题时先在后端配置里打开控制台SQL日志打印在application.yml中配置mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl然后看控制台输出的完整预处理SQL和参数列表。绝大多数SQL问题一看日志就能定位省下大量瞎猜时间。6.3 性能优化的几点心得系统数据量到了几十万条时有几个优化收益很明显的点。第一是列表查询必须分页MyBatis分页插件用PageHelper即可但注意不要在循环里执行分页查询会产生严重性能问题。第二是统计报表接口要善用MySQL的GROUP BY和DATE_FORMAT做按天、按月汇总不要让Java层做大量内存计算。第三是医生工作台的患者列表查询要限制返回条数日常场景一天一个医生的患者一般不会超过几百个一次查全部纯属浪费。另外可以给高频只读数据加一层缓存比如科室列表、药品分类这类字典数据用SpringBoot自带的Cacheable注解就能实现。缓存能让这类读多写少的接口响应速度快上一个量级。注意改了字典数据之后要主动清理对应缓存否则用户明明更新了科室名称页面上却还显示旧值。根据我个人的实际经验这套系统做成之后最大的学习价值不是“会写几个CRUD接口”而是真正理解了业务规则如何落到代码和数据库上。从表结构设计到事务边界划分从前端状态管理到后端分步部署每一步都会遇到课本上不会写的真实问题。如果你拿到源码后想继续扩展建议从“微信小程序预约挂号”和“电子病历的PDF导出”这两个方向入手这两个需求在真实医院场景中需求量极大而且技术上都完全能基于现有系统做增量开发。