前后端分离的疗养院管理系统,光听名字可能觉得就是个常规CRUD项目,但真把一套完整的Spring Boot + Vue + MyBatis + MySQL方案落地、跑通、部署上线,里面值得梳理的东西其实不少。我自己前前后后做过几个类似的管理系统,包括养老机构、社区健康服务站的项目,踩过的坑也不算少。这套疗养院管理系统源码加部署教程,解决的核心问题很明确:疗养院的老人档案、床位分配、护理记录、健康数据、费用结算这些日常事务,过去靠Excel和纸质台账管理,效率低、易出错、查询难;用系统管起来之后,前台接待、护士站、财务、院长各角色各干各的活,数据实时共享。适合正在学Java Web和Vue的开发者作为完整实战项目参考,也适合中小型疗养机构的技术外包方快速搭建原型。
整篇文章我会从选型逻辑、数据库设计、前后端实现、部署排错四个维度拆开讲。
1. 项目整体设计与技术选型思路
1.1 为什么必须做前后端分离
疗养院管理系统这个场景,和普通企业官网有本质区别。官网内容是静态展示,一套模板渲染就够了;管理系统则是典型的交互密集型应用,护士要快速录入体征数据、前台要实时查询空余床位、财务要按月份统计费用,页面局部刷新、异步请求、多状态联动都是刚需。如果还用传统的服务端渲染方案,每次点击都刷新整个页面,光是老人在住列表翻页、筛选,操作体验就非常糟糕。前后端分离之后,后端只负责提供JSON数据接口,前端Vue负责页面渲染和交互,数据更新通过异步请求局部刷新,操作流畅度明显提升。
前后端分离还有一个隐性好处:团队协作可以并行。后端按接口文档开发,前端按同一份文档写页面,不用互相等待。我见过不少小团队用前后端不分离的老套模板,改一个页面样式要重启Tomcat,调试效率低得让人抓狂。这套项目用分离架构,后端Spring Boot打包成独立Jar包,前端Vue构建成静态资源扔给Nginx,两边独立部署、独立扩容,后续接移动端App或者小程序也方便——接口和UI天然分开了。
1.2 技术栈选型的底层逻辑
选Spring Boot而不是Spring MVC + XML配置那一套老方案,最大的理由是启动速度和开发效率。Spring Boot自动配置机制省掉了大量XML配置,内嵌Tomcat让项目一个命令就能跑起来,这对中小型管理系统来说非常实用。Spring生态本身就成熟,事务管理、安全框架、数据校验这些都有稳定方案,遇到问题搜索资料也容易。
持久层用MyBatis而不是JPA/Hibernate,这是我特别想强调的一点。疗养院管理系统的业务查询场景很复杂:老人档案需要多条件组合查询(姓名、入住状态、楼层、护理等级),统计报表要按月份、科室、费用类型多个维度聚合,JPA的自动SQL在这种场景下反而难控制,经常出现N+1查询问题。MyBatis把SQL写明白摆在明面上,动态SQL可以根据条件拼接,多表关联查询、分组统计手写SQL效率更高。配合PageHelper做分页,逻辑清楚,也便于DBA后期review慢查询。
前端选Vue,对中小团队和入门开发者最友好。Vue的核心概念少,模板语法直观,组件化开发思路和HTML结构天然接近,比React的Hooks心智负担要小。Vue生态里的Element UI组件库提供了现成的表格、表单、弹窗、日期选择器,管理系统最常见的“表格 + 搜索条件 + 新增编辑弹窗”模式几乎可以拼装出来。加上Vue Router管理路由、Vuex或Pinia做状态共享,整套前端工程结构清晰,没有过度设计。
数据库MySQL是这个体量项目的稳妥选择。5.7以上版本支持事务、JSON字段、窗口函数,应付疗养院几千条老人档案、几万条护理记录完全没问题。和Oracle比部署简单、零授权成本;和PostgreSQL比,国内开发者和运维的熟悉程度更高,生产环境遇到问题更容易找到人解决。
1.3 项目功能范围怎么划
标题里说的“管理系统”,实际落地时功能范围要控制好,否则很容易做成一个几个月都交付不了的深坑。我见过不少项目一上来就规划了排班系统、药品库存、亲属探视预约、营养餐管理十几个模块,最终都是草草收场。这套项目的合理范围建议如下:
- 系统管理:用户登录、角色权限、菜单管理、操作日志。
- 老人档案:入住登记、档案编辑、退住注销、档案详情查询。
- 床位管理:楼栋/楼层/房间/床位四级结构,床位状态(空闲、占用、维修)管理,入住分配与换房。
- 护理管理:护理计划、护理记录、每日体征数据录入(体温、血压、心率等)与趋势查看。
- 费用管理:床位费、护理费、餐饮费等费用项设置,月度账单生成,缴费记录登记。
- 统计报表:入住率统计、费用汇总、护理工作量统计。
这些模块覆盖了疗养院日常运营的主线业务,同时又控制在合理工作量内。技术上有代表性,涵盖了单表CRUD、多表关联、条件组合查询、数据统计、权限控制等常见开发场景,作为完整源码项目学习或者二次开发的基础都足够扎实。
2. 核心功能模块与数据库设计
2.1 业务模块拆解与数据流转
先捋清楚业务上的数据是怎么流转的,再动手建表,这是我做这类系统一贯的顺序。一个老人入住疗养院,从入院信息登记开始,系统会为其建立档案记录,分配空闲床位,同时关联护理等级与费用方案。入住之后,护士每天填写护理记录和体征数据,后台生成费用账单。退住时更新床位状态为“空闲”,档案标记为“已退住”并保留历史记录。整条业务链路就像一根链条,每一步操作都在修改对应的数据状态,而数据库表结构必须支撑起这根链条的每一环。
这也是为什么我不建议一开始就拼命建表。先画一遍业务流程,明确每个环节的数据输入、输出和状态变化,再反推表结构,这样设计出来的库不会出现“一张大表装天下”或者表与表之间冗余混乱的问题。例如床位的状态变更,不只是在床位表里更新status字段,还要在入住记录表中留痕,记录谁在什么时间入住了哪个床位、何时换房或退住,这样后续统计“床位周转率”才有了数据基础。
2.2 数据库表结构设计要点
我直接说表结构里几个关键设计点,这些都是实际做过项目才容易注意到的细节。
老人档案表(elder_info)不要只设计姓名、身份证号、联系方式这些字段。疗养院场景里,紧急联系人信息、自带药品清单、过敏史、既往病史、饮食禁忌都非常重要。设计时把“基础信息”和“医疗/护理信息”拆到同一张表的不同字段组里,类型上能用TINYINT表示的状态字段(如性别、入住状态)不要用VARCHAR,节省空间也便于索引。入住状态建议用0-待入住、1-在住、2-已退住三个值,而不是存中文文本,前端再映射为标签显示。
床位表(bed_info)要与房间、楼栋解耦。我见过直接把房间名和床位号拼成一个字符串存的做法,导致换房、统计空床率时格外痛苦。建议楼栋(building)、房间(room)、床位(bed)各建一张表,层级用关联字段表达,例如房间表通过building_id关联楼栋,床位表通过room_id关联房间。这样前端楼栋筛选、楼层筛选、房间状态一目了然,后端统计每个楼栋的入住率也好写SQL。
费用表(fee_bill)是另一个容易踩坑的地方。疗养院费用计算不是一笔固定总价,而是由床位费、护理费、伙食费等多个费用项累加,而且可能因为护理等级调整产生中间月份的差价。我的建议是费用项用单独一张表(fee_item)维护,账单表和账单明细表拆开,账单主表记录归属月份、老人、合计金额、缴费状态,明细表存每个费用项的金额,这样月中调整费用时只需要在明细表中增加一条调整记录,不需要改主表金额逻辑。
健康体征记录(health_record)要按时间维度设计,关联老人ID和记录时间,把体温、收缩压、舒张压、心率、血糖等用单列存储,便于后续做趋势分析和图表展示。注意不要用JSON字段塞所有体征数据,虽然存储方便,但后续按单个指标做范围筛选、计算均值时会很别扭。
2.3 权限模型:RBAC怎么落地
疗养院系统的角色大概有:系统管理员、前台接待、护士、医生、财务、院长(只读报表)。权限控制用经典的RBAC模型,建五张核心表:用户表(sys_user)、角色表(sys_role)、菜单表(sys_menu)、用户角色关联表(sys_user_role)、角色菜单关联表(sys_role_menu)。
后端在用户登录成功后,查询该用户拥有的角色,再汇总角色对应的菜单权限,生成权限标识集合,存入登录态。后端接口做权限校验时,直接在接口方法或者拦截器层面比对所需的权限标识。前端则根据登录接口返回的菜单列表动态生成侧边栏路由,用户没权限的菜单不显示,是有权限控制很好的配合方式。
菜单表设计时建议加一个component字段,对应Vue前端路由的组件路径;前端拿到菜单之后,通过动态路由注册的方式把组件加载出来。权限控制这里有个常见坑:前端隐藏菜单只是体验优化,真正的权限控制必须在后端接口层面做,否则直接发一个请求就能访问没有权限的数据。项目里一定要确保每个接口都有对应的权限校验逻辑。
3. 后端核心实现与接口设计
3.1 项目目录结构与分层规范
后端代码结构我建议按功能模块分包,而不是按技术分层分包。按controller/service/mapper这样横向分包,模块多了之后找代码很累;按模块分包实际开发中最顺手:
com.example.sanatorium ├── common // 通用返回结果、异常处理、常量、工具类 ├── config // 配置类:跨域、拦截器、MyBatis配置 ├── security // 登录认证与权限校验相关 ├── module │ ├── system // 用户、角色、菜单等系统管理 │ ├── elder // 老人档案模块 │ ├── bed // 床位管理模块 │ ├── nursing // 护理记录与体征模块 │ ├── finance // 费用账单模块 │ └── report // 统计报表模块 └── SanatoriumApplication.java每个模块内部再分controller、service、mapper三层。controller只做参数接收和结果返回,不写业务逻辑;service层写业务逻辑和事务控制;mapper层只写数据库访问方法。这样分层的价值在于可测试性和可维护性。后期如果某个模块要拆出去独立成一个微服务,模块内的代码可以直接搬迁,不需要做很大的调整。
3.2 统一返回结构与全局异常处理
前端拿到接口数据,不可能每个接口都写一遍“解析成功/失败”的逻辑。统一返回结构是前后端联调的基础。项目里定义一个通用返回类,核心字段就三个:code(状态码)、message(提示信息)、data(业务数据)。成功时code为200,业务异常时code为业务自定义状态码,未登录时code为401。这样前端axios拦截器里统一判断code,200直接返回data,其它code弹提示或跳登录页。
全局异常处理如果用得好,能省掉大量冗余代码。在Spring Boot里定义@RestControllerAdvice,捕获业务异常、参数校验异常、兜底的Exception,分别转成对应的返回结构。特别是参数校验,用javax.validation的注解(@NotBlank、@NotNull)标注实体类字段,在Controller参数上加@Validated,框架会在进入业务代码之前完成参数校验,不合格直接抛异常走全局异常处理。这比在service里写一堆if-else判空要简洁许多。
3.3 MyBatis动态SQL与分页查询实战
MyBatis是这个项目里后端比较有技术含量的一块,尤其适合老人档案列表这种“条件组合不固定”的查询场景。比如列表页搜索条件是姓名模糊匹配、入住状态精确匹配、护理等级精确匹配,用户可能填其中任意几个条件。动态SQL此时尤为重要,用 标签加 标签拼接,有条件的字段就带上,没有就不拼,SQL不会因为空条件而报错。
分页查询直接用PageHelper插件,只在service层调用PageHelper.startPage(pageNum, pageSize),紧跟其后的第一条Mapper查询会自动带上LIMIT,查询结果用PageInfo包装后返回前端。这里有一个必须提醒的坑:startPage之后只能跟一条SQL查询,如果在这中间执行了其它查询(比如查完列表后又查了一次总数),分页就会失效或者错乱。所以业务逻辑里要保证startPage紧跟目标查询,中间不要混入其它数据库操作。
多表关联查询也是管理系统的常态。查询老人列表时,需要同时带出当前床位号、护理等级名称,而不只是显示一个外键ID。我在Mapper里直接写联表查询,用LEFT JOIN把bed_info和nursing_level表对应的名称字段查出来,封装到VO对象返回,避免前端拿ID再发N次请求去翻译名称。这种联表查询写起来不复杂,但要注意字段名冲突,多表查询时统一用别名区分,比如b.bed_no AS bedNo、el.id AS elderId。
3.4 登录认证与权限拦截实现
认证方案我在前面提了RBAC模型,再补充一下落地细节。项目里用一个拦截器实现登录校验即可,不必上Spring Security那套重型框架,对中小项目反而简洁。登录接口校验用户名密码成功后,生成一个Token(可以用JWT,也可以简单用UUID存在Redis里)返回给前端。前端每次请求在Header中带上这个Token,后端拦截器从Header取Token,校验通过就放行,并把用户信息放入ThreadLocal,后续业务代码可以从上下文中取当前操作人。
我试过用JWT,也试过Redis Token方案,各有取舍。JWT不需要Redis存储,天然支持分布式,但注销和强制下线不好处理;Redis方案逻辑直观、可控制过期时间,但每次请求都要查一次Redis。单机部署的疗养院系统,Redis Token方案即可,实现代码更少,理解成本也更低。
拦截器里放行登录接口和静态资源路径,其它路径全部校验。权限校验方面,可以在需要权限控制的接口上添加自定义注解,比如@RequirePermission("elder:add"),拦载器里解析注解并比对用户的权限标识集合,没有权限直接返回403对应的异常。这样接口权限控制做到了声明式,加一个注解就能保护一个接口,比散落各处的if判断清晰得多。
4. 前端Vue实现与前后端联调
4.1 Vue项目结构与页面规划
前端工程用的是Vue 2 + Element UI(Vue 3项目用Element Plus也可以,思路是一样的)。项目结构按views目录组织页面,按api目录组织接口调用:
src ├── api // 接口请求封装,按模块拆分文件 │ ├── elder.js │ ├── bed.js │ ├── nursing.js │ └── finance.js ├── assets // 静态资源 ├── components // 公共组件:分页、弹窗、导入导出等 ├── router // 路由配置 ├── store // 状态管理 ├── utils // 工具:axios封装、日期格式化 └── views ├── login.vue ├── dashboard.vue ├── elder // 老人档案列表、详情、编辑 ├── bed // 床位管理 ├── nursing // 护理记录 ├── finance // 费用管理 └── system // 用户、角色、菜单页面规划上,我建议列表页和表单页要么用独立路由,要么用同一个路由加弹窗切换。列表页用弹窗做新增编辑最省事,因为表单字段多,弹窗宽度控制为600px或800px,交互直观;详情页用抽屉组件展示结信息,老人带医疗信息文本较长,放弹窗里体验不太好。
4.2 路由与状态管理设计
路由配置要考虑动态路由和静态路由结合。登录页、404页是静态路由,其它业务页面根据登录用户返回的菜单数据动态生成。前端在路由守卫里做登录态检查,用户未登录直接跳登录页,已登录但没有菜单数据时,先请求菜单接口生成动态路由再放行。这里有一个兼容点:动态路由注册是async添加的,如果用户在页面刷新时,菜单还没加载完就跳转,会出现空白页。实际项目中可以配合全局前置守卫里加载菜单、再next(),确保路由完整再放行。
状态管理方面,用户信息、菜单列表、权限标识放全局store里,是合理的。页面业务的组件间传值,优先用props和事件,不要什么都塞store。把store当垃圾桶用,后续维护会非常痛苦。
4.3 接口封装与跨域处理
axios封装是前端项目的基础工程。我在utils/request.js里创建一个axios实例,设置baseURL(开发环境指向http://localhost:8080,生产环境指向api路径),并添加请求拦截器:每次请求从store或localStorage里取Token放进Header。响应拦截器统一处理返回值:code为200时直接返回data;code为401时清除登录态并跳转登录页;code为其它值时弹出错误提示。
开发阶段跨域问题很常见。前端跑在8080端口,后端跑在8080端口时?如果前后端端口一样就同源了,但通常前端工程用8081,后端用8080,端口不同也是跨域。最简单的方案是在前端工程配置代理转发。Vue CLI项目里在vue.config.js中配置devServer.proxy,把/api路径的请求代理到http://localhost:8080,生产环境用Nginx反向代理也是一样的思路。这样才能避免在后端强行开启CORS放行所有来源——方便是方便,但生产环境会有安全隐患。
我在给某机构做项目时,前端联调阶段总是遇到“接口504”或“请求挂起”的假象,查到最后十有八九是代理地址写错了或者后端没启动。联调前先把后端接口在浏览器直接访问一遍,确认能正常返回数据,再排查前端代理配置,这个顺序能省很多时间。
4.4 表格、表单与数据联动实现
Element UI的el-table是列表页的核心。列配置里要注意格式化,比如性别、入住状态这类枚举值,建议在前端用字典映射显示中文Label,而不是后端直接返回中文。比如后端返回gender: 1,前端根据字典显示“男”,返回gender: 0显示“女”。这样后端接口更通用,前端展示也更灵活。
表单实现最需要注意的是校验规则和提交数据的处理。老年人档案里身份证号是必填且需要格式校验,出生日期可以根据身份证自动带出,填写时联动赋值,是很好的交互细节。手机号和家属联系电话用正则校验。提交时,create和update两种情况要区分:新增时主键ID为空,编辑时主键ID带回来,提交前要清空不需要传给后端的展示字段。
床位选择联动是疗养院系统里比较有代表性的交互:点击“分配床位”按钮,弹出一个对话框,先选楼栋,再选楼层,再选房间,最后出现可选床位列表。实现时,楼栋变化之后清空楼层选项、请求该楼栋的楼层列表;楼层变化之后清空房间选项;房间变化之后请求该房间的床位列表,并把已占用的床位置灰。这种级联交互写起来不复杂,但是比较考验对组件状态更新的理解,尤其是Vue 2里v-model和watch的配合,稍不留神就会出现数据滞后的问题。
5. 部署上线与常见问题排查
5.1 本地环境准备
项目跑起来之前,环境变量要准备齐全。开发机器上至少需要:
- JDK 1.8及以上
- Maven 3.6+
- MySQL 5.7+
- Node.js 14+与npm
- 开发IDE(写Java用IDEA,前端用VS Code即可)
推荐用版本管理工具控制Java和Node的版本。我见过一个比较折腾的案例:Node版本太高,Vue 2项目里node-sass编译失败,卡了一整天;Java版本太高,某些老库反射报警。稳妥做法是项目里统一写一个README环境说明文档,标注测试通过的版本组合,为后续接手的人省大量时间。
5.2 后端打包与启动
后端部署打包有两种方式。方式一:直接用Maven的package命令,打包成Spring Boot的可执行Jar。执行mvn clean package -DskipTests,然后java -jar sanatorium.jar启动。打包前注意检查application.yml里的数据库连接配置,生产环境的数据库地址、账号、密码不要硬编码在代码里,用环境变量方式引用:spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/sanatorium},这样在部署环境里通过环境变量覆盖默认值,代码仓库里不存生产密码,更安全。
方式二:如果需要独立部署到外置Tomcat,需要把Spring Boot项目改造为war包。但我个人不太推荐这类做法,Spring Boot的内嵌容器开箱即用,Jar包部署方式运维成本最低,跟Nginx配合时,后端单一端口对外服务也简洁。
启动之后检查日志,如果出现“端口已被占用”,用netstat命令查找冲突进程并处理。我建议生产环境用systemd守护Jar进程,配置自动重启,避免因为进程意外挂掉导致服务不可用。
5.3 前端构建与Nginx部署
前端部署的要点在构建配置和静态资源路径。
Vue项目的vue.config.js中,publicPath选项在生产环境建议设置为相对路径或Nginx子路径。如果直接部署在域名根路径,publicPath可以设为'./';部署在子目录下,则要配置为子目录路径。这个配置错了,打包出来的index.html里引用的js/css路径就会404,页面打开白屏。
执行npm run build之后,dist目录就是构建产物,包含index.html、static或assets目录。部署方式:
- 本地直启:把dist目录扔进Nginx的html目录。
- 服务器部署:用Nginx作为静态服务器,配置root指向dist目录。同时配置/api开头的请求反向代理转发到后端服务:
location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这个代理配置同时解决了生产环境的跨域问题,前端请求api路径,Nginx转发到后端,浏览器端看到的是同源请求。
5.4 常见问题速查表
我把实际运行中最容易遇到的几个问题整理成了表格,排查顺序从高到低。
| 问题现象 | 可能原因 | 排查与解决方式 |
|---|---|---|
| 页面白屏,Console报静态资源404 | 前端publicPath配置不当 | 修改vue.config.js的publicPath,重新构建 |
| 登录请求报500 | 数据库连接失败或SQL异常 | 先看后端日志,确认数据库可连接,再检查Mapper SQL |
| 接口返回504或超时 | 后端服务未启动或代理地址错误 | 直接curl后端接口测试连通性,再检查代理配置 |
| 列表分页无效 | PageHelper误用 | 确认startPage紧跟目标查询,中间无其它SQL |
| 前端请求跨域报错 | 开发环境未配置代理或后端未开CORS | 开发环境配proxy,生产环境配Nginx代理 |
| 中文乱码 | 数据库字符集非utf8mb4 | 创建数据库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci |
中文乱码这个问题我印象很深刻。有一次项目部署到客户的Windows服务器上,所有中文菜单全部变成问号,排查到最后发现是数据库创建时没有指定字符集,MySQL默认使用了latin1。在创建数据库语句里明确指定utf8mb4之后,重启应用并重导数据就正常了。凡是做中文管理系统,建库时第一件事就是指定字符集,这个基础操作不要省略。
实际部署过程的小经验
最后说点经验之外的经验。我在部署这类管理系统时逐渐养成了几个习惯:第一,所有配置文件用环境变量预留占位,这样开发环境、测试环境、生产环境之间切换配置只需要改环境变量,不需要改代码;第二,上线之前一定要完整走一遍核心业务流程,从老人入院登记到分配床位,再到护理记录填写和月度账单生成,任何一个环节出现问题提前暴露,都比上线之后再返工强;第三,前端接口联调阶段就把接口文档维护好,哪怕是自己一个人开发,三天后回来看代码也会感谢当时的文档。
还有一个容易被忽略的点:系统的定时任务。费用管理模块的月度账单如果要定时生成,建议用Spring的@Scheduled注解实现简单的定时任务,每天凌晨检查是否到了账单生成日,自动为所有在住老人生成当月账单。这个功能会让系统比“纯手动录入”显得完整得多,但实现时注意判断幂等性:同一个月同一个老人不要重复生成账单,通过唯一索引或者状态判断来控制。
这套Spring Boot + Vue + MyBatis + MySQL的疗养院管理系统,技术栈经典、业务场景明确、部署路径清晰,适合作为前后端分离项目的完整实战参考。如果你正在找一套能真正跑起来的管理系统源码,或者想把前后端分离的整套流程走通,从数据库设计到接口开发再到前端联调和部署上线,这套项目足够你研究一阵子。过程中遇到任何问题,欢迎按我上面列出的排查顺序动手验证,大部分问题都能在几分钟内定位。