
做毕设选题或者接外包项目的时候养老院管理系统是个出现频率很高的题目。我前前后后帮人做过几版从一开始的简单CRUD到后面真正能落地上线、有人天天在用的系统中间踩了不少坑。如果你现在正准备做这样一套系统或者正在为课程设计、毕业设计发愁这篇文章会把整个基于SpringBoot的养老院管理系统从需求拆解、技术选型、数据库设计、核心功能实现、部署上线全链路讲清楚给出一套可以直接复现的方案。这并不是一篇纯理论讲解。我在这套系统里实际用过的组合是SpringBoot 2.7作为后端基础框架MyBatis-Plus做持久层MySQL 8.0存数据Redis缓存登录状态和热点数据前端用Vue3 Element Plus搭建管理后台部署走Docker Compose。下面所有内容都是基于这套方案展开的每一处设计我都会说明为什么这么做以及哪些地方容易出问题。1. 项目整体设计与需求拆解1.1 养老院管理的核心业务场景养老院管理系统的难点在于它不是一个简单的信息登记系统而是一个融合了人员管理、空间资源管理、健康管理、财务计费的综合系统。你可以把养老院想象成一个特殊的酒店医院学校混合体老人是住客也是病人床位是核心资产护理是核心服务费用是敏感账目家属是外部的监督者。我刚接到需求时对方负责人给的说法是就是记录老人信息、安排床位、算费用。但真正把业务拉出清单之后至少包含这些维度老人的基本信息、健康档案、家属联系信息入住管理入院登记、床位分配、退住结算护理管理护理等级评估、护理记录、用药提醒健康管理体检记录、血压血糖趋势、健康预警餐饮管理膳食偏好、配餐计划费用管理床位费、护理费、伙食费、医疗费、押金探访管理家属到访登记员工管理护工排班、工作记录系统管理登录、权限、操作日志你如果只按老人信息表的增删改查来做最后交付的一定是个没人愿意用的玩具。真正能用的系统必须把上面这些业务串成一条线老人进来要选床位住下之后护工要写护理记录每个月要自动算钱家属来探访要登记走了要结算退费。任何一环断了系统在真实场景里就跑不起来。1.2 三类角色与功能边界做权限设计之前我先把用户角色收敛为三类管理员院长、行政、护理人员护工、护士、前台接待、收费。有没有必要再加一个家属端如果你想把项目做得更有亮点可以加但核心的三类角色必须优先做好。管理员看到全部数据管理员工账号、角色权限、基础配置床位、收费项目、查看统计报表。管理员不直接操作护理记录和收费这是为了避免既当运动员又当裁判的审计问题。护理人员操作老人档案、写护理记录、录入健康数据、处理用药提醒。护理人员不接触费用数据更不应该看到押金余额这是隐私也是职责边界。前台办理入住退住、接待探访登记、生成费用账单、收取费用。前台能看老人基本信息但不能编辑健康档案和护理记录。角色为什么要收得这么紧因为养老院系统上线之后是要给真实员工用的权限太松会出责任事故比如护工不小心改了账单或者前台改了护理记录后期追责非常麻烦。权限边界在需求阶段就划清楚后边编码会省很多事。1.3 功能模块全景梳理我把功能模块分成两个层次来梳理基础数据层和业务操作层。基础数据层解决的是系统里有哪些可被引用的基础信息包括老人档案、员工信息、床位资源、收费项目、膳食配置业务操作层解决的是每天实际发生的动作包括入住退住、护理记录、健康监测、费用结算、探访管理、统计报表。这种分层的直接收益是数据库设计和接口设计会清晰很多。举个例子床位管理在基础数据层就是一张床位的增删改查但在业务操作层床位状态的变化是由入住、退住、维修这些动作驱动的而不是直接改状态字段。所以在设计接口时我不会提供一个修改床位状态的裸接口而是通过入住、退住事务来间接更新床位状态这样数据才不会乱。2. 技术选型解析SpringBoot与其他组件的合纵连横2.1 为什么锁定SpringBoot而不是SSH或Spring Cloud很多人在考虑技术选型时会有疑问SSMSpring SpringMVC MyBatis还能用Spring Cloud又显得重到底该怎么选从我做这类管理系统的经验来看SpringBoot几乎是唯一不用纠结的选择。原因有三。第一SpringBoot的自动配置机制极大简化了SSM时代繁琐的XML配置内嵌Tomcat意味着一个jar包直接run起来这对单人开发或者小团队开发至关重要。第二SpringBoot的生态太成熟了几乎所有的中间件都有官方starterMyBatis-Plus、Redis、Sa-Token都有现成的集成方案遇到问题查资料也好查。第三对于课程设计、毕业设计或者小型商业项目SpringBoot是面试官和客户都认可的主流技术栈后续扩展到Spring Cloud也是顺理成章的事。这里提一个版本选择的建议如果你用的是SpringBoot 3.xJDK版本必须17以上有些老的第三方starter可能还不兼容踩坑几率不小。如果你不想在环境上折腾SpringBoot 2.7 JDK 8是最稳的组合业界存量系统也大多数跑在这个版本上我推荐用它。2.2 持久层选型MyBatis-Plus为什么比JPA更顺手持久层我直接选了MyBatis-Plus没有选Spring Data JPA。有人说JPA开发效率高但我实际写下来发现在养老院这类多表关联复杂、动态条件查询多的系统里MyBatis-Plus的灵活度明显更高。举个最典型的场景老人列表页的查询条件是动态的按姓名模糊查、按护理等级查、按入住状态查、按床位号查这些条件可能任意组合。用MyBatis-Plus的LambdaQueryWrapper可以非常自然地拼接条件LambdaQueryWrapperElder wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Elder::getName, name) .eq(careLevel ! null, Elder::getCareLevel, careLevel) .eq(status ! null, Elder::getStatus, status) .orderByDesc(Elder::getCreateTime);Wrapper的每个方法第一个参数是布尔条件为true才拼这个SQL条件这比手动写一大堆if判断和XML拼接简洁太多。同时MyBatis-Plus也保留了XML写复杂SQL的能力比如费用汇总统计、入住率计算这类复杂的报表查询我会直接写在XML里SQL的优化余地更大。简单查询用Wrapper复杂报表用XML这是我推荐的组合方式。2.3 整体分层架构与工程结构参考我的工程结构是这样分的照着这个结构建项目基本不走弯路elder-care-system/ ├── pom.xml ├── src/main/java/com/example/eldercare/ │ ├── config/ // 配置类Redis、拦截器、CORS、Sa-Token │ ├── controller/ // 控制层接收请求、参数校验 │ ├── service/ // 业务层核心业务逻辑 │ ├── mapper/ // 数据访问层MyBatis-Plus Mapper │ ├── entity/ // 实体类对应数据库表 │ ├── dto/ // 数据传输对象请求参数、响应封装 │ ├── vo/ // 视图对象给前端返回的聚合数据 │ ├── utils/ // 工具类日期处理、费用计算 │ └── exception/ // 全局异常处理 └── src/main/resources/ ├── mapper/ // XML SQL文件 └── application.yml分层的关键原则是Controller不写业务逻辑只做参数接收和结果封装Service是业务核心事务控制全部放在这一层Mapper只做数据访问。这个原则坚持下来后期排查问题会省很多事。我见过不少项目把业务判断写在Controller里导致一个接口几百行后期根本没法维护。3. 数据库设计一张床、一位老人的状态机3.1 核心表清单与设计思路数据库设计是这套系统最重要的部分甚至比代码还重要。养老院系统里大量业务本质上是对状态的流转老人从待入住到已入住再到已退住床位从空闲到占用再到空闲费用从待生成到已结算。状态字段我统一用tinyint数字枚举表示比字符串直观也比布尔值有扩展空间。我整理一下核心表设计清单表名用途关键状态字段elder_info老人信息status1待入住/2已入住/3已退住bed_info床位信息status1空闲/2占用/3维修elder_bed_record入住记录status1在住/2已退住health_record健康监测记录无nursing_record护理记录无medication_task用药提醒任务status1待执行/2已完成/3已过期fee_item收费项目定义无fee_bill费用账单status1待结算/2已结算/3已作废employee_info员工信息status1在职/2离职sys_user系统用户账号status1启用/2禁用sys_role角色表无visit_record探访记录无这张表不是一次定死的后边开发中会微调。但核心思路要提前想清楚凡是有关键状态流转的表必须有status字段凡是需要追溯历史的表必须有create_time和update_time凡是金额相关的字段一律用decimal精度给到(10,2)不能用float或double。3.2 老人档案与健康数据表的建模细节老人信息表是核心主表字段不要图省事只做几个实际业务需要的字段远比你想象的多。我落地的表结构是这些姓名、性别、出生日期date类型算年龄直接在SQL里用TIMESTAMPDIFF、身份证号加密存储展示时脱敏、床号冗余存储方便列表展示不直接join床位表、护理等级1级/2级/3级/特级、入住状态、家属姓名、家属电话紧急联系人建议存两个、既往病史、过敏史、当前用药文本字段textarea录入、入住时间、预交押金、备注。健康数据表容易犯的设计错误是一行一个指标还是一行多条指标混存。我采用的方案是health_record表一行存一次测量记录老人ID、血压收缩压、舒张压、心率、血糖、体温、测量时间、录入人、备注。这样可以最大化查询灵活性做趋势图时按时间范围查询该表做预警时按指标阈值过滤不用做复杂的行转列也不会有空字段雪崩的问题。3.3 费用模块的表设计要点费用是养老院系统里最敏感的模块设计不好后期对账会非常痛苦。我的做法是拆成三张表fee_item收费项目定义、fee_bill账单头、fee_bill_detail账单明细。fee_item定义了收费项目和单价比如床位费-单人间800元/月、护理费-一级1000元/月、伙食费500元/月数量单位是月。fee_bill是每月给每个老人生成的账单头记录老人ID、账单月份、应缴总额、实缴金额、结算状态。fee_bill_detail是明细行每条明细关联fee_item记录项目名称、单价、数量、金额、备注。这样拆的好处是月底生成账单时遍历该老人的在住记录和fee_item定义自动生成账单头和多条明细行如果中途某个项目要减免只改明细行不影响其他项目家属查账时能一目了然地看到这笔钱花在哪儿了少了很多扯皮。三张表的关联关系很简单fee_bill一对多fee_bill_detailfee_bill_detail多对一fee_item。4. 核心业务模块实现要点4.1 登录认证与权限控制用Sa-Token简化会话管理登录认证我直接推荐用Sa-Token而不是手写JWT拦截器。Sa-Token的API极其友好登录成功一行代码就完成会话创建StpUtil.login(userId);然后每个接口只要加一个注解就能校验登录和权限SaCheckPermission(elder:add)Sa-Token配合Redis存储会话天然支持集群部署和会话续期不需要像手写JWT那样自己管理过期时间、续期、踢人下线。权限模型就是标准的RBAC用户-角色-权限。sys_user和sys_role关联sys_role和sys_permission关联登录成功时Sa-Token自动把当前用户的权限码列表加载到会话里后面每个接口的鉴权都是O(1)的本地判断。权限设计上要特别注意三类边界护理人员不能访问费用模块前台不能修改健康档案管理员不能给自己分配超级权限。把这些边界在权限注解上写清楚后期就不会出现护工篡改了账单这种责任事故。实现时我用了注解级别控制少数需要细粒度判断的地方再在Service里写StpUtil.hasPermission做二次校验。4.2 老人入住与退住流程的完整实现入住退住听起来简单实际上是一个多表状态联动的复杂事务。入住不是简单insert一条老人记录而是一个事务里同时完成几件事校验老人状态必须是待入住且选中的床位状态必须是空闲更新elder_info的status为已入住同时冗余写入床号更新bed_info的status为占用插入elder_bed_record入住记录记录入住时间、入院原因、预交押金如果预交了押金同步生成一条押金账单明细这个流程我用Transactional注解整体包裹事务放在Service方法上。这里有个非常容易踩的坑在同一个类里方法A调用方法B如果B上有Transactional事务不会生效因为Spring事务是通过AOP代理实现的内部调用走的是this而不是代理对象。所以入住流程我直接写在一个Service方法里不拆成两个public方法互相调用避免事务静默失效。退住流程是对称操作校验当前在住、核对费用账单、生成退住结算单、释放床位、更新老人状态。退住比入住更麻烦的是费用处理当月费用要按天折算押金要扣减应缴费用后余额退回这些我放到下面费用计算一节详细说。4.3 费用自动计算与月度账单生成费用自动计算我实现了一个Spring的Scheduled定时任务每个月1号凌晨2点扫描所有在住老人为每人生成当月账单。为什么选凌晨2点因为这个时段业务系统基本没人操作数据库压力也最小生成账单的过程即使慢一点也完全不影响使用。费用项目的计算逻辑我实际落地是这样的床位费按整月计费如果月中入住或退住按天折算。公式是单价 / 当月天数 × 实际在住天数护理费按护理等级对应的单价同样按天折算伙食费一般按整月收不做折算具体看养老院的收费规则按天折算的时候钱的计算必须用BigDecimal而不是double。这个坑我印象太深了0.1 0.2用double算出来不是0.3是0.30000000000000004费用差一分钱客户都要找你掰扯。而且BigDecimal构造时一定要用字符串构造new BigDecimal(0.1)是对的new BigDecimal(0.1)会得到一个不精确的值。如果老人中途退住单独重算退住当月账单多退少补。账单状态我设计了三个枚举待结算、已结算、已作废。已作废用来处理账单生成错了要重新生成的场景不能直接删账单记录保留审计痕迹。4.4 健康提醒与待办任务的消息推送健康提醒是这个系统里最能体现业务价值的功能之一。我实现了一个用药提醒模块护工在系统里录入老人的用药计划比如阿司匹林 每天早8点一次。系统根据用药计划生成medication_task到了时间点任务状态变成待执行同时写入Redis的一个待办集合前端首页轮询读取当前登录护工的待办列表。后端实现不复杂核心是一个定时任务扫描到点的用药任务把任务状态置为待执行写入redis key格式我设计为care:todo:{employeeId}。前端Vue组件每隔30秒拉取一次即可看到最新待办勾选完成时再调接口回写状态。这里我故意没用WebSocket。为什么养老院系统是典型的内网低并发场景在线用户可能不到50个人轮询30秒一次的延迟完全可接受。引入WebSocket要考虑断线重连、心跳保活、消息可靠性一系列复杂度对这类系统来说是过度设计。能用简单方案解决的问题不要为了技术炫技增加维护成本。5. 前端界面与交互设计5.1 Vue3 Element Plus 页面规划前端我用的是Vue3 Vite Element Plus。为什么不用Vue2Element Plus是官方维护的Vue3组件库而且Vue2在2023年底已经停止维护新项目没有理由再用老技术栈。Vite的开发热更新速度比Webpack快一个量级改完代码秒级反馈开发体验完全不一样。页面规划按后台管理系统最常见的形式展开左侧菜单栏、顶部面包屑和账号信息、中间内容区。按业务模块配置路由/dashboard 首页仪表盘/elder/list 老人档案管理/bed/list 床位管理/admission 入住办理/nursing/record 护理记录/health/list 健康监测/fee/bill 费用账单/system/user 用户管理一套小后台系统路由大概在10个左右vue-router配置不复杂。需要注意的是权限菜单登录后根据后端返回的角色权限码动态生成左侧菜单这里要用到vue-router的addRoute方法动态注册。没有权限的页面菜单不显示路由不放行双保险更稳妥。5.2 关键页面设计思路老人档案列表页是使用频率最高的页面我设计时做了几个针对性优化顶部是查询条件区放姓名输入框、护理等级下拉框、入住状态下拉框、床位号输入框中间是el-table表格列设计为姓名、性别、年龄、床位号、护理等级、入住状态、家属电话、操作按钮操作列放详情、编辑、办理入住、退住四个入口。分页用el-pagination配合后端MyBatis-Plus的Page对象一套流程非常顺。入住办理页要特别提醒它不是简单表单而是分步操作。我把入住拆成三步用el-steps组件串联。第一步选择老人搜索待入住的老人并选中第二步选择床位用可视化格子展示空闲、占用、维修三种状态的床位点击选中第三步填写入住信息包括入院原因、预交押金、家属确认。分步表单显著降低填表压力也更贴近前台员工的实际操作习惯。健康监测详情页我放了简单的趋势折线图用ECharts实现血压和血糖的月度变化曲线。ECharts和Vue3配合很顺畅但要注意引入方式只按需引入echarts/core相关模块和用到的图表类型不要整包引入否则打包体积会大得离谱。6. 部署上线与常见问题排查6.1 Docker Compose 编排部署方案本地开发完成后部署我直接走Docker Compose把MySQL、Redis、后端、前端四个服务编排在一起。写一个docker-compose.yml示例如下version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: elder_care ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine ports: - 6379:6379 backend: build: ./elder-care-system ports: - 8080:8080 depends_on: - mysql - redis frontend: build: ./elder-care-web ports: - 80:80 volumes: mysql_data:后端Dockerfile的关键点是多阶段构建先用Maven镜像打出jar包再拷贝到只有JRE的轻量镜像里运行这样最终镜像体积从六七个百分点缩小到两百多兆部署和拉取都快很多。前端用nginx镜像托管dist目录注意要配try_files实现history路由回退否则刷新页面直接404。启动时用docker compose up -d一条命令全部拉起查看日志用docker compose logs -f backend。这套部署方案维护成本很低服务器上只要装了Docker和Compose就能跑不用在服务器上装Java环境、MySQL客户端这些乱七八糟的东西。6.2 典型问题排查会话失效、时区混乱、事务不生效我整理了几个实际项目中几乎必然会踩的坑直接列成速查表方便你排查问题现象常见原因解决方案登录后请求接口一直401前端axios请求头没携带Token在axios拦截器统一添加Authorization头定时任务生成账单时间偏移服务器时区是UTCMySQL连接串加serverTimezoneAsia/Shanghai容器加TZ环境变量事务方法内抛异常但数据没回滚同类内部方法调用导致代理失效事务方法写在独立Service或拆分不同Bean或改用编程式事务上传文件中文名乱码过滤器没有处理UTF-8字符配置CharacterEncodingFilter强制编码UTF-8列表按时间排序结果混乱日期字段是String类型没转Date所有日期用datetime类型实体类用LocalDateTime第一个人问题我刚开始联调时也遇到过Sa-Token把Token放在请求头里返给前端前端如果没在axios拦截器里手动带上每一次请求都是未登录态排查了一天最后发现就是一行header的事。第二个问题更隐蔽MySQL连接串没配serverTimezone所有时间字段和定时任务都比北京时间少8小时账单日期全乱。这些都是环境类问题和业务逻辑无关但往往最耗费时间。结尾这套系统我做下来最深的体会是养老院管理系统表面看是个技术项目实际到后期考验的是对业务细节的理解。床位的状态流转、费用的按天折算、权限的边界控制这些都比用SpringBoot实现CRUD要难得多。如果你正在做类似的系统我建议先花两天把业务流程在纸上画清楚再动手建表写代码能避免一半以上的返工。数据库设计时多看几遍业务清单把状态字段和关联关系想透比后期在代码里打补丁重要得多。最后再分享一个小技巧做完一个模块后用Knife4j自动生成接口文档前后端联调时把接口文档地址发给前端同事比传Word文档高效太多。这套SpringBoot Vue的模板其实可以复制到很多管理系统项目上比如校园讲座预约、社团管理、办公用品管理等核心架构是通用的差别只在于业务表的设计和核心流程的不同。希望这篇实战记录能给你省点时间少踩几个坑。