简介:面向Java后端开发与毕业设计人群,这份材料是一套基于Spring Boot的社区智慧养老监护管理平台设计与实现源码及论文配套资源。平台围绕管理员、后勤人员、护工、体检员、用户五类角色构建闭环业务,覆盖房间信息与入住管理、老人健康状态档案、物资申请审批、留言反馈、公告发布等主要模块,适合课程设计、毕业设计或Spring Boot综合项目学习。包体共473个文件,约23.78MB,以java后端、vue前端、sql数据库脚本为主,配合xml配置、js交互、svg图标和bat一键部署脚本,可帮助使用者快速跑通环境并理解前后端交互。目前已有241人学习。借助角色权限划分、数据库表结构以及部署脚本,使用者不仅能复现完整的养老监护管理流程,还可参考论文说明进行功能扩展,是实战性较强的Spring Boot入门与进阶参考资料。
1. 社区智慧养老监护平台:一个Spring Boot工程如何撑起五类角色
社区养老服务站的护工每天下班前要核对十几个房间的入住老人,体检员要随时查老人的慢性病史,后勤人员要盯着物资申请有没有人处理,而家属最关心的是留言有没有人回。这套基于springBoot的智慧养老监护管理平台,把五类角色塞进一个Spring Boot服务里:管理员管房间和老人档案,后勤人员查反馈和物资申请,护工看入住和留言,体检员看健康档案和公告,用户提交留言和物资申请。对做java毕设或课程设计的人来说,它最大的参考价值不是前端页面,而是数据模型怎么落、角色权限怎么切、业务流程怎么保证不越权不重复。下面重点拆三个部分:Mysql表结构设计、基于拦截器的权限控制、物资申请与留言的状态流转,最后给出一套答辩现场用得上的构建和数据验证方案。
2. 老人、床位、入住关系的Mysql表结构设计与状态字段取舍
2.1 一张sys_user承载五类角色:为什么先区分角色再建业务表
平台里有管理员、后勤人员、护工、体检员、用户五种身份。很多人在课程设计里习惯建五张用户表,后面做登录和权限判断时全是if-else,每张表字段还高度重复。这个项目的做法是只建一张sys_user表,用role字段区分身份,角色维度上的业务权限交给接口层控制。
CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30), phone VARCHAR(20), role TINYINT NOT NULL COMMENT '0-管理员 1-后勤 2-护工 3-体检员 4-用户', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;角色字段用TINYINT而不是VARCHAR,是因为代码里做权限判断时拿整数直接比对,比如role == 0表示管理员,比字符串比较更干净,也避免把“Admin”“admin”“管理员”各种写法混在一起。密码字段建议存MD5值,MD5('123456')这样的密文用在演示系统里够用,但如果你打算作为正式毕业设计提交,最好换成BCrypt,因为Mysql数据库一旦泄露,明文密码就是安全事故。
初始化数据时直接把五类账号造好,答辩现场不用现注册:
INSERT INTO sys_user (username, password, real_name, role) VALUES ('admin', MD5('123456'), '系统管理员', 0), ('logistics', MD5('123456'), '后勤张姐', 1), ('nurse01', MD5('123456'), '护工小李', 2), ('doctor01', MD5('123456'), '体检员王医生', 3), ('elder01', MD5('123456'), '用户陈奶奶', 4);这里把“用户”也放进sys_user,是很多智慧养老项目容易漏掉的一层。老人自己或者家属登录后提交物资申请、发布留言,都需要一个身份标识,user_id要能被后续业务表引用。如果单独建一张老人表再和用户表关联,会多一次关联查询,而这个项目里用户和老人的对应关系其实是一对一,直接在业务表里冗余user_id字段即可。
2.2 房间表与老人表的主数据字段设计
房间信息管理是整个平台的基础数据来源,护工查看入住老人、体检员查看老人健康状态,最终都会落到房间和老人两张主表上。房间表的核心字段不是面积和装修,而是床位数量和房间状态。
CREATE TABLE room_info ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL UNIQUE, floor_no INT, room_type VARCHAR(20) COMMENT '单人间/双人间/护理间', bed_count INT DEFAULT 2, room_status TINYINT DEFAULT 0 COMMENT '0-空闲 1-部分入住 2-已满 3-维修', remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE elder_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT COMMENT '关联sys_user中的用户角色', name VARCHAR(30) NOT NULL, age INT, gender TINYINT, health_status VARCHAR(200) COMMENT '当前身体状态描述', chronic_disease VARCHAR(200) COMMENT '是否有慢性疾病', emergency_contact VARCHAR(50), emergency_phone VARCHAR(20), id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );注意room_status和bed_count是两条信息:bed_count是物理床位数量,room_status是当前实际状态。管理员新增房间时,room_status默认0;当有老人入住时,由程序判断已入住人数和bed_count的关系,自动更新room_status为1或2。不能靠管理员手工改状态,否则就会出现床位已满但room_status还显示空闲的错误。
老人表里的chronic_disease字段建议存文本描述而不是布尔值,比如“高血压II级”“糖尿病,需胰岛素”,体检员查看老人信息时直接读文本比看0和1更直观。health_status则是动态信息,护工和体检员都可以在各自权限内查看,管理员编辑老人信息时修改它。
2.3 入住记录表:用is_active保留历史床位数据
房间入住管理是整个平台里最容易做错的一张表。最常见的错误写法是直接在room_info表里加一个elder_id,表示当前谁住在里面。这样做的后果是:老人退住后,历史入住记录彻底丢失,管理员无法统计每个房间住过多少人,也无法回溯某段时间的入住情况。
正确做法是单独建一张room_occupancy关联表,每次入住生成一条记录,退住时不物理删除,只把is_active置为0:
CREATE TABLE room_occupancy ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, elder_id INT NOT NULL, check_in_time DATETIME, check_out_time DATETIME, is_active TINYINT DEFAULT 1 COMMENT '1-在住 0-已退住', remark VARCHAR(200) );查询当前在住老人时,过滤条件固定写is_active = 1:
SELECT e.name, e.age, e.health_status, r.room_no, r.room_type FROM room_occupancy o JOIN elder_info e ON o.elder_id = e.id JOIN room_info r ON o.room_id = r.id WHERE o.is_active = 1 AND r.room_status != 3 ORDER BY r.room_no;这条SQL是护工端“房间入住查看”和后端数据处理的核心。is_active字段让退住操作变成一次UPDATE而不是DELETE,查询历史记录时只需把条件改成is_active = 0。另外,外键在这个项目里不需要建物理约束,因为管理员删除房间时可能提示外键冲突导致删除失败,逻辑关联配合代码校验已经足够,所谓外键留给数据库不如留给Service层。
3. 基于拦截器与@RequireRole注解的多角色接口权限控制
3.1 为什么毕业设计不直接引入Spring Security
Spring Boot入门阶段接触到的Spring Security配置复杂,过滤链、UserDetailsService、密码编码器一套下来,对课程设计而言太重。如果你在springboot面试题里被问到过Spring Security的过滤器链,就知道它内部处理顺序稍微配置错,接口就全部403。这个平台的五类角色权限边界非常清晰,用拦截器加自定义注解就能解决,代码量不到三十行,可读性也好,答辩老师问起来你能把每条路径的权限规则讲清楚。
3.2 自定义@RequireRole注解与拦截器实现
先定义一个注解,标注在Controller的方法上,声明该方法允许哪些角色访问:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int[] value(); }然后在拦截器里读取注解,比对当前登录人的角色:
@Component public class RoleInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod method = (HandlerMethod) handler; RequireRole anno = method.getMethodAnnotation(RequireRole.class); if (anno == null) { anno = method.getBeanType().getAnnotation(RequireRole.class); } if (anno == null) { return true; } HttpSession session = request.getSession(); Integer role = (Integer) session.getAttribute("role"); if (role == null) { response.sendRedirect("/login.html"); return false; } for (int r : anno.value()) { if (r == role) { return true; } } response.setStatus(403); return false; } }这段代码的核心逻辑是:先判断请求是否来自Controller方法,如果不是直接放行,避免静态资源被拦截;然后依次找方法上的注解和类上的注解,都没标就默认登录即可访问。拿到Session里的role后,遍历注解的value数组,命中任何一个角色就放行,都不匹配返回403。
注册这个拦截器时注意路径规划:
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private RoleInterceptor roleInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(roleInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); } }这里/api/**匹配所有后端接口,登录接口单独放行。登录成功后把userId和role放进Session,后续所有接口都能通过Session拿到身份信息。拦截器和过滤器要区分一下:过滤器是Servlet层面的,拿不到HandlerMethod,也就没法判断方法上的注解;拦截器在SpringMVC内部,能拿到方法对象,所以注解权限判断用拦截器更自然。
3.3 各角色核心接口路径与返回格式规划
接口路径直接按角色前缀区分,一眼能看出归属,也方便拦截器按前缀理解权限范围:
| 角色 | 典型接口 | 方法 | 说明 |
|---|---|---|---|
| 管理员 | /api/admin/room/list | GET | 分页查询房间信息 |
| 管理员 | /api/admin/room | POST | 新增房间 |
| 管理员 | /api/admin/elder | POST | 新增老人信息 |
| 后勤人员 | /api/logistics/material/list | GET | 物资申请列表 |
| 后勤人员 | /api/logistics/feedback/list | GET | 反馈信息列表 |
| 护工 | /api/nurse/occupancy/list | GET | 查询入住老人 |
| 护工 | /api/nurse/message/list | GET | 留言查看 |
| 体检员 | /api/doctor/elder/list | GET | 老人健康档案查询 |
| 体检员 | /api/doctor/notice/list | GET | 公告查看 |
| 用户 | /api/user/material | POST | 提交物资申请 |
| 用户 | /api/user/message | POST | 发布留言 |
接口Controller示例,管理员新增房间:
@RestController @RequestMapping("/api/admin/room") public class AdminRoomController { @PostMapping @RequireRole({0}) public Result addRoom(@RequestBody RoomInfo room) { roomService.insert(room); return Result.success(); } }@RequireRole({0})表示只有管理员能调用,护工和体检员即便知道接口地址也无法新增房间。角色枚举和注解配合,权限的规则散落在各个Controller方法上,比集中式配置直观。前后端分离部署时,前端Vue项目打包后的静态文件放在src/main/resources/static目录下,后端接口统一走/api前缀,不存在跨域问题,也就不需要额外配置CorsFilter。
4. 物资申请与留言管理两条业务流程的状态设计与事务处理
4.1 物资申请:从提交到处理的乐观状态流转
用户在“物资申请管理”界面新增一条申请,后勤人员在“物资申请查看”界面看到后来处理。这个流程看似简单,但它涉及状态变更,容易出两类问题:一是用户重复提交,二是后勤人员并发审批同一条申请导致状态错乱。
先看用户提交申请的Controller:
@PostMapping("/api/user/material") @RequireRole({4}) public Result submitApply(@RequestBody MaterialApply apply, HttpSession session) { apply.setUserId((Integer) session.getAttribute("userId")); apply.setStatus(0); // 0-待处理 1-已批准 2-已驳回 materialService.insert(apply); return Result.success(); }状态字段塞在申请单里,0表示待处理。后勤人员处理时不能直接无条件更新状态,而是要用CAS思想,把“当前状态是0”作为更新条件:
@PostMapping("/api/logistics/material/handle") @RequireRole({1}) public Result handle(@RequestParam Integer id, @RequestParam Integer result) { int rows = materialService.handleWithStatus(id, 0, result == 1 ? 1 : 2, loginUserId); if (rows == 0) { return Result.error("该申请已被处理,请勿重复操作"); } return Result.success(); }对应的SQL是关键,Mysql的UPDATE语句自带行锁,把期望状态放进WHERE条件里:
UPDATE material_apply SET status = #{targetStatus}, handle_time = NOW(), handler_id = #{handlerId} WHERE id = #{id} AND status = #{expectStatus}如果两个后勤人员同时点击处理同一条申请,Mysql的行锁会让第二个UPDATE等待,等第一个提交后,第二个的WHERE条件status = 0已经不成立,影响行数为0,代码里rows == 0的分支就会提示“已被处理”。这是典型的乐观锁写法,比select后再update安全得多,毕业设计里如果你在springboot配置了多数据源或者用了MyBatis-Plus,同样能套用这个模式。
4.2 留言与回复:单表单回复字段还是父子表
用户留言、护工查看留言、管理员回复留言,这个模块的数据结构有两种设计思路。一种是建parent_id自关联父子表,支持多级回复;另一种是单表加reply_content字段,一条记录存留言和回复。这个平台的需求是“用户新增留言,并查看管理员回复”,单表单回复字段就够了,多级回复用不上反而增加查询复杂度。
CREATE TABLE message_feedback ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '留言人', content VARCHAR(500) NOT NULL, reply_content VARCHAR(500) COMMENT '管理员回复内容', reply_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;查询留言列表时,需要把回复状态计算出来,用IF函数在前端直接展示:
SELECT id, user_id, content, reply_content, create_time, IF(reply_content IS NOT NULL AND reply_content != '', '已回复', '待回复') AS reply_status FROM message_feedback ORDER BY create_time DESC LIMIT 0, 10;这里IF的判断条件要同时检查NULL和空字符串,因为管理员可能只点了保存没填内容。LIMIT 0, 10做分页,前端每次滚动加载传page参数,对应SQL里的LIMIT #{offset}, #{size}。
事务处理上,用户提交留言和查询列表是读多写少的场景,不需要加锁。但管理员回复留言时,回复内容和回复时间要同步更新,用@Transactional保证一起成功或一起回滚:
@Transactional(rollbackFor = Exception.class) public void replyMessage(Integer id, String replyContent) { messageFeedbackMapper.updateReply(id, replyContent); }注意@Transactional只能通过代理对象调用时生效,如果在同一个类里调用带事务的方法,事务会失效,这是springboot面试题里常挖的坑,实际开发时把事务方法放到独立Service类里。
4.3 只读角色的查询优化与索引使用
护工查看入住老人列表、体检员查看老人信息,这两个功能本质上是多表关联查询。数据量不大时看不出差别,但入住记录积累一年后,room_occupancy表可能有上千条数据,关联查询开始变慢。常用的优化策略是给外键字段和状态字段建联合索引:
ALTER TABLE room_occupancy ADD INDEX idx_room_active (room_id, is_active); ALTER TABLE message_feedback ADD INDEX idx_user_time (user_id, create_time);idx_room_active覆盖了“查某个房间当前谁在住”的场景,idx_user_time覆盖了“用户查自己的留言列表”的场景,查询时避免回表。体检员查看老人信息时只查elder_info主表,按age、chronic_disease等字段做条件过滤,如果有“按疾病类型筛选老人”的需求,再对chronic_disease加普通索引,但文本字段的索引长度要控制,字符串前缀索引(10)就够。
5. 答辩演示前的一键构建、数据验证与常见启动排错
5.1 三个bat脚本的分工与参数
项目里带了1-install.bat、3-build.bat、2-run.bat三个脚本,很多第一次拿到源码的人容易按文件名顺序理解成执行顺序,实际上是install、build、run三个阶段,我的建议是答辩前一晚按这个顺序跑一遍:
@echo off rem 1-install.bat:清理并安装依赖到本地Maven仓库 mvn clean install -DskipTests -q pause @echo off rem 3-build.bat:打包成可执行jar mvn package -DskipTests -q pause @echo off rem 2-run.bat:以8080端口启动服务 java -jar target/smart-elder-care-1.0.0.jar --server.port=8080 pause-DskipTests跳过测试减少打包时间,-q安静模式只输出错误不刷进度条。java -jar后面的--server.port=8080是Spring Boot的命令行参数,优先级高于application.yml里的server.port配置,现场如果8080被占用,改成--server.port=8081即可,不用改文件重新打包。
前端文件已经编译好放在static目录下,包含index.html和chunk-vendors等静态资源,Spring Boot内嵌Tomcat会直接把static目录映射为根路径,启动后访问http://localhost:8080就能看到登录页。如果你的机器上没装Maven,直接执行2-run.bat需要jar包已经存在,否则会报找不到target目录。
5.2 演示现场的数据验证技巧
答辩现场演示时要避免“新增一条就刷新一下页面”的尴尬,准备好两条SQL提前核对数据:
SELECT r.room_no, r.bed_count, COUNT(o.id) AS live_count FROM room_info r LEFT JOIN room_occupancy o ON o.room_id = r.id AND o.is_active = 1 GROUP BY r.id HAVING live_count < r.bed_count;这条SQL查出所有还有空位的房间,演示前先跑一遍,确保登录管理员账号后“房间信息管理”列表里有空闲房间。用户端和后勤端的联动演示,可以提前用用户账号提交一条物资申请,状态设为0,演示时后勤账号登录后直接就能看到待处理记录,不用现场现填。
Mysql连接配置检查重点看字符集和时区,很多启动报错都出在这里:
spring.datasource.url=jdbc:mysql://localhost:3306/smart_elder_care?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456characterEncoding=utf8解决中文乱码,serverTimezone=Asia/Shanghai解决Mysql 8.x的时区报错。如果你用的数据库没有smart_elder_care这个库,先执行CREATE DATABASE smart_elder_care DEFAULT CHARSET utf8mb4,再导入sql文件。
5.3 最常见的三个启动失败原因
端口被占用是最常见的,启动日志里看到Port 8080 was already in use,在Windows下用netstat -ano | findstr 8080找到占用进程PID,taskkill /pid 进程号 /f强制结束,或者直接改启动端口。第二类是Mysql驱动版本不匹配,springboot 2.x默认配mysql-connector-java 8.x,如果本地是Mysql 5.7,驱动也能兼容,但URL里的driver-class-name要确认是com.mysql.cj.jdbc.Driver而不是旧的com.mysql.jdbc.Driver。第三类是页面白屏但接口正常,打开浏览器F12看Console,多半是static目录下前端资源引用了绝对路径,/app.xxx.css这种开头少了一层context-path,检查application.yml里有没有设置server.servlet.context-path,如果设置了,前端静态资源路径也要同步调整。
本文还有配套的精品资源,点击获取