ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis医院后台管理系统:核心设计与部署实践

SpringBoot+Vue+MyBatis医院后台管理系统:核心设计与部署实践 这套“企业级医院后台管理系统”的话题我在技术群里见过太多次了。SpringBoot Vue MyBatis MySQL这套组合几乎是国内中小型企业内部系统、课程设计、毕业设计里最经典的配置医院后台管理系统就是其中一个非常有代表性的形态。你搜源码的时候会发现版本特别多有带前端的、有不带前端的、有Spring Cloud微服务的但真正在公司环境里跑得稳、能讲清楚设计思路的反而是这种单体应用加清晰分层的版本。这篇文章不打算贴一大段代码然后让你自己看而是从我和这类系统打了几年交道的角度把你拿到这套源码之后最需要搞懂的几件事拆开讲整个系统在解决什么业务问题、SpringBoot后端和Vue前端是怎么协作的、MyBatis在真实业务里的缓存和分页有哪些坑、以及最后怎么把它从源码变成一台能访问的服务器。不管你是把源码当成课设参考还是准备在公司内网部署一套医疗管理系统这篇文章都值得你花十分钟看完比你自己闷头翻代码有效得多。1. 一套医院后台系统到底在做什么1.1 模块拆解与业务复杂度分析医院后台管理系统听名字好像就是“挂号、开药、结账”三个功能但真正画出功能清单会发现它其实是一个典型的RBAC权限模型加多模块业务系统的组合体。以我接触过的同类项目来说最基础的功能模块通常包括系统管理用户、角色、菜单权限、基础数据科室、医生、诊室、药品字典、门急诊业务挂号、分诊、医生接诊、收费结算、住院管理床位、医嘱、费用、药房管理库存、发药、退药、统计报表日营收、就诊量、药品消耗。这些模块之间不是孤立的。挂号会关联科室和医生排班收费会关联挂号记录和药品明细药房库存又会受收费发药影响。一个看起来简单的“病人缴费”操作后端往往要同时更新收费主表、收费明细表、药品库存表、挂号状态表涉及至少四张表的数据一致性。这也是为什么这类系统特别适合用来练习事务管理和多表关联查询——业务复杂程度刚好不会像电商系统那样庞大到劝退又比简单的CRUD多出不少真实场景的约束。从架构形态上看医院后台管理系统用的是典型的B/S架构浏览器通过HTTP请求访问后端接口后端用SpringBoot提供RESTful API数据落在MySQL里。前端Vue负责页面渲染和交互通过axios和后端通信。这个模式的好处是前后端完全分离Vue打包成静态文件扔进NginxSpringBoot打包成jar独立运行部署和理解成本都很低对团队协作也很友好。1.2 为什么是SpringBoot Vue MyBatis这套组合先说SpringBoot。医院这类系统有一个特点业务规则多、迭代频繁、维护周期长。SpringBoot在这方面的价值是“少配置、快启动、生态全”内置Tomcat让部署变得极其简单配合Spring家族的AOP可以做日志切面、权限拦截、全局异常处理。相比老一代SSH或者纯Servlet开发开发效率提升得不是一星半点。Vue这边选它不是因为比React强而是因为国内前端生态里Vue的学习曲线更平缓Element UI这类组件库对后台管理系统几乎是量身定做的。“表单加表格”这种后台管理页面最常规的形态用Element UI的el-form和el-table拼接半天时间就能完成一个基础模块的页面开发。Vue的双向绑定和组件化思路也很直观有Java基础的人看几天Vue代码就能上手改页面。MyBatis在国内企业里的普及度非常高原因在于它把SQL的控制权完全交还给开发者。医院业务的查询逻辑很复杂比如“查询某医生在某时间段的挂号记录并带出患者信息和收费状态”这种SQL用MyBatis的resultMap和动态SQL写出来逻辑一目了然后期调优DBA也能直接看懂SQL。相比Hibernate的全自动ORM映射MyBatis的半自动模式在复杂查询场景下反而更可控。MySQL更不用多说开源免费、稳定可靠医院内部系统的数据量级对MySQL来说毫无压力。真正用好这套组合的关键不在于会选型而在于知道每个环节里的细节和坑在哪里。2. 后端核心实现数据库与MyBatis的实操细节2.1 数据库设计与常用表结构拿到源码后第一件事不要急着启动先打开SQL脚本看表结构。一套好的医院后台系统数据库设计通常能反映出业务的全貌。以挂号收费这条主链路为例最少需要这几张核心表用户表sys_user、角色表sys_role、菜单权限表sys_menu负责登录和权限科室表dept、医生表doctor、排班表schedule负责挂号资源管理患者表patient、挂号记录表register、收费记录表charge、收费明细表charge_item负责门诊核心业务药品表drug、库存表drug_stock、出入库记录表stock_log负责药房管理。设计这套表的时候有几个常见的取舍。患者表要不要和挂号表分开要分。同一个患者可能多次挂号如果字段全塞在挂号表里数据冗余会很严重。药品库存这种高频更新字段要不要存冗余库存数量要存但是要以库存流水表为准也就是说每次出入库都记流水当前库存是流水累计出来的结果直接改库存字段很容易对不上账。字段设计上还有一个细节值得注意金额字段用decimal而不是float或者double。这是医疗系统里最容易踩的坑涉及钱的计算如果用了浮点类型可能出现0.1加0.2不等于0.3的问题。我见过不少源码把费用字段设计成double这是绝对不可取的正确做法是decimal(10,2)起步复杂场景甚至用decimal(12,4)加前端展示转换。2.2 MyBatis映射、动态SQL与多表关联的常见写法看MyBatis部分的时候重点看三个东西resultMap、动态SQL、多表查询的写法。resultMap是MyBatis的核心它解决的是数据库字段和Java对象属性之间的映射关系。医院系统里下划线命名的字段特别多比如register_no、patient_name而Java里习惯驼峰命名比如registerNo。在配置文件里开启mapUnderscoreToCamelCase让下划线自动转驼峰可以省去大量手动映射的代码。但遇到关联查询查出多张表字段时column属性还是需要显式指定否则容易出现字段覆盖问题。动态SQL是MyBatis最实用的能力。比如查询挂号记录时管理员可能按患者姓名过滤也可能按时间范围过滤也可能按科室过滤。用原生JDBC写这种查询要拼接SQL字符串还要小心引号和特殊字符。MyBatis的where标签配合if标签可以优雅解决select idpageList resultTypecom.hospital.entity.Register SELECT r.*, p.patient_name, d.dept_name FROM register r LEFT JOIN patient p ON r.patient_id p.id LEFT JOIN dept d ON r.dept_id d.id where if testpatientName ! null and patientName ! AND p.patient_name LIKE CONCAT(%, #{patientName}, %) /if if testdeptId ! null AND r.dept_id #{deptId} /if if teststartDate ! null AND r.create_time gt; #{startDate} /if /where ORDER BY r.create_time DESC /select这里注意两个细节一是where标签会自动去掉多余的AND前缀所以写条件时带上AND也没问题二是时间比较时不要用直接写在XML里符号会被XML解析器认为是标签结束必须用gt;转义这是新手最容易踩的坑。多表关联查询的写法上医院系统里常见的是两层结构嵌套。比如查询收费明细需要带出药品名称用LEFT JOIN即可。但碰到“医生信息带出科室信息再带出门诊排班”这种三层嵌套直接用resultMap的association嵌套映射更合适比多层JOIN的性能好代码也更清晰。2.3 缓存与分页插件容易出问题的两个点MyBatis的缓存机制看起来简单实际用起来有不少坑。一级缓存是SqlSession级别的默认开启同一个SqlSession中执行两次完全相同的SQL会走缓存。Spring整合MyBatis后每次请求都会新建SqlSession所以一级缓存的作用范围基本被限制在一个事务内问题不大。真正的问题是二级缓存它是mapper级别的默认关闭。有人觉得开启二级缓存能提升性能但在医院这种读写频繁、数据实时性要求高的业务里开启二级缓存很容易出现“上一个请求改了数据下一个请求却读到旧值”的脏读问题。我的建议是这个系统默认不开二级缓存维持现状就好真有性能瓶颈时优先优化SQL和加索引别一开始就动缓存。分页插件是MyBatis生态里使用率最高的组件PageHelper几乎成了标配。用法很简单PageHelper.startPage(pageNum, pageSize); ListUser list userMapper.selectUserList(); PageInfoUser pageInfo new PageInfo(list);但这东西有个非常经典的坑PageHelper.startPage()调用后必须紧跟第一个select查询中间不能有任何其他SQL操作否则分页参数会被下一个查询消费导致莫名其妙的数据错乱。另一个坑是pageNum从1开始不是0。前端Vue里el-pagination默认也是从1开始两边对齐问题不大。真遇到“前端传1查不出第一页”的情况多半是后端有人写了pageNum pageNum - 1找出来删掉就好。3. 前端Vue部分的工程化实践3.1 工程初始化和目录结构Vue项目这块拿到源码后先看package.json搞清楚用的是Vue 2还是Vue 3、用的是Element UI还是Element Plus、构建工具是webpack还是Vite。医院后台系统开源的代码目前大量还是Vue 2 Element UI Vue CLI的组合这并不代表落后这类系统要的是稳定Vue 2的生态非常成熟踩坑资料也多。目录结构上一套合理的Vue项目通常长这样src下面分views页面组件、router路由配置、api接口请求、components公共组件、utils工具函数、storeVuex状态管理如果用了的话。其中最核心的是views和api的对应关系。后端一个模块对应前端一个页面这个对应关系理清了你改代码的时候才能快速定位。有些源码会把所有接口请求直接写在页面组件里每个页面重复写axios调用。这种写法不是不行后期维护会很痛苦。更规范的做法是单独建一个api目录每个后端Controller对应一个JS文件把接口统一封装成方法页面里只关心调用和回显。3.2 axios封装与登录态管理axios封装是前端工程质量的分水岭。好的封装会做三件事统一请求头、统一错误处理、统一携带token。以这个医院系统为例用户登录成功后后端返回一个JWT令牌前端存储在本地然后axios请求拦截器里自动加上Authorization请求头。service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; });响应拦截器要处理的是后端返回的统一响应体。这套系统的后端接口一般会封装Result对象有code、msg、data三个字段。当code不为200时前端拦截器直接弹出错误提示不需要每个页面试着catch一遍。还有一个细节是处理登录过期当拦截器捕获到401状态码时清除本地token并跳转到登录页这是很多源码容易漏掉的地方。登录态管理这一块token存localStorage还是sessionStorage各有说法。localStorage刷新后还在适合“记住我”的场景但存在XSS风险。sessionStorage关闭标签页就没了更安全一点。医院内部的系统安全要求通常高一些建议用sessionStorage并且后端接口要校验token的有效期。3.3 动态菜单和路由权限控制权限控制是后台管理系统里不好绕过去的点。医院系统的角色分为超级管理员、医生、护士、收费员等多种不同角色看到的菜单和能访问的页面不一样。前端实现权限控制有几种方案最常见的做法是登录后从后端获取当前用户的菜单列表和权限标识前端根据这些数据动态生成侧边栏菜单。路由权限这块可以用vue-router的addRoutes方法动态注册路由也可以只做页面级按钮的v-if控制。实际项目里我更推荐一个折中方案路由表写全部页面但配合导航守卫做角色校验用户访问没有权限的路由时直接重定向到首页。这种方式配置简单不容易出错安全性以后端接口校验兜底。因为前端菜单隐藏只是用户体验层面的事真正的权限拦截必须在后端接口上做否则有人绕过前端直接请求接口就能越权操作这是医院系统绝对不能犯的错误。后端权限这块我在这类项目里一般用拦截器配合注解实现。自定义一个RequirePermission注解标注在Controller方法上拦截器里读取当前用户的角色和权限集合做校验不通过就抛出401异常。这种方案比Spring Security简单也够用。4. 业务模块实现挂号、收费、药房这些典型场景怎么落地4.1 挂号与医生排班模块的实现套路挂号是医院系统的入口业务它牵扯到患者建档、医生排班、号源锁定三个环节。患者第一次来要先建档填写姓名、身份证、手机号等信息生成一个患者ID。再来就诊时输入手机号或身份证号就能调出档案。这个逻辑在后端对应的是一个“查不到就新增”的操作用SELECT判断后再INSERT很容易出现并发重复建卡的问题更稳妥的做法是在患者表身份证号字段上建唯一索引用INSERT ... ON DUPLICATE KEY UPDATE来保证数据唯一。医生排班的实现通常是排班表加时间段规则。医生表关联排班表排班表里存上午、下午两个时段每个时段限定号源总数。挂号时先查询该时段剩余号源判断是否大于0然后执行扣减。这个“查剩余号源再扣减”的操作如果并发量上来需要加锁或者用乐观锁控制否则两个患者同时挂最后一个号源可能导致超卖。不少简单源码没有处理这个问题考试交作业没问题真要上线做改造这是必须补的一环。挂号成功后系统要生成挂号记录并关联门诊收费状态。状态一般分“未收费”“已收费”“已退号”几种。退号操作要检查是否已经发生收费和药品发放如果医生已经开了药是不允许直接退号的需要走退费流程。这些状态流转的校验代码是业务逻辑里最需要细心看的部分。4.2 收费结算模块的事务处理收费结算是整个系统里数据一致性要求最高的模块。一笔门诊收费涉及的操作包括更新挂号记录状态为已收费、插入收费主表记录、插入收费明细记录、扣减药品库存、生成药品出库流水。任何一个步骤失败整个操作都要回滚否则会出现“钱扣了但药房库存没扣”的严重业务Bug。SpringBoot里用Transactional注解解决这个问题但要注意几个使用细节。注解默认在RuntimeException时回滚如果代码里捕获了异常然后自己做处理事务不会回滚。正确做法是只捕获异常、做好日志、然后继续向上抛出让Spring帮忙回滚。另外不要在一个事务里调用另一个类中被Transactional标注的方法Spring的事务是基于代理实现的自调用会绕过代理导致事务失效。这个是非常经典的“事务不生效”的场景。收费金额的计算这里也要强调一定要用BigDecimal。药品单价乘以数量的结果如果用double计算肉眼看着没问题但有时候会出现小数点后的误差。对账的时候差几分钱查到你头秃也找不到原因。BigDecimal的写法BigDecimal totalAmount price.multiply(new BigDecimal(quantity)) .setScale(2, RoundingMode.HALF_UP);4.3 药房库存和统计报表药房模块的核心是库存流转闭环。入库增加库存出库扣减库存每一步操作都要写流水表。库存不能自由修改只能通过出入库操作来调整。盘点时发现库存不对排查的依据就是流水表。这套系统里的药品表、库存表、流水表是三个关联紧密的表理解的时候要当成一个整体来看。统计报表这块要用到MySQL的日期函数和分组查询。日报表就是按天统计收费金额和挂号数SQL大概长这样SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS register_count, SUM(total_amount) AS total_amount FROM charge WHERE create_time #{startDate} AND create_time #{endDate} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d)这类SQL本身不复杂但报表查询通常会跨多张表联合统计不加索引的话数据量大时会非常慢。常见的优化手段是保证where条件里的时间字段建了索引group by字段也建了索引。前端报表页面用ECharts折线图展示每日营收趋势这一块在源码里通常是Dashboard页面不算太复杂理清SQL结果怎么映射到图表数据的思路就够了。5. 部署上线从源码到能访问的完整流程5.1 环境准备MySQL、JDK、Node这些基础依赖在IDE里跑通这个系统不难难的是把源码部署到一台干净的服务器上。先说环境这系统跑起来需要三样东西MySQL、JDK、Node构建前端用的。MySQL安装可以参考网上任何一个安装教程但要注意版本。这套源码的SQL脚本如果是基于MySQL 5.7写的放在MySQL 8.0上执行大概率会有兼容问题最常见的报错是排序规则不兼容或者datetime默认值语法变了。所以我一般建议直接用MySQL 8.0少数SQL脚本需要手工改一下。安装完MySQL后用Navicat新建数据库字符集选utf8mb4排序规则选utf8mb4_general_ci然后导入SQL脚本。导入SQL脚本有一个经常出问题的点如果SQL脚本文件是GBK编码在utf8mb4的库上导入会出现中文乱码。用Navicat导入时要注意文件编码和数据库编码保持一致。如果不确定用记事本打开SQL文件另存为UTF-8编码再导入基本不会错。JDK方面SpringBoot 2.x系列要求JDK 8以上我用的是JDK 8稳定省心。如果你拿到的源码是基于SpringBoot 3.x写的那就要JDK 17了两者差距很大先确认版本再装环境。5.2 前后端打包与服务器部署后端打包在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个jar包。这一步如果报错大概率是依赖下载失败或者测试用例没跳过。还有一种情况是MySQL连接配置的问题打包本身不会校验数据库连接但运行时启动就会报错所以application.yml里的数据库地址、用户名、密码要在打包前就确认好。SpringBoot有两种部署方式直接用java -jar跑或者部署到Tomcat的webapps目录。用内置Tomcat的方式更简单一条命令就能启动。但要注意服务器防火墙要放行项目端口SpringBoot默认是8080如果外网访问不了先检查防火墙和安全组规则。前端的构建很简单npm install npm run build构建完成后dist目录就是纯静态文件。部署方式通常是把dist目录上传到Nginx的html目录里然后配置Nginx把请求转发到后端接口。这里有个很关键的地方前端和后端不在同一个端口跨域问题是怎么处理的。比较省事的方式是在Nginx里配置反向代理让前端页面请求/api开头的接口时统一转发到后端的8080端口location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样配置的好处是前端代码里只需要写相对路径不用写死IP和端口后续换服务器或者改端口不用重打前端包只需要改Nginx配置就行。6. 实际开发中踩过的坑与排查思路6.1 分页插件失效的几个常见场景PageHelper失效是个容易把人搞懵的问题。表现是查询结果没有被分页一条SQL把全表数据都查出来了。常见的坑有三个第一个是startPage之后没有紧跟查询中间隔了别的逻辑第二个是在分页查询的SQL里有自定义拦截器或者嵌套子查询PageHelper的count查询生成出错第三个是多个数据源场景下PageHelper配置没有指定方言或者没有拦截到正确的Executor导致分页不生效。排查思路很简单先看MyBatis打印出来的SQL如果SQL最后没有LIMIT关键词说明分页根本没拼接上去顺着这个方向去查startPage和查询之间的代码就行。6.2 MySQL 8的时区与编码问题MySQL 8连接时区报错是出现频率最高的问题。错误信息通常是“The server time zone value is unrecognized”或者连接直接超时。解决办法是在JDBC连接串里加时区参数url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai注意useUnicode和characterEncoding这两个参数一起用不然中文会出现乱码。另外符号在YAML文件里不需要转义在properties文件里也不要写成amp;这是很多人会搞混的地方。编码问题还有一个隐蔽场景数据库表字段是utf8mb4但JDBC连接串没指定characterEncodingutf8插入中文数据时会显示问号或者乱码表现是“能连接、能查询、但中文全变问号”。遇到这个问题先检查连接串再加验证数据库和表两级的编码设置。6.3 跨域配置与前端上线后的404场景前端开发环境访问不到后端接口最常见的问题是跨域。开发阶段Vue的devServer可以配置代理解决生产阶段用前面说的Nginx反向代理解决。还有一种偷懒方式是后端写一个CorsConfig配置类放开所有跨域限制。本地联调这么搞可以但上线前一定要做限制只允许固定域名访问否则等于把系统接口裸奔在公网上完全不具备企业级系统应有的安全底线。上线后遇到刷新页面404是另一个高频问题。原因是前端采用了history模式的路由URL路径在服务端不存在刷新时服务器返回404。解决方式是在Nginx里配置try_fileslocation / { try_files $uri $uri/ /index.html; }这个配置的意思是如果请求的路径找不到对应文件就回退到index.html让前端路由接管。这个坑很多人第一次部署都会遇到记住了以后基本不会再翻车。从拿到源码到把系统跑起来我个人的经验是不要急于去看业务代码先跑通链路再顺着一条最能代表系统全貌的业务流去读代码。我一般从挂号开始看到收费结算结束这条链路读懂了整个系统的骨架也就掌握了。这套SpringBoot Vue MyBatis MySQL的组合未来几年里依旧会是国内后端开发的主流形态之一。最后分享一个小习惯我会在阅读这类源码时用思维导图按模块记录表和接口的对应关系等整个系统跑通了再回头看这张图就是你对这套系统最宝贵的认知沉淀对自己写过的项目复盘同样适用。
返回列表