ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+Vue的养老院信息管理系统设计与实现

基于SpringBoot+Vue的养老院信息管理系统设计与实现 接手这个项目之前我刚帮一个朋友做完他家里养老院的信息化改造。他的管理方式特别典型老人档案是一摞纸质台账缴费记录躺在三个Excel表格里月底对账全家上阵算错一次就得翻半天原始单据。这种现状在中小型养老机构里太常见了——业务不算复杂但信息分散、更新滞后、查询靠翻、统计靠算。所以当我决定自己动手做一套完整的养老院信息管理系统时思路非常明确用SpringBoot Vue这套前后端分离的方案把老人档案、床位分配、护理记录、费用结算等核心业务全部搬到线上让每一笔数据都能查、能算、能追溯。这篇文章我就从项目拆解、数据库设计、后端实现、前端页面、部署联调五个维度把整个系统的搭建过程完整梳理一遍包括我踩过的坑和优化过的细节。1. 项目整体设计与技术选型思路1.1 养老院管理的核心场景拆解养老院的信息管理系统和普通的企业管理系统有明显的差异。最大的特点在于它管理的是人而且是需要持续照护的老年人群体。这就决定了系统的核心不是简单的增删改查而是围绕老人的全生命周期做信息追踪——从入院登记开始到每天的护理记录、每周的健康监测、每月的费用结算一直到出院或转院所有环节都要有据可查。我把核心业务拆成了几个模块基础档案管理老人、员工、家属、空间资源管理楼栋、房间、床位、日常运营管理护理记录、健康监测、探望登记、费用结算管理床位费、护理费、餐费、医疗费。每个模块之间都有强关联老人分配到床位护理记录挂在老人名下费用明细按老人汇总。这种模块划分不是拍脑袋定的而是我实际走访了几家养老机构之后总结出来的共性需求。1.2 为什么选择SpringBoot Vue这套组合技术选型这块我几乎没有犹豫直接定了SpringBoot Vue。原因有三第一SpringBoot是目前Java后端的主流框架内嵌Tomcat、自动配置、起步依赖这些特性让项目搭建成本极低不用像传统SSH那样写一大堆XML配置第二Vue作为前端框架组件化开发和响应式数据绑定非常适合管理系统这种表单表格弹窗密集交互的场景第三这套组合的社区活跃度极高遇到问题基本都能搜到解决方案对后续的维护和二次开发非常友好。横向对比一下如果用Python的Django后端开发确实快但国内运维和部署的生态不如Java成熟如果用React替代Vue功能上完全可行但对中小团队来说Vue的学习曲线更平缓中文文档也更完善。我最终选SpringBoot Vue更多是出于工程落地和长期维护的考虑而不是追求技术上的新奇。1.3 前后端分离架构的优势这个项目从第一天起就采用了前后端分离的架构。后端只提供RESTful API前端用Vue独立构建页面两边通过JSON格式的数据交互。为什么不像传统项目那样直接用Thymeleaf做服务端渲染关键在这几点一是前后端可以并行开发我和前端小伙伴各干各的不用互相等二是API可以复用以后要做小程序或者App后端接口直接对接就行三是部署灵活前端静态文件可以扔到Nginx也可以打进SpringBoot的static目录怎么方便怎么来。不过前后端分离也让不少人吃过亏最大的坑就是跨域。开发环境下前端跑在8080端口后端跑在8081端口浏览器会拦截跨域请求。所以我在后端统一配置了CORS过滤器开发环境放开所有跨域限制生产环境只允许指定域名访问。这个配置在后端启动类里加个过滤器就能搞定但容易遗漏我见过好些个项目在联调阶段被这个问题卡住。2. 数据库设计与核心表结构拆解2.1 数据库设计的基本原则数据库设计是整个系统的地基这个环节出了问题后面写再多代码都是白搭。我按照第三范式来做表结构设计尽可能消除数据冗余。比如老人信息和床位信息分开存通过房间ID关联而不是把房间号直接冗余到老人表里——虽然这样查询时要多一次关联但避免了床位调整时数据不一致的问题。主键策略上我全部用了自增ID没有用UUID。原因很简单自增ID在MySQL里走聚簇索引插入和查询性能都更好而且在这个系统的数据量级下撑死几千个老人UUID的全球唯一性优势根本用不上。时间字段的处理也有一点讲究所有表都加了create_time和update_time两个字段create_time用数据库的DEFAULT CURRENT_TIMESTAMPupdate_time靠MyBatis-Plus的自动填充这样在应用层完全不用手动维护审计溯源的时候非常有用。2.2 核心业务表详解整个系统我一共设计了12张表核心的几张业务表单独说一下。elder_info老人信息表是系统的核心表字段包括姓名、身份证号、性别、出生日期、入住日期、房间ID、床位号、健康状态、紧急联系人及电话。其中身份证号我在数据库层做了唯一约束——这个逻辑跟业务强相关不能出现同一个老人重复入院的情况。健康状态我用枚举值存储良好/一般/失能/半失能这样在前端可以做下拉选择后台做统计时也方便。room_info房间床位表保存楼栋、楼层、房号、床位编号、朝向、面积、床位类型单人间/双人间/多人间、当前状态空闲/已入住/维修中。这里有个设计细节房间和床位是一对多关系所以我用room_id关联床位编号不直接写成3层12床而是房号加床位号的组合方便前台展示和后台查询。care_record护理记录表记录了每一次护理行为字段包括老人ID、护工ID、护理类型晨间护理/翻身/喂药/洗澡等、护理时间、护理内容备注。这张表是数据增长最快的也是后面做护工绩效考核的数据来源。在设计时我特别注意给老人ID和护理时间加了联合索引否则一个月下来几万条记录按老人维度查询会明显变慢。fee_record缴费明细表记录老人的每一笔费用字段包括老人ID、费用类型床位费/护理费/餐费/医疗费/押金、金额、产生时间、缴费状态待缴/已缴/逾期。这张表我单独设置了fee_month字段比如2025-06方便按月汇总对账。养老院的费用核算有它的特殊性费用不是一次性产生的而是按天累积月底统一结算所以设计上不能只记一笔总数必须把每一条费用流水都留下来。2.3 表关联关系与索引优化表之间的关联关系是elder_info关联room_info多对一care_record关联elder_info和staff_info多对一fee_record关联elder_info多对一。所有外键我都没有在数据库层面做物理约束而是在应用层维护。这个取舍是经过考虑的——物理外键虽然能保证数据完整性但会增加每次插入和更新的性能开销而且后期如果要拆库拆表物理外键会变成麻烦。只要应用层的逻辑严格校验数据一致性是可以保证的。索引方面除了主键索引我重点做了几个优化elder_info的id_card加了唯一索引care_record的(elder_id, care_time)建了联合索引fee_record的(elder_id, fee_month)建了联合索引room_info的status字段加普通索引。这些索引设计直接来自实际的查询模式——系统里最高频的操作就是查某个老人的护理记录和查某个月的费用明细索引建对了数据量上来之后查询性能才有保障。3. 后端SpringBoot核心实现与配置要点3.1 项目结构与依赖配置后端项目的包结构是标准的四层架构controller接口层、service业务层、mapper数据访问层、entity实体层。我见过不少项目把业务逻辑直接写在controller里表面上省事实际上后面想复用逻辑、加单元测试都寸步难行。我的习惯是controller只做参数接收和结果封装业务逻辑全在service层这样接口精简逻辑清晰。pom.xml的核心依赖有这么几个spring-boot-starter-webWeb基础、mybatis-plus-boot-starter数据访问、mysql-connector-java数据库驱动、jjwtJWT令牌、lombok消除样板代码。我特意选了MyBatis-Plus而不是原生MyBatis因为它的BaseMapper内置了常用的增删改查方法分页查询也有现成的插件能省掉大量重复的SQL编写工作。遇到复杂的多表关联查询我再用自定义XML来写这样既有开发效率又保留了SQL的灵活性。3.2 SpringBoot配置文件里的那些细节application.yml是这个项目里最容易被忽略却又最容易出错的文件。我贴一下核心配置并解释每一处的意图server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/nursing_home?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里有几个坑必须提一下第一url里的serverTimezone必须指定否则高版本的MySQL驱动会直接报时区错误第二useSSL一定要设为false本地开发环境证书校验只会添乱第三jackson的date-format配置如果不做后端返回的日期会是时间戳格式前端处理起来非常痛苦。还有mybatis-plus的log-impl配置开发环境建议打开能直接在控制台看到运行的SQL语句排查问题效率翻倍。有不少人遇到过SpringBoot版本太高引发的兼容性问题比如用了SpringBoot 3.x但mybatis-plus的旧版本还不支持启动直接报错。我的建议是除非有特殊需求否则不用一味追新SpringBoot 2.7.x加JDK 8的组合是最稳的生态兼容性最好网上能搜到的解决方案也最多。3.3 登录认证与JWT实现养老院管理系统涉及老人隐私数据登录认证是必不可少的一环。我用了JWTJSON Web Token做无状态认证。相比Session方案JWT的好处在于后端不用维护会话状态接口天然支持横向扩展应用部署到多台服务器也不需要做会话同步。实现逻辑并不复杂用户提交账号密码后端校验通过后生成一个有效期为24小时的JWT令牌返回给前端。前端把令牌存在localStorage里每次请求在请求头加上Authorization: Bearer token。后端用一个拦截器统一校验令牌合法就放行否则返回401状态码。密码存储用了BCrypt加密同一个密码每次加密后的结果都不同即使数据库泄露攻击者也很难反推出原始密码。3.4 统一返回结构与分页查询接口返回的数据格式如果不统一前端写起来就会非常烦躁。我从一开始就定义了一个Result对象结构是code状态码、message提示信息、data业务数据。成功返回200业务异常返回自定义错误码系统异常返回500前端拿到响应后先判断code再决定怎么处理逻辑非常清晰。分页查询这里是个重点。我用的方案是MyBatis-Plus的分页插件Controller接收pageNum和pageSize两个参数Service层封装成Page对象最终返回给前端的结构是{ total: 100, records: [...], current: 1, size: 10 }。前端拿到这个结构后配合Element UI的el-pagination组件就能无缝对接。这里有个小细节分页参数如果不做上限限制用户按10000条一页去查数据库压力会非常大。所以我在接口层对pageSize做了校验超过100就强制改为100用很小的代码成本避免了一个潜在的性能坑。3.5 业务逻辑层的几个设计细节业务逻辑层的设计值得一提。以老人入住为例这个操作不是单纯往elder_info插一条记录就完了而是一个事务性的操作要先检查老人是否已存在身份证号查重再检查床位是否空闲然后创建老人档案、把床位状态改为已入住、生成一条入住记录的日志。这三个操作要么全成功要么全失败所以我在Service层加了Transactional注解保证事务的原子性。费用结算模块我用了策略模式。费用类型虽然字段固定但计算逻辑完全不同床位费按天计算护理费根据老人的健康状态分档按月收取餐费按实际就餐次数结算。我把每种费用的计算逻辑封装成独立策略类通过工厂类根据费用类型获取对应的策略对象。这样以后新增费用类型只需要加一个策略类不用改动已有的代码符合开闭原则。4. 前端Vue核心实现与页面交互4.1 前端项目结构与工程化配置前端这块我用Vue 3 Vite Element Plus这套组合配合Pinia做状态管理。选择Vue 3的原因很直接组合式API让逻辑复用更顺畅响应式系统的性能比Vue 2更高而且Element Plus就是专为Vue 3设计的组件库。Vite相比Webpack最大的优势是启动速度开发环境下几乎是秒开热更新也快得多。我记得第一次用Vite启动项目的时候那种保存代码、浏览器立即刷新的体验确实比Webpack舒适太多。不过Vite的配置有几个细节要注意打包时静态资源的base路径要配置好否则部署到子目录会出现资源找不到的问题开发环境的代理要指向后端服务的地址避免接口跨域。项目的目录结构是这样划分的views目录放页面级组件components目录放可复用的UI组件router目录放路由配置store目录放Pinia状态定义api目录封装所有后端接口请求。api目录的封装我单独说一下——每个接口都对应一个独立的方法比如getElderList(params)、saveElder(data)、deleteElder(id)。这样页面组件里只需要调用这些方法不用关心axios的URL、请求头这些细节后端接口路径变了也只需要改一个文件。4.2 动态路由与权限控制权限控制这部分我用了动态路由的方案。系统里有两种角色管理员和普通护工。管理员能看到全部菜单包括系统管理模块护工只能看到老人管理和护理记录相关的菜单。我后端的菜单表配置了对应的路由路径前端在用户登录成功后根据后端返回的角色信息动态拼接路由通过router.addRoute()注入到路由表中。这种做法的好处是菜单和权限都在后端控制前端就算改了路由表没有接口权限也拿不到数据安全性更有保障。路由守卫是另一个关键点。我在全局前置守卫里做了三件事检查本地是否有token没有就跳转到登录页拼接好动态路由后再放行每次路由跳转时更新页面的标题。这部分的代码不长但维护了整个系统的访问控制可以说是前端最核心的一段逻辑。4.3 页面组件设计与插槽的实际应用Element Plus是我用的主要组件库。在老人档案管理页面我用el-table展示老人列表el-pagination做分页el-dialog做新增和编辑的弹窗el-form做表单校验el-select做下拉选择。这些组件的组合使用基本是管理系统的标准模式但有几个使用细节值得分享。表格列的自定义渲染我大量使用了插槽slot。比如健康状态这一列不直接显示存储值而是通过插槽根据不同的值渲染不同颜色的el-tag——良好显示绿色、一般显示橙色、失能显示红色这样页面上的信息层级一目了然。操作列也是用插槽来实现的把编辑和删除按钮放在每一行的操作区域里需要点击时通过click事件把当前行的数据传出去配合scope.$index拿到行号。插槽的灵活性很高但我建议不要在一个列里面写太复杂的逻辑如果一个列的内容超过三行代码我会抽成独立的子组件通过props传值这样模板保持简洁的同时可维护性也更好。4.4 数据交互和状态管理的实现思路前端的API请求我用axios封装了一个统一实例设置了基础URL和请求拦截器。请求拦截器给每个请求自动加上token请求头——这一步非常关键否则每个接口都要手动传token逻辑重复且容易漏。响应拦截器里做了统一的错误处理比如token过期时清除本地存储并跳回登录页后端返回业务错误时自动弹出Message提示页面组件不需要在每个接口调用处重复处理错误场景。Pinia的状态管理主要用来存两份全局数据一是当前登录用户的个人信息和角色权限二是系统里的基础字典数据比如费用类型、健康状态枚举值。这些数据在多个页面都会用到放到全局状态里能避免重复请求。实际用下来Pinia的setup写法比Vuex的commit/mutation那一套简洁不少新上手的人更容易理解。5. 部署上线与前后端联调实战5.1 本地开发环境准备与版本兼容环境配置这件事看起来简单实际上是最容易让人卡壳的环节。我先列一份我自己实践下来最稳妥的版本组合JDK1.8稳妥老版本不要直接上17或21部分依赖可能会有兼容性问题MySQL5.7或8.0均可推荐5.7占用资源更少配置更简单Node.js18.x LTS版本Vite 4以上要求Node版本不能太低Maven3.6.x或3.8.xIDEIDEA 2022及以上版本数据库初始化这块项目里附带了一份完整的SQL脚本。但我建议不要直接无脑执行先打开脚本看一遍表结构理解每张表的用途再执行。我在实际项目里见到过新手直接把脚本跑完就开始写代码结果搞不清哪些字段是业务字段、哪些是逻辑删除标记后面改起来特别被动。执行完脚本之后记得用Navicat或者命令行确认一下表都建出来了基础数据比如管理员账号已经写入再进入下一步。这里特别提醒一下Vue的环境配置安装Node之后建议把npm的镜像源切换为国内镜像来避免下载依赖时网络不稳定导致的各种半途失败。切换方法很简单执行npm config set registry https://registry.npmmirror.com之后npm install的速度会有肉眼可见的提升。如果某个依赖版本报错可以用npm install 包名指定版本来锁定版本不用重新搞整个环境。5.2 Vue打包并集成到SpringBoot开发完成后面临一个部署问题是前后端完全分离部署还是把前端打包后放进SpringBoot一起部署我的选择是后者——对于一个中小型养老院项目单独搞一套Nginx部署前端虽然更规范但增加了部署成本和运维复杂度。直接把Vue构建后的静态文件放进SpringBoot的static目录一个jar包就能跑起来部署体验最好。具体操作是这样的第一步在Vue项目的vite.config.js里设置base: ./确保打包后的资源引用的是相对路径。第二步执行npm run build生成dist目录。第三步把dist目录里的所有文件复制到SpringBoot的src/main/resources/static目录下。第四步重新打包SpringBoot项目一个完整的可运行jar就出炉了。这里有一个必须注意的坑前端路由用的history模式如果直接部署会出现刷新页面就404的问题。原因是history模式下前端路由是由JS解析的后端服务器找不到对应路径的资源。解法有两个方案一是把前端路由改成hash模式地址栏里会多个#号但刷新没问题方案二是在SpringBoot加一个控制器把非API的路径全部转发到index.html。我自己用的是方案二比hash模式更优雅代码也就几行的事。5.3 跨域问题与代理配置实战前后端分离开发模式下跨域问题几乎必然会遇到。开发环境下前端Vite跑在5173端口后端SpringBoot跑在8081端口浏览器会拦截这个跨域的HTTP请求。解决方式有两种我按场景分别使用。第一种是后端CORS配置适合接口层统一放行的情况。在SpringBoot配置类里注册一个CorsFilter允许指定的前端地址访问。开发环境可以直接放开所有来源生产环境必须限制为实际部署的域名不然等于给所有人开了接口权限。第二种是前端Vite代理适合不想动后端代码的时候。在vite.config.js里配置server.proxy把/api开头的请求转发到后端的localhost:8081。这样前端代码里请求的地址就是/api/xxx看起来像是同源的浏览器不会拦截。我推荐开发环境用Vite代理方案因为它完全不动后端代码而且代理转发的行为和生产环境Nginx的转发逻辑更接近联调通过之后部署基本不会有意外。5.4 我遇到过的几个经典问题和排查思路问题一启动报端口被占用。这是新手最常见的问题浏览器或别的Java进程占用了8080/8081端口。排查命令是Windows下用netstat -ano | findstr 8080Linux下用lsof -i:8080找到占用进程的PID后杀掉即可。问题二前端请求接口返回404。这个坑我帮别人排查过好多次原因是请求路径写错了或者后端context-path有配置。先在后端控制台确认接口访问路径再仔细对比api目录里封装的路径和实际请求路径多半能找到差异。问题三加了token校验后接口全部401。如果前端没有在请求拦截器里加上Authorization头后端拦截器自然校验不通过。检查axios封装的请求拦截器看token是否被正确拼接到header里同时确认后端的token校验逻辑没有写错header名称。问题四Vue打包后页面白屏。大概率是打包路径配置出了问题。检查vite.config.js里的base是否设置为./以及static目录下的index.html里引用的js/css路径是否正确。另一个可能是路由守卫出现死循环比如to.path是登录页但redirect又被拦回登录页这个通过控制台的打印日志能很快定位。问题五数据库连接不上。先ping一下数据库服务器确认网络再确认MySQL服务确实启动了最后检查用户名、密码和数据库名称是否正确。如果用的是MySQL 8.0要确认驱动版本是com.mysql.cj.jdbc.Driver并且pom里引入的是8.x版本的connector否则会报驱动类找不到。5.5 项目文档与交付物管理这套系统除了源码还配套了完整的数据库脚本、部署说明文档和设计文档。数据库脚本里有建表语句和初始数据文档里写清楚了环境要求、部署步骤和接口说明。很多人拿到项目源码第一步就想跑起来但往往因为环境不一致卡半天所以我在部署文档里把每个环节都写得特别细包括JDK安装配置、Maven设置、MySQL初始化这些前置步骤。文档这块我建议哪怕只是自己用也要顺手写好。原因很实在写文档的过程会逼着你把项目的关键决策和实现逻辑梳理清楚很多代码里看不出来的设计意图文档里三句话就能说明白。我自己接手过不少没有文档的项目那种代码就是文档的说法在短期内可行半年之后回来看自己的代码都会发懵更别说别人了。6. 写在最后的经验总结做这套系统最大的收获不是把某个框架用得有多熟练而是真正理解了管理系统这四个字的含义。养老院的业务并不复杂真正的难点在于把事情做对一条护理记录可能关系到老人的健康安全一笔费用差错可能引起家属投诉所以系统里对关键操作都要留痕、都要可追溯。我在设计护理记录模块时坚持所有记录不允许修改只允许追加更正说明虽然这给用户操作带来了一点不便但从数据可信度的角度看是非常值得的。最后再分享一个小技巧给这套系统做二次开发或相似系统时建议把通用系统管理的部分用户管理、角色管理、菜单权限、操作日志单独抽取成一组通用模块。因为几乎所有的管理信息系统都包含这些功能下次再接到类似项目比如物业管理系统、社区关怀系统、学校宿舍管理系统直接把通用模块搬过去只需要开发核心业务部分开发周期能压缩一半以上。这套养老院系统我后续还打算接入两个扩展一是对接智能手环的健康数据让老人的心率、步数自动同步到系统里二是增加家属端小程序让家属可以实时查看老人的护理信息和缴费账单。这两个方向的需求都很真实技术上也不算复杂在现有架构上扩展起来非常自然。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进