资讯动态

Spring Boot养老院管理系统:从表结构到权限控制的完整实现

发布时间:2026/10/17 8:19:10 来源:尧图企业网站定制
1. 养老院的纸质台账痛点才是这套系统的出发点我接触过不止一家小型养老机构管理方式基本还停留在“一张 Excel 表 一本纸质护理台账”的阶段。老人入住、床位调整、护理记录、费用收缴都靠人肉登记数据一多就开始乱同一个老人在两张表里性别不一致某张床明明已经空出来了但系统里没有释放家属来问上个月护理费怎么算的前台翻半天本子说不清楚。这些问题不是靠责任心能解决的而是需要一套能把“人、床、钱、护理过程”统一管起来的信息系统。这套基于 Spring Boot 的养老院信息管理系统就是把上述管理流程搬到线上。后端采用 Spring Boot 作为主框架持久层使用 MyBatis-Plus数据库用 MySQL配合统一异常处理、权限拦截、分页查询、事务控制这些标准化写法形成一套可运行、可演示、可二次开发的完整项目。项目交付物包含源码、文档、调试过程和讲解材料说白了就是“除了代码还把为什么这么写、碰到问题怎么排错也一并讲清楚”。这套系统适合谁来参考呢第一类是正在做课程设计或项目实习的 Java 后端学习者第二类是想拿一个完整业务系统练手、但是又不想从零去造轮子的开发者第三类则是养老机构里做信息化选型或内部系统建设的人员。项目本身的业务不算高深难的是数据模型怎么设计、权限怎么控制、费用和状态怎么流转。这篇文章会把这几块关键逻辑拆开来讲包括我实际调试时踩过的坑和最终采用的方案给你一条可以直接照着走的实现路径。2. 先把表结构和分层架构定好这是一切功能的地基很多新手拿到项目标题后第一反应是“开始写登录功能”这个顺序其实是错的。登录、角色、页面这些属于表现层真正决定系统能不能长期维护的是底层的数据库模型和工程结构。我在做这套系统时第一步反而是画表关系第二步才是建项目目录。2.1 分层目录怎么组织才能让人一眼看懂Spring Boot 项目最容易出现的问题是“所有类都堆在一个包里”。我习惯按职责划分出清晰的分层读者拿到源码后不用花太多时间就能定位到对应的代码com.example.eldercare ├── config # 配置类拦截器、跨域、MybatisPlus分页配置 ├── controller # 接口层接收请求、参数校验、返回Result ├── service # 业务层事务、状态流转、核心业务逻辑 │ └── impl # Service实现类 ├── mapper # 数据访问层MyBatis-Plus的Mapper接口 ├── entity # 数据库实体对应表结构的Java对象 ├── dto # 入参对象避免把实体直接暴露给前端 ├── vo # 出参对象按前端需要组装数据 ├── common # 统一返回、全局异常、常量等公共类 └── interceptor # 登录与权限拦截器这个结构看起来普通但它把每个类的职责卡死了。Controller 只做参数接收和结果返回不写业务代码Service 里才放事务和状态判断Mapper 只做与数据库的交互。这样做的最大好处是调试的时候链路很清楚接口报错先看 Controller 的参数对不对再进 Service 查业务条件最后怀疑 Mapper 的 SQL。同一批代码里如果有三个同学并行开发分好层之后各自改各自的合并冲突也会少很多。2.2 核心数据表老人、床位、护理、费用、访客我先列出这个系统里必不可少的几张表以及它们之间的关系。这个设计不是拍脑袋想出来的而是对着养老机构的日常工作一条条捋出来的。表名说明关键字段sys_user系统用户管理员/护工/前台/家属id、username、password、real_name、role、phoneelder老人档案id、elder_no、name、gender、birthday、health_status、status、room_id、bed_idroom房间信息id、room_no、floor、room_type、statusbed床位信息id、room_id、bed_no、status、elder_idcare_record护理记录id、elder_id、nurse_id、care_type、content、create_timevisit_record来访登记id、elder_id、visitor_name、relation、visit_time、remarkfee_record费用记录id、elder_id、fee_type、amount、status、period、create_time这里最核心的关系是房间和床位。一个房间对应多张床位一个床位同一时间只能入住一位老人。我在设计 elder 表时保留了 room_id 和 bed_id目的是一次查询就能拿到老人当前所在的房间和床号不用反复连表。代价是维护时要保证这两列和 room、bed 两张表的状态一致所以搬家、退住这类操作必须放到同一个事务里执行。另一个容易被忽略的表是 visit_record。养老院对访客管理是有实际要求的谁在什么时间探望了哪位老人需要留痕。这不是核心业务但把这个模块做出来整个系统的完整性会明显提升演示的时候也更有话可讲。2.3 为什么一定要保留逻辑删除和创建时间实体表里我基本都会加三个公共字段deleted、create_time、update_time。逻辑删除的做法是默认值 0删除记录时把deleted改成 1而不是真的执行 DELETE。理由很简单老人的护理记录、费用记录这些历史数据在法律或管理上都需要长期保存物理删除会造成追溯链条断裂。时间字段也值得多说一句。create_time 在插入时由数据库默认值生成update_time 在更新时自动刷新。MyBatis-Plus 里有TableField(fill FieldFill.INSERT)这种自动填充机制配合一个 MetaObjectHandler 实现类可以省掉大量重复的 setCreateTime 代码。这两个字段在做“本月新增了多少老人”“费用分布在哪个时间段”这类统计时是唯一的依据前期不保留后面想加也加不回来。2.4 初始化 SQL 写成什么样才算合格建表 SQL 我会在文档目录里单独放一份要求是“在干净环境里能直接执行”。下面这段是老人档案表的结构示例重点体现了字段注释、公共字段和索引CREATE TABLE elder ( id bigint NOT NULL AUTO_INCREMENT, elder_no varchar(32) NOT NULL COMMENT 老人编号, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint DEFAULT 1 COMMENT 性别1男 2女, birthday date DEFAULT NULL COMMENT 出生日期, health_status varchar(255) DEFAULT NULL COMMENT 健康状态描述, room_id bigint DEFAULT NULL COMMENT 房间ID, bed_id bigint DEFAULT NULL COMMENT 床位ID, status tinyint DEFAULT 1 COMMENT 状态1在住 2已退住 3临时外出, deleted tinyint DEFAULT 0 COMMENT 逻辑删除0正常 1已删除, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_bed (room_id, bed_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人档案表;懒加载查询在 MyBatis-Plus 中写起来相当省事比如“查询所有在住老人并关联出房间名”一条 LambdaQueryWrapper 就能搞定。但要注意连表查询的字段索引一定要建好否则数据量一上来简单的分页接口都会变得很慢。我自己就是因为忘记给 room_id 建索引在演示时遇到过页面卡顿的情况后来才在表中补上了联合索引。3. 登录认证和权限控制这是系统安全的最后一道门养老院系统的数据非常敏感涉及老人健康状态、家属联系方式、费用记录所以登录和权限必须分开处理。登录解决“你是谁”的问题权限解决“你能干什么”的问题。很多教程只教你做登录却忽略了角色区分这在实际项目中是过不了验收的。3.1 系统角色怎么划分才合理我把系统用户拆成四类角色每种角色看到的页面和能操作的接口都不一样角色权限范围典型操作admin 管理员全部模块用户管理、退住结算、全局数据查看nurse 护工老人的健康与护理数据新增护理记录、查看自己负责的老人列表reception 前台入住与接待相关老人登记、床位分配、来访登记family 家属仅限绑定老人查看老人动态、费用账单、预约探视这里最容易被做错的地方是“家属只能看自己关联的老人”。如果权限校验只写了“必须登录”那家属接口只要换个老人 ID 就能查到别人家老人的档案这是严重的越权漏洞。正确做法是从登录状态里拿当前用户再根据用户 ID 去查询他绑定的老人 ID而不是直接信任前端传进来的老人 ID。3.2 拦截器和自定义注解比引入整套安全框架更轻量的方案对于这类单体管理后台使用 Spring Security 当然规范但它自带的一整套过滤链、CSRF、Session 并发控制等机制对初学者来说配置成本不低而且一旦配错接口 403 都很难排查。我在这套系统里采用的是更轻量的方案自定义一个AuthInterceptor加上一个RequireRole注解在方法级别控制角色。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; // 放行静态资源和非Controller方法 } String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { throw new BusinessException(401, 未登录或登录已过期); } // 从Redis或Token中解析出当前用户信息 LoginUser loginUser loginService.getLoginUser(token); if (loginUser null) { throw new BusinessException(401, 登录状态无效); } // 把用户信息放入Request上下文后续接口直接使用 request.setAttribute(loginUser, loginUser); return true; }角色校验放到注解里接口上写一行就能完成RequireRole({admin, reception}) PostMapping(/elder/checkIn) public ResultString checkIn(RequestBody Valid CheckInDTO dto) { return Result.success(elderService.checkIn(dto)); }拦截器负责解析 token注解负责校验角色。这样每个接口的权限需求都写在了方法签名上代码可读性高调试时也不用翻长串配置。3.3 密码为什么不能用明文也不能只做简单 MD5我在初版代码里犯过一个错误密码直接用 MD5 存储。表面上看数据不是明文但其实现在很多在线平台都能反查 MD5 字典几乎等于裸奔。正确做法是使用 BCryptPasswordEncoder 的哈希算法每次加密结果带随机盐同一密码两次加密结果不同数据库被拖走也不用担心密码被批量还原。这里顺带提醒一句密码找回、重置这些功能不要做成“管理员能看到用户密码”的模式。管理员只能重置不能查看。这个设计既是为了合规也是为了避免内部人员把数据泄露出去。文档里我会专门写一段说明强调这一点。4. 入住、护理、缴费核心模块的代码是怎么串起来的一个管理系统的价值最后还是要落到核心业务好不好用上。流程设计得再完美代码写成一锅粥也白搭。这里我把三个关键模块的实现逻辑逐个拆开讲。4.1 老人入住登记为什么必须考虑事务和排他入住登记不是“插入一条老人记录”这么简单。它实际包含三个步骤创建老人档案状态设为在住把选中的床位状态改为已入住生成一条入住流水记录入住时间、经办人、初始健康评估。三步必须成功一起成功失败一起失败。如果只保存了老人信息床位状态没改那么再有人入住时就会发现同一张床又被分配了这种错误在业务上是灾难性的。所以 Service 层必须加事务注解Transactional(rollbackFor Exception.class) public Long checkIn(CheckInDTO dto) { // 1. 校验床位当前状态防止并发重复分配 Bed bed bedMapper.selectById(dto.getBedId()); if (bed null || bed.getStatus() ! 0) { throw new BusinessException(该床位不可用); } // 2. 创建老人档案 Elder elder new Elder(); elder.setName(dto.getName()); elder.setGender(dto.getGender()); elder.setRoomId(bed.getRoomId()); elder.setBedId(bed.getId()); elder.setStatus(1); elderMapper.insert(elder); // 3. 占用床位 bed.setStatus(1); bed.setElderId(elder.getId()); bedMapper.updateById(bed); // 4. 记录入住流水 checkInLogMapper.insert(new CheckInLog(elder.getId(), dto.getOperatorId())); return elder.getId(); }事务的坑在于 rollback 的条件。Spring 默认只在遇到 RuntimeException 时回滚如果业务代码里把异常 try-catch 吞掉了事务就不会生效。所以我在这套项目里统一使用自定义的BusinessException继承 RuntimeException并指定rollbackFor Exception.class保证任何异常都能让数据回到操作前的状态。4.2 护理记录掌握时间范围查询和分页这两件事护理模块使用频率最高护工每天都要填写翻身、喂药、体温测量、活动情况等。记录本身不复杂复杂的是查询条件组合。一个比较常见的页面是选择某位老人 护理类型 时间范围然后分页展示。MyBatis-Plus 的 LambdaQueryWrapper 配合 Page 对象写起来很顺手。需要注意时间字段的处理数据库用 datetime 存取Java 实体中用 LocalDateTime前端传参格式要统一。我一般使用JsonFormat(pattern yyyy-MM-dd HH:mm:ss)保证序列化格式查询时把开始时间和结束时间包装成起止两个字段。public PageResultCareRecordVO queryCareRecords(CareQueryDTO dto) { LambdaQueryWrapperCareRecord wrapper Wrappers.lambdaQuery(); wrapper.eq(dto.getElderId() ! null, CareRecord::getElderId, dto.getElderId()); wrapper.eq(StringUtils.hasText(dto.getCareType()), CareRecord::getCareType, dto.getCareType()); wrapper.between(dto.getStartTime() ! null dto.getEndTime() ! null, CareRecord::getCreateTime, dto.getStartTime(), dto.getEndTime()); wrapper.orderByDesc(CareRecord::getCreateTime); PageCareRecord page careRecordMapper.selectPage(new Page(dto.getPageNum(), dto.getPageSize()), wrapper); // 组装VO补充护工姓名、老人姓名等冗余信息 return PageResult.of(page); }这里有个细节值得展开护理记录的列表接口不该把整张表 JOIN 到前端而是用 VO 对象来组装返回结果。VO 里除了记录本身还可以带老人姓名、护工姓名前端拿到就能直接渲染不用再发第二个请求去查基本信息。做这个转换虽然多写几行代码但明显减少前端联调成本。4.3 月度费用自动生成和手动调整并存的方案费用模块我这边拆成了两条线。一条是固定费用自动生成另一条是临时费用手动录入。固定费用包括床位费、基础护理费、餐费这些按周期计算。我通过 Spring 的 Scheduled 注解在每月 1 号触发定时任务扫描当前在住老人按老人所在的房间类型和护理等级生成一张待缴账单。这样做的好处是财务人员不再需要每月手动算一遍系统直接给出应收列表。Component public class FeeGenerateTask { Scheduled(cron 0 30 1 1 * ?) public void generateMonthlyFee() { ListElder allLiving elderService.listLivingElders(); for (Elder elder : allLiving) { feeRecordService.generateFee(elder.getId(), 床位费, elder.getRoom().getRoomType().getPrice()); feeRecordService.generateFee(elder.getId(), 护理费, elder.getCareLevel().getPrice()); feeRecordService.generateFee(elder.getId(), 餐费, mealPrice); } } }临时费用则包含药品费、医疗耗材费、特殊照护服务费等这些通过一个手动录入接口处理。收费后状态变成“已缴”退住时再做结算。自动生成和手动调整并存既保证常规账单不遗漏又给异常情况留了处理入口。费用记录和老人的入住、退住时间要做好联动退住后就不再生成次月固定费用这一点我在定时任务的查询条件里加入了明确的“在住”过滤。4.4 统一返回与全局异常处理让接口层“瘦身”到底前端拿到的每个接口返回结构应当保持一致我在 common 包下定义了 Result 类和全局异常处理器。public class ResultT { private int code; private String message; private T data; // 省略构造方法和静态方法 }业务层抛出的 BusinessException、参数校验错误、未知异常统一收敛到 RestControllerAdvice 中处理。这样 Controller 里就不用写大量的 try-catch每个接口只关心自己的业务逻辑。别小看这个设计调试时它的价值会迅速体现出来无论哪里出错返回的 JSON 结构都是固定的前端统一弹错误提示后端看日志定位异常类型不用每个接口各写各的错误格式。5. 调试纪实从“跑不起来”到“正常演示”的完整排错过程这部分我刻意放在功能讲解后面因为实际开发中 80% 的时间都花在排错上。我把这套系统里出现频率最高、也最具代表性的几个问题整理了出来每个都附上我是怎么定位的。5.1 启动失败别看最后几行报错要往上看 Caused by新手最常见的误区是项目启动失败后只盯着控制台最后那几行日志看。其实 Spring Boot 启动异常的关键信息通常隐藏在中间的Caused by里。比如数据库连不上日志最后可能是“APPLICATION FAILED TO START”但真正的线索是中间那句“Access denied for user”。我遇到的一次启动失败就是 MySQL 8 的驱动配置问题。MySQL 8 之后驱动类改成了com.mysql.cj.jdbc.Driver如果还在用老的com.mysql.jdbc.Driver虽然有时候能启动但会告警。更要注意的是连接串里的时区参数spring: datasource: url: jdbc:mysql://localhost:3306/eldercare?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456没有serverTimezone时新版本驱动在连接时经常报“Server returns invalid timezone”一类信息。这个错误极其常见解决办法就是显式指定时区。5.2 页面 404静态资源丢在了错误目录另一个高频问题是 Controller 接口调通了但页面或静态资源 404。Spring Boot 对静态资源的默认搜索路径包含classpath:/static/、classpath:/public/等。如果你把 HTML、JS、CSS 放到了classpath:/templates/目录下面它默认是给模板引擎用的直接用浏览器访问静态路径是进不去的。我的建议是纯前后端分离的接口项目把前端打包后的静态文件放到 static 目录如果要用模板渲染就在配置文件里把模板引擎的 prefix 和 suffix 配好两种方式不要混用。排查时第一件事就是看项目结构确认资源路径和配置是否一致。5.3 分页失效查出来所有数据原因基本都是插件没注入MyBatis-Plus 的分页不是天然可用的必须显式注入分页插件。很多同学复制了别人的代码却发现调用 selectPage 结果返回了全部数据多半是因为少了下面这段配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置缺失的时候MyBatis-Plus 会“安静地”退化为全表查询接口不会报错但性能完全不对数据量一大页面直接卡死。我在项目里把这个配置类单独放在 config 包下并且在文档里用加粗标注“分页必须先配插件”就是因为踩过这个坑。5.4 接收不到 JSON 参数八成是 Content-Type 或者字段名不一致接口接收 RequestBody JSON 对象时经常出现参数为 null排查思路按两个方向走一是看请求头 Content-Type 是否为application/json;charsetUTF-8二是看前端传参的字段名和后端 Java 属性是否对齐。有一个细节值得特别提醒前端如果传的是createTime而后端对象字段是create_time在 JSON 序列化框架里并不一定自动转换。如果项目启用了蛇形命名映射可能没问题如果没有就需要给字段加 JsonProperty 或者统一下发 DTO 时用驼峰命名。我在这套系统的 dto 里统一使用驼峰命名同时数据库字段用下划线靠 MyBatis-Plus 的默认驼峰转换来处理中间映射这个方案最省心。5.5 越权和数据串号从 Session 里取用户而不是相信前端传参还有一次印象深刻的问题家属端接口能查到别人的老人信息。排查后发现是 Controller 里直接用前端传来的 elderId 去查数据完全没有校验这个老人是否属于当前登录用户。这种漏洞在演示时一旦被问“你怎么保证安全性”现场会非常被动。修复方法很简单在接口上加一层归属校验先根据当前登录用户 ID 查他绑定的老人 ID 列表再判断请求中的老人 ID 是否在列表内不在就抛“无权访问”。权限拦截器解决的是“能不能登录”的问题这部分校验解决的是“登录后能不能动这条数据”的问题两者缺一不可。5.6 事务不生效同一个类里的自调用是最大的坑还有一种隐蔽的问题事务方法明明写了 Transactional但数据就是没回滚。原因往往是方法被同一个类里的另一个方法直接调用了。Spring 的事务是通过代理类实现的同类内调用走的是 this 引用绕过了代理事务自然不生效。我的处理办法是把“入住登记”这种需要事务的方法放到独立的 Service 类中Controller 调用 ServiceService 内部不要出现自调用。如果业务上必须在一个类里协作可以通过注入自身的代理或拆分成多个 Service 来解耦。排查这类问题最简单的方法是看日志里有没有“Creating new transaction”输出没有就是事务没被代理到。6. 源码、文档、调试、讲解怎么把一套项目“交付”得像样最后这部分想聊聊交付层面的东西。很多人以为“做完功能”就等于“项目完成”但真正让一个项目有说服力的是别人拿到你的源码和文档后能不能快速看懂、跑起来、照着演示。6.1 源码里该有的注释和命名规范源码命名我坚持“至少做到见名知义”。Controller 类名用模块名结尾比如ElderController、FeeController方法名直接体现行为比如checkIn、checkOut、getCareRecords。注释不是越多越好而是要在关键业务处写清楚“为什么”。比如入住登记那段代码注释写“校验床位状态防止重复分配”就比写“判断床位是否为空”更有价值。前者解释了设计意图后者只是把代码翻译成人话。调试和评审时别人是通过注释来理解你的思考过程的这一点的确很加分。6.2 文档怎么写才能让别人 10 分钟跑起来文档的优先级排序很重要。README 放最前面里面必须是“下载代码后如何启动”的完整步骤环境要求、数据库初始化脚本的执行方式、配置文件修改点、默认账号密码、访问地址。然后才是数据库设计说明、接口文档、部署说明。数据库设计文档要画出每张表的核心字段并解释表与表之间的关系。接口文档不用做到商用级别但至少要把每个接口的用途、入参、出参列出来。我发现一个很实用的小技巧把启动过程容易出现的问题整理成 FAQ 放在文档末尾比如“端口被占用怎么办”“MySQL 版本不一样怎么处理”这样对方遇到问题时不用四处求助也侧面说明你确实经历过调试过程。6.3 讲解材料的逻辑顺序跟着请求链路走如果要做讲解最容易被认可的讲法不是从页面开始而是从一个请求的完整链路开始用户点了某个按钮前端把 JSON 请求发到 ControllerController 做参数校验后交给 ServiceService 处理业务和事务再通过 Mapper 跟数据库交互最后把结果一层层返回。以“老人入住登记”为例这一条链路涵盖了事务、状态变更、权限校验、统一返回是天然的最佳讲解素材。讲解时还要准备几个“为什么”的问题为什么床位状态要先查再改为什么密码要用 BCrypt为什么分页查询要配插件这些问题的答案越具体越能体现你真正理解项目而不是只会背 PPT。6.4 这套系统还能往哪些方向扩展项目交付后不是终点我在这套系统的迭代过程中还考虑了几个扩展点。第一个是缓存优化热点数据比如房间状态、床位列表可以放到 Redis 里减少数据库压力。第二个是消息通知老人状态异常或者探视申请提交后可以给管理员推送通知技术选型用一套简单的 WebSocket 或者对接消息队列都行。第三个是数据可视化按月份统计入住率、按护理类型统计工作量、按费用科目统计收入把统计结果用折线图或柱状图在前端展示。这些扩展方向不一定要全部实现但在文档里写清楚“未来可以怎么做”会让项目的完整度和思考深度提升一个档次。实际开发时也可以挑其中一个方向做个简单版本作为进阶亮点。我个人的体会是这类信息管理系统做一遍最大的收获不是“会用 Spring Boot 写接口”而是学会怎么从业务里抽象出数据模型怎么把不稳定的状态变化用事务和字段设计控制住以及怎么通过日志和排查思路去解决真实环境中的问题。把这几点真正走通以后再接触更复杂的业务系统基本思路和项目实践能力是可以平移过去的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑