拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue养老院管理系统实战解析:架构、权限与前后端分离

简介基于Spring Boot与Vue前后端分离架构的养老院管理系统源码包面向计算机、数学、电子信息等专业的课程设计、期末大作业与毕业设计场景适合有一定Java和前端基础、乐于钻研调试的开发者参考。压缩包共226个文件涵盖91个Java后端类、32个Vue前端组件、SQL数据库脚本、XML配置、项目说明文档等整体约17.17MB目录清晰可直接导入IDE运行。系统功能覆盖人员管理、药品食品管理、报修与故障管理、外出与访客管理、留言与新闻发布、服务大厅、每日健康打卡、个人中心、消息中心等模块并配有养老院介绍、环境展示等首页内容业务链条完整。目前已有211人学习下载。借助源码、数据库和项目说明读者可系统学习Spring Boot MyBatis-Plus Redis MySQL的后端整合以及Vue3 Element Plus的前端界面开发快速搭建运行环境并进行二次扩展。1. 养老院管理系统为什么用 SpringBoot Vue 前后端分离做养老院管理系统这类业务系统最怕的不是功能多而是权限乱、状态散、改一处崩三处。传统 JSP 单体项目里页面、业务、数据访问全挤在一个工程随着用户管理、药品食品管理、报修、外出、访客、留言、新闻、服务大厅这些模块堆上来每次发版都要小心翼翼。换成 SpringBoot Vue 前后端分离之后后端只出 RESTful 接口前端用 Vue 管理页面状态两边可以独立开发、独立部署接口联调时也能通过 Mock 数据并行推进实际交付周期能缩短不少。这份源码对应的系统覆盖了养老院的日常运营场景入住老人的健康打卡、药品发放、报修申请、外出登记、访客管理还有员工的请假调休、系统监控等。技术栈上后端是 SpringBoot MyBatis-Plus Redis MySQL前端是 Vue3 Element Plus典型的当前中小型管理系统主流组合。适合做课程设计、毕业设计也适合刚接触前后端分离项目的开发者拿来研究接口设计思路。下面直接拆解这套项目里最值得复用的部分模块划分、数据库设计、后端接口、前端联调以及我实际跑通后踩过的坑。2. 模块拆分与数据库表设计先把业务边界划清楚2.1 从功能清单反推模块边界拿到压缩包后先别急着启动。看项目说明和源码里的包结构能发现这个系统的核心思路是按照「主数据 流程单 统计展示」来切分的。主数据是人员老人、员工、家属、药品、食品、房间流程单是报修单、外出单、访客单、请假调休单、健康打卡记录统计展示则是首页的养老院介绍、服务内容、环境展示以及个人健康信息图表。这种划分的好处是权限可以比较自然地挂到角色上。管理员管系统设置、新闻发布、分院信息护工主要操作健康打卡、药品发放、报修申请前台处理访客和留言老人或家属只看个人信息和公告。前后端分离后前端路由通过后端接口返回的菜单权限动态生成后端接口通过拦截器校验 token职责很干净。2.2 核心表结构人员、药品、报修、外出数据库脚本在资源里的sql目录下导入 MySQL 后可以看到完整建表语句。我挑几张核心表说明设计思路。user表负责登录账号和基本信息关键字段是role_id、status、real_name、phone它和elder_info老人档案是分开的。这样设计的好处是一个账号可以关联多个老人比如家属替老人查看信息老人档案里只存身份证号、床位号、护理等级、紧急联系人不掺入登录态。elder_info的护理等级字段建议用字典表而非硬编码。源码里用了一个dict_data表来存「自理、半自理、全护理」这类枚举值这样前端下拉框渲染、后端校验、统计报表都可以复用同一份字典不需要改代码。药品管理表medicine_info和发放记录medicine_record是一对多关系。发放记录里不直接存药品名称而是存medicine_id同时冗余elder_id、dosage、send_time。冗余老人 ID 是为了查「某个老人这周吃了什么药」时不需要关联两张表属于典型的查询效率换存储空间的设计。报修管理涉及repair_order和repair_progress。报修单里有elder_room、repair_type、description、status进度表记录每次状态变更。这里有个容易踩的坑前端展示进度时间线时需要按create_time升序取数据如果进度表没有加idx_create_time索引数据量上来后查询会明显变慢。外出管理表outing_record和访客表visitor_record字段结构类似都包含person_name、target_elder_id、start_time、end_time、reason、status。区别在于外出单需要审批所以多了approver_id、approve_remark。审批状态一般设计为pending / approved / rejected / cancelled四个值前端按钮根据状态渲染。2.3 Redis 在系统里到底缓存了什么很多人拿到带 Redis 的项目会困惑明明 MySQL 就够了为什么要加 Redis这套系统里 Redis 的用途很明确。第一是登录 token。登录成功后生成 UUID 作为 tokenkey 为login:token:{userId}value 是用户信息 JSON设置过期时间。前端每次请求在 header 里带 token后端拦截器从 Redis 查而不是每次查数据库既能做单点登录又能主动踢人删除 key 即可。第二是验证码。用 Redis 存图形验证码或短信验证码key 为captcha:{uuid}5 分钟过期。用 Redis 存验证码的好处是可以设置过期时间和失败次数限制比内存 Map 更可控。第三是热点数据。比如首页的养老院介绍、院区新闻、公告这些内容更新不频繁可以缓存为news:latest、notice:latest。源码里一般会在 Service 层判断先查缓存命中直接返回未命中查库后回填。我在看这套源码时发现部分列表接口没有加缓存比如员工月报修统计。如果后续要优化可以先把这些统计接口加入 Rediskey 设计为statistics:repair:{month}并设定每天凌晨失效。这个思路比给 MySQL 加索引见效更快。3. 后端实现SpringBoot MyBatis-Plus Redis 的接口实操3.1 项目结构与启动配置解压源码后后端工程是标准的 Maven 多模块或单模块结构。我拿到的是单模块包名大致为com.example.nursing分成controller、service、mapper、entity、config、common等。启动入口是NursingApplication.java直接右键运行即可。关键配置文件在resources/application.yml。注意以下几点MySQL 连接串要改成本地数据库地址和密码Redis 默认是localhost:6379如果没有设置密码就保持为空MyBatis-Plus 的日志配置mybatis-plus.configuration.log-impl建议打开方便在控制台看 SQL。示例配置如下server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/nursing_home?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 timeout: 5000ms mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autocontext-path设成/api后所有接口路径都会加上/api前缀。前后端分离时前端开发环境通过 Vite 代理把/api转发到http://localhost:8080生产环境则通过 Nginx 同样转发。这个前缀统一了接口入口也方便后期做网关层转发。3.2 使用 MyBatis-Plus 实现分页与条件查询MyBatis-Plus 最大的价值是不用写重复的 CRUD。在 mapper 接口里继承BaseMapperT就能直接使用selectPage、selectList、selectById等方法。比如用户管理模块的列表接口RestController RequestMapping(/user) public class UserController { Autowired private UserService userService; GetMapping(/page) public ResultIPageUserVO page(RequestParam Integer pageNum, RequestParam Integer pageSize, RequestParam(required false) String keyword) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), User::getRealName, keyword) .orderByDesc(User::getCreateTime); PageUser page new Page(pageNum, pageSize); IPageUser userPage userService.page(page, wrapper); // 此处可做 BeanUtils 拷贝为 VO隐藏密码等敏感字段 return Result.success(copyToVO(userPage)); } }逻辑说明LambdaQueryWrapper用方法引用的方式指定列名避免了字符串硬编码导致的重构风险。like方法第一个参数是 boolean 条件keyword为空时不会拼接该条件。Page对象传入当前页码和每页条数MyBatis-Plus 自动生成LIMIT ?分页语句。这里有个小坑如果使用默认分页插件需要配置MybatisPlusInterceptor否则page查询不会触发分页。参数说明pageNum从 1 开始pageSize建议前端限制在 100 以内。Result是统一返回体包含code、message、data三个字段前端根据code判断请求是否成功。返回的IPage对象里带有total、records、current、size字段前端分页组件直接绑定即可。3.3 基于 Redis 的登录鉴权与拦截器登录接口在AuthController中核心逻辑是校验用户名密码、生成 token、写入 Redis。密码在数据库里存的是 MD5 或 BCrypt 加密后的值源码里用的是 MD5 加盐在实际生产环境建议换成 BCrypt。登录成功后的代码大致如下Service public class AuthServiceImpl implements AuthService { Autowired private StringRedisTemplate redisTemplate; Override public LoginVO login(String username, String password) { User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getUsername, username)); if (user null) { throw new BusinessException(用户不存在); } if (!SecureUtil.md5(password).equals(user.getPassword())) { throw new BusinessException(密码错误); } if (user.getStatus() ! 1) { throw new BusinessException(账号已被禁用); } String token UUID.randomUUID().toString().replace(-, ); String key login:token: user.getId(); redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 2, TimeUnit.HOURS); // 同时记录 token 与 userId 的映射便于前端带 token 时快速找到用户 redisTemplate.opsForValue().set(login:token:map: token, String.valueOf(user.getId()), 2, TimeUnit.HOURS); return new LoginVO(token, user); } }逻辑说明登录成功后生成 UUID 作为 token以两个 key 写入 Redis。第一个 key 是login:token:{userId}用途是保证同一个用户只保留一个有效登录态重复登录时覆盖旧 token实现「新登录踢掉旧登录」第二个 key 是login:token:map:{token}用途是拦截器拿到 token 后直接映射到 userId再查 Redis 里的用户信息。这两个 key 都要设置相同的过期时间避免一个过期另一个还留在 Redis 里。拦截器在WebConfig中注册继承HandlerInterceptorAdapter或实现HandlerInterceptor。preHandle方法里从请求头获取 token调 Redis 校验。放行路径通常包括/auth/login、/captcha、/sys/index等。如果校验失败返回 401 状态码和统一的 JSON 错误信息。3.4 复杂业务每日健康打卡和请假调休健康打卡是养老院系统的特色功能涉及两个维度一个是老人每日的体温、血压、心率数据另一个是打卡状态正常、异常、未打卡。后端表结构是health_check表字段包括elder_id、check_date、temperature、blood_pressure、heart_rate、status、create_time。为避免同一天重复打卡需要加唯一索引uk_elder_date(elder_id, check_date)或者先查再插。源码里用的是「先查再插」的写法public void checkIn(HealthCheckDTO dto) { LocalDate today LocalDate.now(); Long count healthCheckMapper.selectCount( new LambdaQueryWrapperHealthCheck() .eq(HealthCheck::getElderId, dto.getElderId()) .eq(HealthCheck::getCheckDate, today)); if (count 0) { throw new BusinessException(今日已打卡请勿重复提交); } // 插入时校验体温和心率是否在正常范围超出则标记 status 为 2 HealthCheck entity new HealthCheck(); BeanUtils.copyProperties(dto, entity); entity.setCheckDate(today); if (dto.getTemperature() 37.3 || dto.getHeartRate() 120) { entity.setStatus(2); } else { entity.setStatus(1); } healthCheckMapper.insert(entity); }这段逻辑说明先查后插是为了避免并发重复打卡。如果想更严谨可以让前端在打卡按钮点击后立即禁用同时后端用唯一索引做兜底。状态字段status用 1 表示正常、2 表示异常前端展示时用 Tag 组件渲染不同颜色。请假调休模块和报修模块的审批流程类似都是「提交申请 → 审批人审核 → 更新状态」。源码里没有引入 Flowable 工作流引擎而是用最原始的status字段驱动因为流程节点只有两级引入工作流反而增加复杂度。这个选型思路值得借鉴业务复杂度低时用状态机枚举足以支撑不需要重型框架。4. 前端实现Vue3 Element Plus 页面渲染与接口联调4.1 前端工程结构与环境配置前端工程在frontend或vue目录下使用 Vite 构建。安装依赖前先确认 Node 版本在 16 以上然后执行npm install npm run devpackage.json里的依赖主要包括vue3、vue-router4、pinia、element-plus、axios、echarts。其中pinia是 Vue3 官方推荐的全局状态管理库替代 Vue2 时代的 Vuex。项目里的用户信息和菜单权限都放在piniastore 中页面刷新后 store 清空再从后端获取。Vite 的代理配置在vite.config.jsimport { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })代理配置说明开发环境下前端请求/api/user/page会被转发到http://localhost:8080/api/user/page。changeOrigin: true会把请求头中的 Host 字段改成目标地址的 Host避免后端收到来源不匹配的请求。生产环境不需要这个配置由 Nginx 做转发。4.2 axios 封装与 token 处理前后端分离项目里token 处理是第一个坑。axios 实例需要统一注入 token并统一处理 401 跳转登录。常见做法是在request.js里做拦截器import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default service这段代码的逻辑说明请求拦截器从localStorage取 token赋值给请求头。后端拦截器读取Authorization字段解析出 userId。响应拦截器先判断 HTTP 状态码再判断业务状态码code避免把业务错误当成成功处理。401 时清空本地 token 并跳转登录页。参数说明timeout设置 10 秒超时如果后端接口执行超过 10 秒比如报表查询前端会直接报错。此时可以单独给大接口设置更长的超时时间service.get(/statistics, { timeout: 30000 })。baseURL为/api配合 vite 代理这样前端代码里不用写完整的后端地址后期切换环境只改代理配置即可。4.3 Element Plus 表单动态渲染与校验养老院管理系统的表单很多比如员工信息、药品信息、报修单、外出单。这些表单的共性是字段多、校验规则多、有些字段需要联动比如选择老人后自动带出床位号。Element Plus 的el-form配合rules能做大部分工作。下面是药品入库表单的简化代码展示动态校验和远程搜索template el-form refformRef :modelform :rulesrules label-width100px el-form-item label药品名称 propname el-input v-modelform.name placeholder请输入药品名称 / /el-form-item el-form-item label生产厂家 propmanufacturer el-input v-modelform.manufacturer placeholder请输入生产厂家 / /el-form-item el-form-item label有效期 propexpireDate el-date-picker v-modelform.expireDate typedate placeholder选择有效期 value-formatYYYY-MM-DD / /el-form-item el-form-item label库存数量 propstock el-input-number v-modelform.stock :min0 :max99999 / /el-form-item el-form-item el-button typeprimary clickhandleSubmit提交/el-button /el-form-item /el-form /template script setup import { reactive, ref } from vue import { ElMessage } from element-plus import request from /utils/request const formRef ref() const form reactive({ name: , manufacturer: , expireDate: , stock: 0 }) const rules { name: [{ required: true, message: 药品名称不能为空, trigger: blur }], manufacturer: [{ required: true, message: 厂家不能为空, trigger: blur }], expireDate: [{ required: true, message: 有效期不能为空, trigger: change }] } function handleSubmit() { formRef.value.validate(valid { if (!valid) return request.post(/medicine, form).then(() { ElMessage.success(保存成功) // 关闭弹窗并刷新列表 }) }) } /script逻辑说明formRef.value.validate会触发所有带prop的el-form-item的校验只有全部通过才执行提交。value-format把日期值格式化为YYYY-MM-DD字符串后端实体用LocalDate接收时可以直接转换。el-input-number对数字输入做了约束避免用户输入负数或小数。4.4 ECharts 健康信息图表可视化系统首页和个人健康信息图表用的是 ECharts。图表组件在 Vue3 中使用时需要先npm install echarts然后在组件中按需引入import * as echarts from echarts import { onMounted, onBeforeUnmount, ref } from vue const chartRef ref(null) let chartInstance null function initChart() { chartInstance echarts.init(chartRef.value) request.get(/health/chart).then(res { const data res.data chartInstance.setOption({ tooltip: { trigger: axis }, legend: { data: [体温, 心率] }, xAxis: { type: category, data: data.dates }, yAxis: [ { type: value, name: 体温(℃) }, { type: value, name: 心率(次/分) } ], series: [ { name: 体温, type: line, data: data.temperature, smooth: true }, { name: 心率, type: line, yAxisIndex: 1, data: data.heartRate, smooth: true } ] }) }) } onMounted(() { initChart() window.addEventListener(resize, () chartInstance chartInstance.resize()) }) onBeforeUnmount(() { window.removeEventListener(resize, () chartInstance chartInstance.resize()) chartInstance chartInstance.dispose() })这段代码说明echarts.init必须在 DOM 渲染完成之后调用所以放在onMounted里。后端返回的data包含dates、temperature、heartRate三个数组前端直接映射到双 Y 轴。双 Y 轴的好处是体温36-39和心率60-100数值范围差异大共用单轴会把体温变化压扁。resize事件监听窗口变化组件销毁时移除监听并dispose图表实例防止内存泄漏。5. 部署排错与二次开发从源码包跑通到扩展新功能5.1 启动顺序与常见报错对照拿到源码包后推荐按「MySQL 导入 → Redis 启动 → 后端启动 → 前端启动」的顺序操作。先导入sql目录下的建库脚本再确认 Redis 服务已启动。如果后端启动时报Unable to connect to Redis优先检查 Redis 是否设置了密码。如果设置了密码需要在application.yml里补上spring: redis: password: yourpasswordMySQL 连接报Access denied时检查用户名密码是否写错另外注意serverTimezone必须存在否则高版本 MySQL 驱动会报时区错误。前端npm run dev后页面能打开但接口 404多半是 Vite 代理没生效检查vite.config.js的proxy.target是否指向后端端口。5.2 二次开发新增「床位管理」模块的操作路径如果要在现有系统上加一个「床位管理」功能可按照这个路线改先建表bed_info字段包括room_no、bed_no、elder_id、status在entity包建BedInfo实体在mapper包建接口再在service包写业务逻辑最后加 controller 接口。前端在菜单路由注册一个新页面页面里用el-table展示床位列表分配老人时调用已有的elder下拉接口。整个流程走通了就能理解这套源码的骨架在什么位置。5.3 权限控制的坑按钮级别权限判断源码的菜单权限是通过后端返回的权限标识数组控制的前端用v-permission自定义指令判断按钮是否显示。定义方式如下app.directive(permission, { mounted(el, binding) { const required binding.value const userStore useUserStore() const permissions userStore.permissions if (!permissions.includes(required)) { el.parentNode el.parentNode.removeChild(el) } } })使用方式为el-button v-permissionsystem:user:add新增用户/el-button。这个指令在组件初始化时删除没有权限的按钮缺点是删除后页面布局可能跳动。如果想保留位置可以改用v-show把按钮置为display: none。源码里用的是v-if我建议在按钮比较多的工具栏改用v-show。5.4 统计查询的 SQL 优化建议系统监控模块里有一类「按月统计每个护工处理的报修数量」的接口如果直接用SUM(CASE WHEN ...)在 Java 里拼维护成本高。推荐在 mapper XML 里直接写select idcountByMonth resultTypemap SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS total FROM repair_order WHERE status 3 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC /select逻辑说明DATE_FORMAT将时间字段格式化为YYYY-MM字符串再按月份分组统计。结果集中的month和total直接被 MyBatis-Plus 映射成 Map。注意status 3表示已完成这个枚举值要和业务中定义的状态常量保持一致否则统计结果会偏。对于超过半年的数据建议在create_time上建索引避免全表扫描拖垮接口。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门