
做养老院管理系统这类Spring Boot项目十个人里有八个是冲着毕业设计去的剩下两个是接外包或者单位内部要搞信息化。先别管你是哪种这个题目的核心其实不在养老院三个字而在信息管理系统这六个字——老人档案、床位分配、护理排班、费用统计、家属探访本质就是一套典型的企业级CRUD业务Spring Boot做这件事属于标准打法没有花哨的高并发、没有分布式难点全在业务表的规划和代码的组织上。我前段时间刚好把一个完整的养老院信息管理系统从零搭到交付源码、数据库脚本、设计文档都整理齐全了。这篇文章就根据这个项目的实际开发过程把系统设计思路、表结构、核心代码、部署步骤和那些不写进文档里的坑全部捋一遍。适合三种人看正在纠结毕设选题的在校生、需要快速搭建中小型管理系统的开发者、还有接到养老机构信息化需求但不知道从哪里下手的工程师。文章是按能不能直接照着做的标准写的看到能抄的地方就直接抄。1. 养老院信息管理系统的业务边界先搞清楚要管什么很多人拿到这类题目就急着建表写代码其实第一步应该做需求拆解。养老院的日常管理跟普通公司管理差别很大它的核心对象不是员工和订单而是老人和护理服务围绕这两个核心衍生的业务链路是固定的。1.1 核心业务模块与原理解析一个能真正在养老院跑起来的信息系统至少要覆盖六个模块老人档案管理、床位与房间管理、护理人员排班、健康记录追踪、费用流水统计、家属探访登记。以老人档案管理为例它不只是存一条姓名和身份证号而是包含入院登记、家属联系人、既往病史、用药禁忌、紧急联系人等多个子信息。这些信息在系统里往往是多张表关联的。入院登记时需要选择房间床位老人入住后每天的护理记录又要挂在老人ID下面——这就是为什么表设计阶段就要明确主外键关系否则后面写业务代码的时候会不停返工。床位管理是养老院系统比较有特色的模块。床位是有状态的空闲、已入住、预定、维护中。排床时要考虑老人的自理能力是否匹配房间类型比如半自理老人不能安排在高楼层且没有电梯的房间。这些规则落到系统里其实就是床位分配时的状态校验和楼层约束逻辑。1.2 利益相关方角色与权限设计系统用户一般分三类系统管理员、前台/财务人员、护理人员。管理员的权限是配置类的比如维护系统用户、查看全部数据前台做入出院登记和探访管理护理人员主要访问健康记录和自己名下的老人列表。权限设计用Spring Boot的拦截器加注解就能实现不必上Spring Security那套复杂的东西除非是毕设想体现技术深度那另说。我做的版本里用的是自定义拦截器校验登录状态和角色拦截器注册时配置放行路径比如登录页、静态资源其余的路径统一走校验逻辑角色不匹配就跳转到无权限提示页。这套方案对中小系统来说维护成本低、逻辑直接比引入一套完整的Security配置链更容易被接手。1.3 业务流程闭环示例举个完整的业务路径你就明白表结构为什么要这样设计了新老人入院 - 前台登记老人基础信息 - 选择可用床位 - 生成入院记录并更新床位状态为已入住 - 系统自动创建当月的费用账单床位费基础护理费 - 护理主管为该老人指定责任护工 - 每日护理记录写入健康档案 - 月末财务生成费用汇总 - 家属通过探访登记系统预约探访时间。这条链路涉及六张以上的表任何一环字段缺了后面统计就会出问题。你写代码时一定要在脑子里过一遍这条流程确认每个环节的数据是从哪张表来、又写到哪张表去。这就是所谓业务驱动设计比先写实体类再去凑业务要靠谱得多。2. 技术选型为什么这套组合最适合快速交付能交差养老院信息管理系统选型第一原则是稳第二原则是别人能接手。我之前见过有人用微服务架构做这类系统十几个中间件堆上去最后连自己都跑不起来纯属给自己挖坑。2.1 技术栈清单与选择理由后端我选的是Spring Boot 2.7 MyBatis Plus MySQL 8.0前端用的Thymeleaf模板加Bootstrap这套组合在中小型管理系统里非常常见原因有三个第一Spring Boot 2.7是目前兼容性和稳定性最均衡的版本网上资料多遇到问题搜一下就有答案。没必要追求3.x新版本新特性在这个项目里根本用不上。第二MyBatis Plus对单表CRUD的简化是碾压级的BaseMapper里已经内置了增删改查和分页查询你只需要写复杂多表查询的XML开发效率能提升一大截。第三Thymeleaf是服务端渲染不用解决跨域问题也不需要考虑前端打包配置对于以功能完整为首要目标的项目来说是最省心的方案。2.2 项目分层架构说明代码结构我习惯于分成五层controller、service、mapper、entity、common。controller只做参数接收和响应封装service里写业务逻辑mapper管数据库操作entity对应用户表结构common放全局异常处理、统一返回结果类、工具类等。service层这里有个建议接口加实现类的写法虽然啰嗦但建议保留。一方面这个项目很可能要写文档类图画起来清晰另一方面后续做扩展的时候接口的存在能避免大面积改调用方的代码。我在项目里就是严格按照这个分层写的文档配的类图基本不需要额外调整就能用。2.3 统一返回格式与异常处理前后端交互的数据格式从第一天就定义好永远不要改。我用的ReturnResult结构包含三个字段code200成功500失败、msg提示信息、data业务数据。所有controller方法统一返回这个类型前端根据code判断操作是否成功。配合全局异常处理器业务代码里只需要抛出自定义的BusinessException异常信息会统一封装成ReturnResult返回给前端。这样写的好处是接口风格高度一致文档里描述API就非常简单每个接口的参数、返回示例都是同一套格式。3. 数据库设计养老院业务的核心表结构拆解数据库设计是这类系统的灵魂。表建得合理后面全是顺风局表建得随意写service的时候会不断怀疑人生。我把这个项目的表结构做了一次完整的梳理下面挑核心的几张表详细说。3.1 老人档案、床位与用户三张核心表老人基本信息表old_people字段大致如下id主键自增前端展示时注意Long精度问题后面踩坑部分详细说name老人姓名id_card身份证号需要设置唯一索引同时做好18位校验否则脏数据会很多gender、age性别、年龄年龄也可以不存根据身份证实时计算看具体需求phone老人本人联系电话如有health_level自理能力等级自理/半自理/全护理这是床位分配时的重要依据room_id房间ID关联room_info表bed_id床位ID关联bed_info表status在院状态在住/出院/请假entry_date入院日期create_time、update_time创建和更新时间所有表都应该有这两个字段床位表bed_info更重要它的核心是状态流转。字段包括bed_no床位编号、room_id所属房间、bed_type单人间/双人间/多人间、status空闲/已入住/预定/维护以及一个bed_level字段标记床位适配的护理等级。为什么要有这个bed_level因为一个完全不能自理的老人不能安排到普通双人间这是管理规则落到数据上就是bed_level要大于等于老人的health_level。用户表sys_user就是登录用户表包括username、passwordBCrypt加密存储、real_name、roleADMIN/前台/护理、status启用/停用。密码明文存的人基本没过过脑子数据库一泄露全员裸奔至少用BCrypt。3.2 健康记录、费用与探访表的设计要点健康记录表health_record字段有record_date体检日期、blood_pressure、heart_rate、temperature、symptom_desc症状描述、nurse_id记录人、old_people_id老人ID。这张表是护理人员最常操作的表所以一定要建联合索引( old_people_id, record_date )否则养老院住几百号人每天几百条记录查一个老人的历史记录时全表扫描会特别难受。费用表(fee_record)比较容易被忽视但我建议设计时考虑两个细节一是fee_type字段要区分床位费、护理费、餐饮费、医疗费等二是要设计pay_status未缴/已缴/部分缴纳和pay_time方便月底对账。系统还可以在老人入院时自动生成一条初始账单这个操作在service层完成不要依赖手动插入。探访登记表visit_record相对简单字段就是老人ID、访客姓名、联系电话、关系、探访时间、登记时间。注意探访时间要做预约和到访区分因为现实中家属通常先预约现场再核销不设计预约状态的话前台管理会很混乱。3.3 数据库初始化脚本与演示数据项目配套的数据库脚本我分成了三个文件schema.sql负责建库建表data.sql用于插入基础配置数据比如管理员账号、初始房间床位数据demo_data.sql存放演示数据几十位老人信息、健康记录等。这样区分的好处是交付出去的数据库脚本其他人导入后能直接看到效果不用自己临时造数据。导入时注意MySQL的字符集建库语句统一用utf8mb4因为老人生理数据记录中可能出现生僻字、特殊符号utf8mb4才能真正兼容。4. 核心功能实现从登录鉴权到业务闭环的代码级拆解这一章是实操的重点。我按登录 - 老人档案 - 床位分配 - 费用统计这条主链路来讲代码怎么写中间穿插说明MyBatis Plus的关键用法。4.1 登录鉴权与拦截器链路登录逻辑其实被很多人写复杂了。真实项目中登录接口做的事情就是接收用户名密码 - 查sys_user核对状态是否启用 - 用BCrypt匹配密码 - 登录成功把用户信息放进Session同时写入登录日志表。Override public ReturnResult login(String username, String password) { LambdaQueryWrapperSysUser wrapper new LambdaQueryWrapper(); wrapper.eq(SysUser::getUsername, username); SysUser user userMapper.selectOne(wrapper); if (user null) { throw new BusinessException(用户不存在); } if (!BCrypt.checkpw(password, user.getPassword())) { throw new BusinessException(密码错误); } if (user.getStatus() 0) { throw new BusinessException(账号已被禁用请联系管理员); } // 记录登录日志略 return ReturnResult.success(user); }拦截器是最容易被漏掉的部分。很多人写了登录接口就去测别的功能了结果发现直接访问某个业务URL也能通因为没有加拦截器。自定义一个LoginInterceptor实现HandlerInterceptor接口的preHandle方法从Session里取用户取不到就重定向到登录页。然后在一个WebConfig配置类里通过addInterceptors方法注册注意要excludePathPatterns把/login、/css/、/js/、/images/这些放行否则样式全部丢失页面会很难看。4.2 老人档案管理的分页与条件查询老人管理页是系统使用频率最高的页面必然是列表页加条件搜索。用MyBatis Plus做分页查询非常直接Override public IPageOldPeople pageOldPeople(int pageNum, int pageSize, String name, Integer healthLevel) { PageOldPeople page new Page(pageNum, pageSize); LambdaQueryWrapperOldPeople wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(name), OldPeople::getName, name) .eq(healthLevel ! null, OldPeople::getHealthLevel, healthLevel) .orderByDesc(OldPeople::getCreateTime); return oldPeopleMapper.selectPage(page, wrapper); }注意这里有个MyBatis Plus使用细节分页功能需要在配置类里注册PaginationInnerInterceptor否则selectPage返回的分页数据是不对的常见现象是total一直为0。这个问题不报错但结果错很容易被忽视。配置方式如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }4.3 床位分配的状态流转代码床位分配是一个典型的事务操作。选床时不仅更新老人表的bed_id还要更新床位表的status字段两个操作必须在一个事务里否则会出现老人分配成功但床位还是空闲状态的数据不一致问题。Transactional(rollbackFor Exception.class) Override public void assignBed(Long oldPeopleId, Long bedId) { BedInfo bed bedMapper.selectById(bedId); if (bed null || !空闲.equals(bed.getStatus())) { throw new BusinessException(所选床位不可用); } OldPeople oldPeople oldPeopleMapper.selectById(oldPeopleId); oldPeople.setBedId(bedId); oldPeople.setStatus(在住); oldPeopleMapper.updateById(oldPeople); bed.setStatus(已入住); bedMapper.updateById(bed); }Transactional这个注解是这类系统事务操作的核心。记住rollbackFor Exception.class这个属性一定加上默认情况下它只回滚RuntimeException而你的业务异常BusinessException如果继承的是Exception不加这个属性就不会触发回滚线上会出现半成功半失败的脏数据。4.4 费用统计的SQL写法月末费用统计报表是这个系统老板最看重的功能一般会做一个按月份汇总的门店总览页面。统计逻辑无非是sum和group by但要注意钱的单位问题数据库里金额统一用分来存用int类型避免double的精度误差。页面展示时再除以100。不要嫌麻烦等到有三五个金额相乘相加的场景你就知道整数运算能省多少事。SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(fee_amount) AS total_amount, COUNT(DISTINCT old_people_id) AS pay_people_count FROM fee_record WHERE pay_status 已缴 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC;5. 从源码到运行环境搭建、配置与打包步骤我拿到过不少运行不起来的源码一半以上是环境配置问题代码本身根本没毛病。这一节把从导入到跑通的完整步骤写清楚每一步我都会附上配置内容跟着做基本能一次过。5.1 环境准备清单开始之前先确认本机环境以下是我推荐的版本组合JDK 1.8不要装JDK 17跑Spring Boot 2.7会有兼容性问题Maven 3.6.3及以上MySQL 8.05.7也可以但部分SQL函数略有差异IDEA社区版够用但强烈建议用旗舰版自带数据库工具方便查看数据注意项目导入后先检查Maven镜像源。国内网络环境建议在settings.xml中配置阿里云镜像否则下载依赖的过程能让人等到崩溃。这不是项目问题是构建环境问题别把时间耗在这上面。5.2 application.yml配置详解数据库连接配置是跑通项目的前置条件必须根据自己的MySQL账号密码进行修改。配置内容如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/nursing_home?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword thymeleaf: cache: false mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: autourl里的serverTimezoneAsia/Shanghai必须写MySQL 8.0默认时区是UTC不指定会遇到时差8小时的问题查询出来的时间会莫名其妙少了几个小时。map-underscore-to-camel-case设为true数据库列名的下划线风格才能自动映射到实体类的驼峰属性比如create_time映射到createTime。5.3 数据库导入与初始账号用Navicat或命令行执行schema.sql和data.sql。导入完成后查看sys_user表里的初始管理员账号通常就是admin/admin123之类的初始密码登录后第一时间修改。如果数据库脚本里密码字段是明文说明这个项目的源码质量不高建议自己用BCrypt加密后再存入修改方法很简单写一个测试代码用BCryptPasswordEncoder加密一下就行。5.4 打包发布与部署开发环境下直接IDEA运行Application主类即可。对外交付时一般用Maven打成jar包mvn clean package -DskipTests打完包后在target目录下会生成一个jar文件部署命令java -jar nursing-home-system.jar生产环境建议在启动命令里显式指定配置文件java -jar nursing-home-system.jar --spring.config.location/opt/app/application.yml这样数据库密码这些敏感信息不用打进包里服务器上单独维护一份配置文件安全性高很多。6. 我实际开发中踩过的坑这些坑不写进文档但很要命最后这部分是全文最有价值的地方。这个项目从开发到交付有四个坑让我记忆深刻全部是网上的教程帖不会告诉你的。6.1 MyBatis Plus字段映射的自动填充失效问题我在实体类里对createTime和updateTime字段用了TableField(fill FieldFill.INSERT)或TableField(fill FieldFill.INSERT_UPDATE)注解本意是让插入和更新时自动填充时间结果跑起来发现create_time一直是null。排查了半天才发现问题自动填充需要实现MetaObjectHandler接口并在配置类里注册成一个Bean才行。如果没有这个handler注解等于白写。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }6.2 前后端交互时Long型主键的精度丢失数据库主键是自增Long类型值超过JavaScript的Number安全范围2^53后前端拿到的ID末尾会变成0000。这个问题在你本地测试数据量小的时候根本不会暴露等导入演示数据、ID超过9007199254740992的后几位时修改记录就会莫名其妙失败。解决办法是统一在序列化层面把Long转成String输出在Spring Boot里可以加一个Jackson配置Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; } }6.3 分页查询时total为0的假象这个坑前面提到过PaginationInnerInterceptor一定记得注册。但还有一种情况更隐蔽如果你手动在Mapper XML里写了selectList这种不带分页的方法只是页面传了pageNum和pageSize但SQL里并没有LIMIT数据会全部查出来页面却显示第一页。排查这类问题时先把MyBatis的SQL日志打开前面配置里的log-impl看控制台打印的SQL语句是否真的带了LIMIT关键字。SQL是判断数据层有没有问题的第一证据不要靠猜。6.4 身份证号重要但不要设置成绝对不能为空最后说一个业务层面而不是技术层面的坑。老人在养老院的实际情况是部分高龄老人证件材料不全代办的家属可能只提供了姓名和联系方式身份证号缺失是真实存在的数据现状。如果你把id_card设计为NOT NULL且唯一索引会把前台录入卡死数据灌不进去最后只能改表结构。我的建议是身份证号允许为空但加上唯一索引MySQL的联合唯一索引对NULL值不生效多个NULL不会触发冲突前端做格式校验遇到空值提示请补充身份证号但不阻断保存。养老院这类信息管理系统本质上没有多高深的技术门槛代码量也不算大真正拉开差距的是业务思考的完整度——表结构是否覆盖了所有角色状态流转有没有漏洞统计口径是否清晰。我在交付文档时总会加一段已知限制与后续扩展建议比如后续可以做的家属小程序端、安防门禁对接、物联网床垫体征数据上报这些都是在当前表结构基础上再延伸的方向。源码和数据库文档拿在手里与其照抄不如顺着这套架构往上叠自己的业务树这才是这类项目对你真正的价值。