Spring Boot实战:智慧养老院管理系统的架构设计与权限控制 从需求到落地我如何用Spring Boot搭起一套智慧养老院管理系统去年年初接手了一个养老院管理系统的开发任务机构那边的情况比较典型三百多张床位护理人员几十号人老人的健康档案还停留在纸质登记家属想了解老人情况只能打电话问前台护理排班全靠Excel手动排。领导说要上一套“智慧养老”系统我一开始以为是噱头等真正把需求理完才发现这块业务远比想象中复杂——它既要做传统的人事、床位、费用管理又要对接健康监测设备和护理任务流转还牵扯到家属端的实时沟通。整套系统从立项到上线用了将近五个月最终的架构是基于Spring Boot为后端核心搭建的把设备对接、任务调度、权限控制、消息推送这些能力全部揉进了一个单体应用中。这篇文章分享一下整套系统的设计思路和关键实现细节。如果你正准备做类似的SpringBoot毕设项目或者公司打算给养老机构做信息化改造应该能从中看到一条比较务实的落地路线——包括数据库建模、定时任务设计、多角色权限控制以及我在实际开发里踩过的那些坑。1. 项目背景与需求盘点养老院管理系统到底管什么动工之前我花了整整一周蹲在养老院里观察他们的日常运转。这个过程非常重要因为养老管理系统和普通的业务系统有个显著区别它的核心数据是“人”而且是需要持续照护的老年人系统一旦出错直接影响的是线下护理安全。所以需求分析阶段不能只坐在办公室看调研表必须搞清楚每个角色每天到底在做什么。1.1 传统养老院管理的三大痛点第一个痛点是老人健康信息碎片化。血压、血糖、服药记录、体检报告散落在纸质档案和护士的随身本子上医生巡诊时要逐页翻找同一个数据可能被重复登记好几次。第二个痛点是护理任务没有闭环。排班表排完就完事了护工是否按时执行了翻身、喂药、体征测量这些任务管理层完全不知道万一老人出现异常事后连责任追溯都做不了。第三个痛点是家属沟通成本极高。家属询问老人情况工作人员要现去查、现场问回复不及时不说还容易因为信息不一致产生矛盾。1.2 系统边界与角色梳理针对这三个痛点我们把系统划分成六大核心模块老人档案管理、健康监测与预警、护理任务管理、床位与入住管理、费用与物资管理、家属端消息服务。角色方面一开始只规划了管理员、护士、护工、医生四种后来机构提出家属也需要登录查看老人健康数据又追加了家属角色最终权限模型变成了五种角色外加系统超级管理员。这里有一个容易被忽视的需求不同角色对数据的可见范围完全不同。比如护工只能看到自己负责楼层的老人列表医生能看到全院的健康数据但不能操作费用模块家属只能看到绑定老人的部分信息。这种数据权限的控制决定了后端的查询逻辑不能只靠简单的用户角色判断必须设计一套可配置的数据范围机制这个后面专门讲。2. 技术选型的现实理由为什么还是用Spring Boot打底说实话在2025年这个时间点谈技术选型可选项太多了微服务、云原生、Serverless……但对这样一个业务复杂、团队规模不大、交付周期紧张的养老管理系统来说Spring Boot仍然是最稳妥的打底方案。原因很直接生态成熟、招人容易、坑都有前人填过而且单体应用在数据事务一致性上比微服务简单得多。2.1 分层架构与目录结构我采用的是经典的四层结构Controller层接收请求、Service层处理业务、Mapper层操作数据库、Entity层映射实体。目录结构上我习惯按业务模块分包而不是按技术层次分包这样后期维护时找代码非常快。com.eldercare.system ├── controller # 接口层只做参数接收和结果封装 ├── service # 业务逻辑层事务控制都在这一层 ├── mapper # MyBatis接口配合XML文件 ├── entity # 数据库实体对象 ├── dto # 前端交互的数据传输对象 ├── config # 各类配置类比如MyBatis、Redis、拦截器配置 ├── common # 统一返回结果、异常处理、工具类 ├── task # 定时任务类集中放的地方 └── aspect # AOP切面日志记录和权限校验这种分包方式的优势是当你接到一个新需求比如“给家属端增加健康周报功能”你只需要顺着业务模块找到对应的controller、service、mapper文件改动的范围非常明确不会出现改一个功能要翻十几个目录的情况。2.2 关键依赖和版本选择版本选择上我吃过一次亏这里直接说结论Spring Boot 2.7.x 配 JDK 8 是最稳的组合尤其是对生产环境已有的老系统而言。如果你是新项目且团队愿意用 JDK 17那直接上 Spring Boot 3.x 也没问题但要注意 MyBatis、PageHelper 这些第三方库是否有对应的适配版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent依赖清单里我额外加了这几个MyBatis-Plus做单表CRUD和分页、Redis做缓存和验证码存储、Spring Security做认证授权、Hutool工具库处理日期和字符串、Fastjson2做JSON序列化、WebSocket用于给家属端推送实时消息。还有一个容易被忽略的依赖是spring-boot-starter-validation参数校验如果不引入这个接口层会堆满手工判断逻辑代码会很难看。2.3 初始化配置里被忽略的细节Spring Boot的自动装配原理很多人都能背出来但实际配置时有个小细节容易掉坑多环境配置文件的拆分。我用的是application.yml加application-dev.yml、application-prod.yml的组合通过启动参数--spring.profiles.activeprod切换环境。同时要注意数据库密码、第三方密钥这些绝不能直接写在配置文件中我这边是结合了环境变量注入和Jasypt加密生产环境配置文件里看到的是一串密文密钥本身放在部署服务器的环境变量里。spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/eldercare?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}还有一个小坑提醒一下Spring Boot 2.7.x 之后跨域配置推荐直接实现WebMvcConfigurer的addCorsMappings方法而不是用CrossOrigin注解到处标。前端的Vue项目打包后如果要和Spring Boot放在同一个服务里跑要注意静态资源路径和接口路径不能冲突我习惯把接口统一挂/api前缀前端打包产物放到static目录下。3. 核心表结构设计与建模思路数据库设计阶段我反复修改了四版才定稿。核心原因在于养老系统的数据关系比一般的管理系统要复杂得多——同一个老人既关联着健康档案、入住记录、床位信息又关联着护理评估、缴费流水和家属绑定如果一开始表结构设计得不够合理后期写SQL的时候会非常难受。3.1 老人档案与健康数据的关系设计老人基础档案我单独建了一张elder表字段包括姓名、身份证号、家属联系方式、紧急联系人、既往病史、过敏药物、入住日期等。这里有个容易犯的错误把健康指标直接作为字段加到elder表里。比如有人会设计成elder表里加blood_pressure、blood_sugar两个字段表面上看省事实际上完全违背了业务逻辑——血压血糖是持续产生的时间序列数据每个老人一天可能测量多次正确做法是单独建一张health_record表每次测量生成一条记录。CREATE TABLE health_record ( id bigint NOT NULL AUTO_INCREMENT, elder_id bigint NOT NULL COMMENT 老人ID, type varchar(20) NOT NULL COMMENT 指标类型blood_pressure/blood_sugar/heart_rate等, value varchar(50) NOT NULL COMMENT 测量值, unit varchar(20) DEFAULT NULL COMMENT 单位, measured_at datetime NOT NULL COMMENT 测量时间, created_by bigint DEFAULT NULL COMMENT 记录人设备采集则为设备ID, PRIMARY KEY (id), KEY idx_elder_time (elder_id, measured_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 床位、护理任务与排班的表关系床位管理看起来简单实际上牵扯到入住流程的完整性。我的设计是bed表记录床位号和所在房间、楼层check_in_record表记录每一次入住和退住的周期elder表通过一个current_bed_id字段指向当前床位。这样设计的理由是一旦发生换床或者暂时外出住院current_bed_id可以随时更新而历史的入住记录保留在check_in_record里方便统计入住率和床位周转。护理任务这块我建了四张表care_task定义任务模板比如“每两小时翻身”“每日早晚测量血压”care_plan记录每个老人的个性化计划care_assignment是具体到某一天某个班次的任务分配care_execution记录执行结果。这种设计把“做什么”“谁来做”“做没做”三层信息完全拆开当我需要生成月报统计“本月翻身任务完成率”时只需要关联care_assignment和care_execution两张表按状态字段聚合即可。4. 关键功能模块的落地与踩坑记录框架搭好之后真正的工作量在业务模块的细节实现上。这一节挑三个最有代表性的功能来拆解每个功能背后都有值得一提的设计决策和实际问题。4.1 健康监测设备的对接Modbus协议与WebSocket推送这家养老院采购了一批智能床垫和手腕式血压计设备数据通过一个本地网关以Modbus协议上传。一开始我打算自己解析Modbus报文后来发现网关厂商提供了基于MQTT的数据转发服务于是走了一条更简单务实的路——Spring Boot充当MQTT客户端订阅设备数据。Configuration public class MqttConfig { Bean public MqttConnectOptions mqttConnectOptions() { MqttConnectOptions options new MqttConnectOptions(); options.setServerURIs(new String[]{tcp://192.168.1.100:1883}); options.setCleanSession(true); options.setConnectionTimeout(10); options.setKeepAliveInterval(30); return options; } }订阅到数据后在消息处理方法里做单位转换和数据格式化然后写入health_record表。同时触发异常阈值判断——比如高压超过180或者心率低于50立即调用预警服务推送站内消息给值班护士、短信通知护士长、并且通过WebSocket实时推送到大屏监控端。这里要重点提醒设备数据的并发写入量虽然不大但很密集每台床垫每30秒一条数据五十台设备同时上报就是每秒近两条写入。虽然MySQL完全扛得住但如果是项目初期建议先加上Redis缓存最近一次测量值读操作优先从缓存取减少数据库压力。4.2 护理任务排班的定时任务实现护理排班是整个系统里逻辑最绕的模块。需求是这样的系统根据护理等级自动生成每日任务比如一级护理的老人每天需要6次血压测量、4次翻身、3次喂药提醒二级护理则减少频次。同时要支持护士长手动调整某个老人的任务计划。实现这个功能我用的是Spring Boot内置的Scheduled定时任务每天早上5点执行一次任务生成逻辑为当天排班的每个护工生成任务清单。Scheduled(cron 0 0 5 * * ?) public void generateDailyCareTasks() { ListCarePlan plans carePlanService.listValidPlans(); for (CarePlan plan : plans) { ListCareAssignment assignments buildAssignments(plan); careAssignmentService.saveBatch(assignments); } log.info(Daily care tasks generated, total: {}, plans.size()); }实际运营过程中发现纯定时生成满足不了需求有临时入住的老人、有临时取消的任务护工在APP端点了“确认执行”后如果超时未完成系统要自动升级提醒。这些都需要在任务任务调度之外增加触发机制。我最终的做法是定时任务负责生成“计划内任务”而所有“临时任务”通过消息队列Async异步写入保证高峰期不会因为线程阻塞影响主流程。另外Spring Boot的Scheduled默认是单线程执行的如果你的系统里有多个定时任务务必在启动类上加EnableScheduling的同时配置线程池否则多个任务会互相排队等待。4.3 家属端查看与消息通知家属端我采用的是Spring Boot Vue分离开发后端只提供RESTful API。这里重点说消息通知的设计。需求要求老人的健康数据出现异常、每月账单生成、护理计划变更时家属都能收到通知。我最初用的是短信但短信费太高一条一毛多一个月几千条下来成本不低。后来接入了微信公众号模板消息家属关注公众号并绑定老人之后后端通过调用微信接口推送模板消息成本为零。public void sendWechatTemplate(String openId, String templateId, MapString, String data) { String accessToken wechatService.getAccessToken(); JSONObject body new JSONObject(); body.put(touser, openId); body.put(template_id, templateId); body.put(data, data); // 通过RestTemplate调用微信推送接口 }如果只是做毕设项目没有真正的微信公众号资质也可以退一步用spring-boot-starter-mail的JavaMail发送邮件通知或者直接做站内信加WebSocket实时提示效果一样能演示完整。5. 多角色权限体系的安全落地权限体系是我花了最多时间调试的部分不是因为技术复杂而是因为养老机构的角色权限维度和常规企业系统不一样——一个护士长可能既要跨科室查看数据又要被限制不能修改某些关键字段医生可以看到生命体征趋势但不能操作缴费护工只能看到自己负责的老人。这种“看得见但动不了”的边界细分必须用RBAC再加数据权限过滤才能实现。5.1 基于RBAC的菜单与数据权限设计用户表、角色表、菜单表、用户角色关联表、角色菜单关联表这里不展开说明。关键在于数据权限的过滤方案MyBatis-Plus提供了DataPermissionInterceptor数据权限插件可以在SQL执行前自动拼接数据范围条件。例如护工登录后查询任务列表时拦截器自动追加WHERE care_assignment.nurse_id 当前用户ID而护士长登录时追加的就是WHERE care_assignment.floor_id IN (护士长管辖楼层)。这种方案的好处是业务代码里完全不用写权限判断逻辑Service层只写正常的业务查询权限规则集中在拦截器里配置。要是你没有用MyBatis-Plus也可以在Service层手动拼接查询条件但代码会冗余很多。另外一个很实用的技巧所有需要做数据权限的Mapper方法第一参数都传一个DataScope对象作为过滤条件载体这样既保持兼容又能手动覆盖默认规则。5.2 登录认证、接口鉴权与操作日志认证这块我选的是JWT Redis的组合。登录成功后生成JWT返回前端同时把token存在Redis中设置过期时间12小时。每次请求通过拦截器校验token是否有效同时校验该用户是否还有操作权限。这里有个细节JWT本身是无状态的一旦签发没法主动失效所以必须依赖Redis里的状态做二次校验。我踩过的坑是最开始只校验JWT签名导致修改密码后旧token依然能访问接口后来加上Redis校验才解决。操作日志用AOP切面实现是最省力的。定义一个Log注解标注在需要记录的方法上切面里获取方法参数、执行结果、当前用户ID、IP地址、操作耗时异步写入日志表。对养老系统来说操作日志不只是审计需要还承担着责任追溯的作用——假如老人出现跌倒事件家属质疑护理不到位系统里能够查清楚谁在什么时间给老人做了哪些护理操作。Aspect Component public class LogAspect { Around(annotation(operLog)) public Object around(ProceedingJoinPoint point, OperLog operLog) throws Throwable { long start System.currentTimeMillis(); Object result point.proceed(); long cost System.currentTimeMillis() - start; // 异步保存日志 loggerService.asyncSave(buildLog(point, operLog, result, cost)); return result; } }6. 部署与性能优化的实战清单系统开发完成之后部署和性能优化阶段又耗了两周。这里分享几个我实际遇到并解决的问题如果你想快速把项目跑起来这些经验能帮你少走不少弯路。6.1 查询性能优化分页、索引、慢SQL最核心的高频查询是“老人列表页”需要关联床位信息、护理等级、当前状态还要支持按姓名、楼层、状态筛选。数据量几百条的时候没感觉等业务跑了两个月、健康记录表到了几十万行之后列表查询明显变慢。排查下来发现慢的主要原因有两个一是多表关联时没有走索引二是health_record表的数据量增长导致联表扫描时间变长。解决方案很简单给elder表的name、current_bed_id字段加了索引给health_record表的elder_id和measured_at建了联合索引。同时把所有列表查询改成了分页查询统一使用MyBatis-Plus的Page对象避免一次查出全表数据。这里建议从项目初期就养成习惯任何列表接口一律分页不要图省事返回全量数据。慢SQL日志一定要在开发阶段就开启MyBatis的mybatis.configuration.log-impl设为StdOutImpl可以在控制台打印完整SQL配合druid连接池的监控页面基本能定位90%的慢查询问题。6.2 打包部署时的常见坑打包部署我经历了两个坑值得一提。第一个是前端Vue项目打包后放进Spring Boot的static目录刷新页面就直接404。原因是Vue的路由是history模式一旦直接访问/elder/list这个前端路由后端的DispatcherServlet会尝试寻找对应的Controller找不到就返回404。解决办法是在Spring Boot里加一个ErrorPageRegistrar把前端路由的404请求统一转发到index.html。第二个坑是打包产物太大jar包超过150MB每次上传服务器都特别慢。分析后发现一半的体积来自静态资源和第三方依赖。我在pom.xml里配置了Spring Boot Maven Plugin的excludes把前端静态资源单独放在服务器上用Nginx托管后端jar包瘦身到60MB左右。实际上生产环境建议前后端完全分离部署Spring Boot进程不处理静态资源让Nginx统一抗并发这是最省事的架构。6.3 容器化部署的额外建议如果你想把系统用Docker部署Dockerfile建议用多阶段构建第一个阶段用maven:3.8-jdk-8执行打包第二个阶段用openjdk:8-jre-alpine作为运行环境只复制jar包进去。这样构建出来的镜像体积可以从1GB以上降到200MB以内。FROM maven:3.8-jdk-8 AS build WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn package -DskipTests FROM openjdk:8-jre-alpine WORKDIR /app COPY --frombuild /app/target/eldercare-system.jar . EXPOSE 8080 ENTRYPOINT [java, -jar, eldercare-system.jar, --spring.profiles.activeprod]数据库如果用Docker跑MySQL一定要把数据目录挂载到宿主机否则容器一删数据全丢。我在测试环境犯过这个错误重新导入数据浪费了半天。生产环境的数据库建议直接部署在宿主机上不跑容器运维起来更省心。实际开发中还有一个和功能无关但很影响体验的事日志规范。这套系统我统一用logback作为日志框架输出格式里包含时间、线程名、级别、Logger名称、消息体。同时在application.yml里配置了按天滚动的日志策略保留最近三十天日志归档文件压缩后存到专门目录。当线上出现问题需要排查时直接grep关键词定位错误效率比翻控制台高得多。回过头来看这个项目技术上并没有用到什么炫酷的新框架核心能力全部建立在Spring Boot这个稳定基座之上。但真正的价值在于对业务的理解——怎么把老人的健康数据流、护理任务流、家属沟通流串起来让系统不再只是一个记录工具而能真正减少一线护理人员的工作负担、降低管理风险。如果你也在做类似的系统开发建议多花时间在业务调研和表结构设计上这两个环节做扎实了后面的代码开发反而是一路顺畅的。