ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot+Vue的养老院管理系统毕设设计与实现

基于Spring Boot+Vue的养老院管理系统毕设设计与实现 简介基于JavaSpringBootVueMySQL的养老院管理系统完整源码包主要面向计算机相关专业学生的毕业设计、课程设计与期末大作业解决养老院日常运营中的数字化管理问题系统功能完善、操作便捷。系统覆盖老人信息管理、员工管理、财务统计、日常活动安排、健康监测等核心模块前后端分离架构界面简洁、操作直观具备较高的实际应用价值和良好的可扩展性。资源压缩包包含591个文件主要有123个Java后端源码、91个Vue前端页面、SQL数据库脚本、部署运行脚本以及配置和设计文档包体大小约为20.4MB部署环境为JDK、Maven、MySQL5.7以上搭配IDEA与Navicat可快速运行整体结构清晰便于学习。目前已有49人学习下载。项目经严格调试确保可运行目录清晰、代码完整适合学习SpringBoot与Vue整合开发也可在此基础上根据养老院具体需求进行功能扩展。1. 这个毕设项目能拿高分靠的不是堆功能而是把权限和业务流程讲透接手这类「养老院管理系统」毕设项目的同学大多会陷入同一个误区先把老人管理、床位管理、缴费管理这些 CRUD 页面全做出来觉得页面多就等于功能全。实际上评阅老师打开系统第一个看的是登录和权限第二个看的是核心业务闭环是否跑得通——养老院的核心不是「登记老人信息」而是「护理任务怎么派发、执行、留痕、统计」。基于 java Spring Boot Vue MySQL 这套技术栈做养老院管理系统正确做法是先建一套能自圆其说的角色权限模型管理员、护理员、家属再把「入住登记 → 护理计划 → 执行打卡 → 异常上报」这条链走通最后才补缴费和统计报表。本文从前端 Vue 到后端 Spring Boot 再到 MySQL 表结构把一条能演示、能答辩、能改造成自己项目的高分路径完整落地。2. 养老院业务建模与 MySQL 表设计先把三张核心表画出闭环2.1 养老院管理系统的角色与权限边界养老院管理系统和普通管理系统最大的区别在于它的使用者横跨三个完全不同的角色管理员关注全局床位利用率、收费情况、护工人数配比护理员关注每日任务今天要给哪些老人翻身、喂药、测血压家属只关心自己老人的动态近一周的照护记录、健康指标。这决定了权限模型必须做到「数据级隔离」——家属登录后不应该能看到其他老人的信息甚至不应该看到床位列表。我的做法是引入三张基础表sys_user用户表、sys_role(角色表)、sys_user_role(用户角色关联表)不做菜单按钮级的那种复杂 RBAC 细粒度权限因为毕设答辩时你很难在三分钟内把「用户-角色-菜单-按钮」四层关系讲清楚。用用户 角色 数据权限字段比如elder_id反而更实用。后端接口通过拦截器校验角色再通过 SQL 拼接WHERE elder_id ?做行级隔离。2.2 三张核心业务表elder_info、care_plan、care_record业务表不需要设计太多八到十张足够拿高分但三张表必须精心设计elder_info老人档案、care_plan护理计划、care_record护理执行记录。这三张表构成了养老院系统的业务闭环老人入住后护理员或管理员为其制定护理计划护理员每日按计划执行并打卡打卡记录沉淀为数据最终呈现在统计报表和家属查看的页面里。CREATE TABLE elder_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 老人ID, name varchar(32) NOT NULL COMMENT 姓名, gender tinyint(1) DEFAULT 1 COMMENT 性别 1男 2女, age int(11) DEFAULT NULL COMMENT 年龄, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, room_no varchar(16) DEFAULT NULL COMMENT 房间号, bed_no varchar(16) DEFAULT NULL COMMENT 床位号, health_level varchar(8) DEFAULT A COMMENT 护理等级 A/B/C, emergency_contact varchar(32) DEFAULT NULL COMMENT 紧急联系人, emergency_phone varchar(20) DEFAULT NULL COMMENT 紧急联系电话, status tinyint(1) DEFAULT 1 COMMENT 状态 1在院 0退住, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1001 DEFAULT CHARSETutf8mb4 COMMENT老人信息表; CREATE TABLE care_plan ( id bigint(20) NOT NULL AUTO_INCREMENT, elder_id bigint(20) NOT NULL COMMENT 老人ID, plan_name varchar(64) NOT NULL COMMENT 计划名称, care_type varchar(16) NOT NULL COMMENT 护理类型血压/翻身/喂药/清洁, frequency varchar(32) DEFAULT NULL COMMENT 频率每天3次/每2小时, start_date date DEFAULT NULL, end_date date DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1启用 0停用, create_by bigint(20) DEFAULT NULL COMMENT 创建人, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder (elder_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT护理计划表; CREATE TABLE care_record ( id bigint(20) NOT NULL AUTO_INCREMENT, plan_id bigint(20) NOT NULL COMMENT 护理计划ID, elder_id bigint(20) NOT NULL COMMENT 老人ID, nurse_id bigint(20) NOT NULL COMMENT 执行护理员ID, care_type varchar(16) NOT NULL COMMENT 护理类型, result varchar(255) DEFAULT NULL COMMENT 执行结果/备注, record_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 执行时间, PRIMARY KEY (id), KEY idx_elder_time (elder_id, record_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT护理执行记录表;逻辑说明care_record不直接关联elder_info而是通过care_plan间接关联同时在表中冗余一个elder_id字段。这是刻意设计的——查询「某位老人的所有护理记录」时直接走idx_elder_time索引避免每次都要 JOIN 到care_plan表才能拿到老人 ID。代价是插入记录时要冗余写入elder_id但查询性能收益远大于存储成本。health_level字段用 A/B/C 表示护理等级A 级代表完全不能自理护理计划频率更高这个字段在统计页会用于计算护理工作量权重。2.3 数据库初始化脚本的写法演示数据比表结构更重要表结构设计好之后评分差距往往体现在初始化脚本上。常见做法是准备两个文件schema.sql建表、data.sql灌数据。data.sql里要预置三种角色账号管理员 admin、护理员 nurse01、家属家属账号以及 20 条左右的老人档案数据、每个老人对应的护理计划和最近 7 天的护理记录。答辩时演示「展示最近一周护理趋势图」如果没有这 7 天的历史数据前端图表就是空的再好的 ECharts 配置也没用。注意data.sql里插入中文数据时MySQL 连接串必须显式指定characterEncodingutf8并在建库时设置DEFAULT CHARSETutf8mb4否则 Windows 环境下很容易出现乱码。另外一个实用技巧在application.yml里配置spring.sql.init.modealways并配合spring.sql.init.continue-on-errortrue这样重新启动项目时会自动执行建表和灌数据脚本省去手动source的步骤。3. Spring Boot 后端实现从登录鉴权到护理打卡的完整链路3.1 用 JWT 拦截器实现登录状态管理不引入 Spring Security很多毕设项目会引入 Spring Security JWT 的组合但 Spring Security 的过滤器链配置对于第一次做项目的同学来说学习曲线太陡答辩时也容易被追问「过滤器执行顺序」这类细节。我更推荐的做法是用 JWT 做无状态登录自己写一个HandlerInterceptor做统一拦截。核心依赖只需要jjwt一个库代码量控制在 100 行以内逻辑一目了然。// JwtUtil.java Component public class JwtUtil { // 注意正式项目密钥要放到配置文件中这里仅为演示 private static final String SECRET nursing-home-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000L; // 24小时 public String createToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }参数说明claim(role, role)把角色放进 token 里后续接口判断权限时不需要查数据库EXPIRE_TIME设置为 24 小时足以覆盖一次完整的毕设演示周期。HS256 是对称签名算法密钥在前后端分离架构下保存在后端即可。需要补充的是jjwt库版本 0.9.x 与 0.11.x 的 API 差异很大后者要求使用Keys.hmacShaKeyFor()传入字节数组密钥如果 maven 拉的是新版上面这段signWith(SignatureAlgorithm.HS256, SECRET)会编译报错。建议锁定jjwt.version0.9.1/jjwt.version或用新版写法适配。3.2 通过拦截器实现角色权限校验每个接口自己做判断还是统一拦截登录校验和权限校验交给拦截器做业务接口里就能省掉「从 session 里取用户、判断是否登录」的重复代码。// AuthInterceptor.java Component public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims jwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }要点说明request.setAttribute把解析后的用户信息放进请求上下文Controller 里通过(Long) request.getAttribute(userId)直接取不需要再写解析 token 的工具方法。OPTIONS请求放行是前后端分离项目最容易忽略的点——Vue 开发服务器向 Spring Boot 发请求时有跨域预检预检请求不会携带自定义 Header如果拦截器不放行预检前端控制台会不断报跨域错误。角色级权限校验我用RequireRole自定义注解 在拦截器里读取 HandlerMethod 的注解做判断。代码逻辑如果方法上有 RequireRole(nurse)就用 request.getAttribute(role) 比对不一致就返回 403。这样新增一个接口时只需要在方法上加注解权限规则集中管理。3.3 护理打卡接口一个典型的事务与状态流转场景RestController RequestMapping(/api/care) public class CareRecordController { Autowired private CareRecordService careRecordService; PostMapping(/record) public Result? record(RequestBody CareRecordDTO dto, HttpServletRequest request) { Long nurseId (Long) request.getAttribute(userId); careRecordService.executeCare(dto, nurseId); return Result.success(护理记录提交成功); } }Service 层核心逻辑里我先校验护士权限以及dto.getPlanId()对应的护理计划是否处于启用状态再插入一条care_record数据同时更新care_plan表里对应记录的最近执行时间。// CareRecordServiceImpl.java Transactional(rollbackFor Exception.class) public void executeCare(CareRecordDTO dto, Long nurseId) { CarePlan plan carePlanMapper.selectById(dto.getPlanId()); if (plan null || plan.getStatus() ! 1) { throw new BusinessException(护理计划不存在或已停用); } CareRecord record new CareRecord(); record.setPlanId(dto.getPlanId()); record.setElderId(plan.getElderId()); record.setNurseId(nurseId); record.setCareType(dto.getCareType()); record.setResult(dto.getResult()); careRecordMapper.insert(record); // 这里做计划进度更新 carePlanMapper.updateLastExecTime(dto.getPlanId(), new Date()); }参数说明Transactional(rollbackFor Exception.class)指定了任何异常都回滚包括自定义的BusinessException。这里没有用默认的RuntimeException回滚策略目的是让业务异常比如计划已停用也触发事务回滚避免出现「提示失败但数据已写入」的脏数据。此外CareRecordDTO包含planId、careType、result三个字段其中careType是从前端下拉菜单传来的但真正落库时以plan表里的类型为准——防止前端传错类型捣乱。3.4 Spring Boot 版本相关的三个常见坑毕设项目最常见的运行环境差异集中在 Spring Boot 2.7.x 与 3.x 之间。如果你的 pom 里用的是 3.xjavax.servlet全部要换成jakarta.servletspringdoc替代springfox做接口文档。还有一个高频问题Spring Boot 3.x 中WebMvcConfigurer的方法签名不变但包名变了拦截器注册代码里的import org.springframework.web.servlet.config.annotation.InterceptorRegistry是不受影响的受影响的是HandlerInterceptor的javax.servlet.http.HttpServletRequest引用。推荐直接用 2.7.18 版本资料最多且网上搜到的配置代码基本都能直接运行。另一个影响较大的坑是 MySQL 驱动坐标8.x 版本的驱动类名是com.mysql.cj.jdbc.Driver但 5.x 是com.mysql.jdbc.Driver。如果 pom 里引了mysql-connector-java8.0.x配置文件写错驱动类名启动时直接报ClassNotFoundException。我的习惯是直接指定runtimeOnly com.mysql:mysql-connector-j:8.0.33新版坐标groupId 不变artifactId 变了。4. Vue 前端实现路由守卫、动态菜单与 axios 拦截器4.1 前端工程结构与 Vue 环境搭建要点前端部分采用 Vue 2 Element UI 还是 Vue 3 Element Plus取决于你本地 Node 版本。Vue 3 要求 Node.js 16但很多教学机上的 Node 还是 14.x这会直接导致npm install阶段报ERESOLVE错误。如果遇到这种情况一种可复现的做法是降低依赖版本而不是升级 Node——在package.json中将element-plus锁到2.2.x版本同时用npm install --legacy-peer-deps绕开依赖冲突检查。另一个更省事的选择是 Vue 2.7 Element UI 2.15.x对 Node 版本要求宽得多几乎不需要额外配置就能跑起来。项目目录组织方面毕设级别的前端不建议搞太复杂的 monorepo 结构src/api放所有接口调用、src/router放路由配置、src/store放 Vuex或 Pinia状态、src/views按业务模块分目录即可。下面重点说三个必写文件axios封装、路由守卫、权限指令。4.2 用 axios 拦截器统一处理 token 注入与 401 跳转// src/utils/request.js import axios from axios import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器把 token 挂到请求头 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { // 与后端拦截器约定的格式Authorization: Bearer token config.headers[Authorization] Bearer ${token} } return config }, error { return Promise.reject(error) }) // 响应拦截器401 跳登录页 service.interceptors.response.use(response { // 后端统一返回 { code, message, data } 结构 const res response.data if (res.code ! 200) { // Message.error(res.message) // Element UI 的全局提示 return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) }) export default service参数说明baseURL: /api配合 Vue 开发服务器的proxy配置把请求转发到后端 8080 端口。如果直接写http://localhost:8080会产生跨域问题虽然后端可以配置CorsFilter解决但本地开发用 proxy 更干净——发出去的请求是相对路径打包部署到生产环境时不需要改代码。响应拦截器里将res.data解包返回业务代码中const res await api.getElderList()拿到的就是data部分。4.3 路由守卫控制访问权限菜单按角色动态渲染// src/router/index.js const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: Layout, redirect: /dashboard, meta: { roles: [admin, nurse, family] }, children: [ { path: dashboard, name: Dashboard, component: () import(/views/Dashboard.vue), meta: { title: 数据看板, icon: Odometer } }, { path: elder, name: ElderList, component: () import(/views/elder/ElderList.vue), meta: { title: 老人档案, roles: [admin, nurse] } }, { path: family, name: FamilyView, component: () import(/views/family/FamilyView.vue), meta: { title: 家属查看, roles: [family] } } ] } ]路由守卫的核心逻辑是读取本地存储中的用户角色与路由meta.roles比对没有权限就重定向到 404 或首页避免用户手动输入 URL 越过菜单访问接口页面。这只是前端的 UI 层限制真正的数据安全靠的是第二章设计的后端行级权限——前端隐藏菜单只是不让用户看到入口防不住有人通过控制台直接调用 API后端必须再拦一道。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } if (to.meta.roles) { const role localStorage.getItem(role) if (!to.meta.roles.includes(role)) { next(/403) return } } next() })菜单动态渲染的做法是用 Vuex 存储menus数组登录后根据角色过滤routes中的children遍历生成菜单项。这里的核心是「根据角色过滤」逻辑写页面时不要为了省事把菜单写死否则答辩时老师让切换角色登录前端菜单不变就会被认为是写死的假权限。4.4 Vue 页面组件与后端的对接写法以「护理记录提交页面」为例展示页面的基础结构表单、提交按钮、调用后端接口。这里有一个在答辩演示中很加分的细节提交成功后不跳转页面而是用 Element UI 的$message.success提示并且把列表刷新和表单重置放在同一个 Promise 链里执行。template div classcare-record-page el-form :modelform refformRef label-width100px el-form-item label护理计划 propplanId el-select v-modelform.planId placeholder请选择护理计划 el-option v-forplan in planList :keyplan.id :labelplan.planName :valueplan.id / /el-select /el-form-item el-form-item label护理类型 propcareType el-input v-modelform.careType placeholder如测血压 / /el-form-item el-form-item label执行结果 propresult el-input typetextarea v-modelform.result placeholder填写检测结果或备注 / /el-form-item el-form-item el-button typeprimary clicksubmitForm提交记录/el-button /el-form-item /el-form /div /template script import { getPlanList, submitCareRecord } from /api/care export default { data() { return { form: { planId: , careType: , result: }, planList: [] } }, created() { this.loadPlans() }, methods: { async loadPlans() { const res await getPlanList() this.planList res }, async submitForm() { await submitCareRecord(this.form) this.$message.success(护理记录已提交) this.form { planId: , careType: , result: } } } } /script附带参数与代码逻辑的展开说明这里调用的是第二章里设计的care/record接口接口本身会校验userId和角色。前端表单的作用仅仅是收集数据并展示结果页面本身不包含任何权限校验逻辑——权限校验应当在路由守卫与后端接口处完成不要在页面里用if (role admin)这种代码控制可见性因为这种控制方式在v-if被绕过时比如直接改内存中的 role 值会导致无权限用户接触数据。5. 答辩与运行验证一份 60 秒快速演示的 MySQL 数据回放脚本毕设项目和工业项目的区别在于演示效果直接影响评分。我建议准备一个专门用于答辩的 MySQL 数据生成脚本把平时积累的散数据替换成一套完整的演示数据。核心技巧是构造「过去 7 天」的护理记录让前端 ECharts 趋势图有数据可画。-- demo_data.sql -- 生成最近7天每个老人每天3条护理记录 SET start_date DATE_SUB(CURDATE(), INTERVAL 7 DAY); INSERT INTO care_record (plan_id, elder_id, nurse_id, care_type, result, record_time) SELECT cp.id, cp.elder_id, e.nurse_id, cp.care_type, CASE cp.care_type WHEN 测血压 THEN CONCAT(120 FLOOR(RAND() * 20), /, 80 FLOOR(RAND() * 10), mmHg) WHEN 测血糖 THEN CONCAT(5.0 RAND() * 2, mmol/L) ELSE 正常 END AS result, -- 生成从7天前到现在的随机时间 DATE_ADD(start_date, INTERVAL FLOOR(RAND() * 7 * 24 * 60) MINUTE) FROM care_plan cp JOIN elder_info e ON cp.elder_id e.id WHERE cp.status 1;这段脚本用RAND()生成随机时间戳和随机指标值保证图表数据有波动——如果所有数据点斜率完全一致一眼就能看出是伪造的。使用前先执行一次TRUNCATE care_record清空手点产生的零散数据。执行后在系统首页的「近7日护理趋势」图表上就能看到三条线血压、血糖、翻身的自然起伏。答辩时容易问到的追问整理成一张速查表追问方向建议应答说出这句话对应的代码证据为什么用 JWT 不用 Session无状态、横向扩展友好适合前后端分离JwtUtil.createToken不含任何 Session 存储多角色权限怎么控制的登录发放角色 claim后端拦截器校验注解AuthInterceptor中读取RequireRole若缓存被删除如何保证一致性数据一致性靠事务保证缓存只是加速executeCare上的Transactional注解养老院的护理单一与普通 CRUD 有何不同护理计划与执行记录构成业务闭环涉及状态机care_plan.status与care_record的联动更新若老人退住如何处理历史记录不做物理删除仅更新状态字段与恢复默认密码elder_info.status字段与sys_user的因果逻辑这份脚本和速查表是用 30 分钟换答辩现场 10 分钟的从容。演示前两周运行时若有时间拿几个账户分别跑一遍「登录 → 各模块转一圈 → 提交一条护理记录」手机录屏回放三层页面的逻辑比背代码更记得住。最后系统里留一个「重置密码」的按钮万一现场演示时录入的测试账号密码被记错也不需要关服务器改库。本文还有配套的精品资源点击获取
返回列表