接手这个"基于SpringBoot与Vue开发的智慧养老服务平台"之前,我以为它跟普通的后台管理系统差别不大,无非是增删改查套一层管理界面。真正把项目拆开做下去才发现,养老场景的特殊性会直接拉高整个系统的复杂度:服务对象是老人,使用者是家属、护工、运营人员、社区管理员,角色的碎片化程度远超普通企业应用;数据链路也不只是表单提交,还牵扯健康设备、定位设备、音视频通话、紧急救助这类实时性业务。而SpringBoot加Vue这套组合,刚好在"快速落地"和"支撑复杂业务"之间给了一个比较稳的平衡点,这也是我最终选定这个技术栈的原因。
这篇文章会从需求拆解、后端建模、前端页面、联调部署到问题排查,把整个"爱老助老"系统的完整开发过程掰开揉碎讲一遍。无论你是拿它当毕业设计,还是想给社区、养老机构做一个真实可用的平台,这里面的功能规划思路、权限设计、设备对接、文件存储、打包部署方案,基本都能直接抄作业。对SpringBoot和Vue怎么前后端分离协作还不熟的人,也可以按这篇文章的节奏把项目从零到一拉起来。
1. 需求拆解与功能架构设计
1.1 智慧养老到底在解决什么问题
养老服务平台如果只做老人信息的录入和管理,那叫"老人台账",谈不上"智慧"。这个项目的核心命题是:如何用一套系统把独居老人的日常安全、服务工单流转、家属远程关注、护工上门服务这几件事串成一个闭环。
我见过不少同类项目死在功能堆砌上——今天加一个健康问答,明天加一个活动报名,最后页面一大堆,核心业务反而没人用。真正用得起来的养老平台,本质上都是在回答三个问题:
- 老人状态怎么样(健康数据、位置、是否发生异常)。
- 老人需要什么帮助(助餐、助浴、助医、紧急呼叫)。
- 谁来解决、解决得怎么样(工单分派、上门记录、家属反馈)。
所以我在设计这个项目的时候,主流程明确为:设备采集数据 → 系统分析判断 → 生成服务需求或告警 → 分派工单 → 服务执行与反馈 → 家属同步知情。所有模块都围绕这条主链路展开,其他功能全部靠边。
1.2 功能模块边界划分
按照上面的主流程,我把系统划分为六个核心模块,每个模块的定位在开工前就要定死:
| 模块 | 核心职责 | 典型功能点 |
|---|---|---|
| 老人档案 | 老人的基础信息、健康档案、家属关系 | 基本信息、既往病史、紧急联系人、居住地址定位 |
| 健康监测 | 对接智能手环/血压计等设备,采集健康指标 | 心率、血压、血氧数据展示、异常阈值告警 |
| 位置与安全 | 老人定位、电子围栏、SOS紧急呼叫 | 实时定位、围栏越界告警、一键呼叫 |
| 服务工单 | 助餐/助浴/助医等服务的预约、派单、完成闭环 | 服务下单、护工抢单、服务记录、评价反馈 |
| 家属互联 | 家属对老人状态的知情与远程互动 | 健康报告推送、视频通话入口、紧急通知 |
| 运营管理 | 平台侧的日常运营配置 | 护工管理、服务项目配置、数据统计报表 |
模块边界划分的另一个好处是数据库表结构能跟着模块走,后面做权限控制时,按模块分配菜单和接口就非常顺。很多新手项目后期改不动,就是前期模块没有边界概念,所有的功能全堆在几张"万能表"里。
1.3 技术选型:为什么是SpringBoot + Vue
这个项目的前后端分离架构,我是在对比过单体JSP方案、纯模板渲染方案之后定的。核心原因有三点。
第一,前后端分离能并行开发。SpringBoot负责纯接口,Vue负责页面交互,定义好接口文档之后,前端和后端可以同时开工,周期明显缩短。对毕设或者小团队来说,这个效率优势很致命。
第二,SpringBoot的生态对快速搭建太友好了。自动装配把大量配置逻辑封装在起步依赖里,Spring Security做认证授权、MyBatis-Plus做数据持久化、MinIO做文件存储、Docker打包部署,每一环都有成熟的集成方案。项目能跑起来的时间从一两周压缩到两三天。
第三,Vue的组件化和响应式机制贴合管理端页面的开发习惯。页面碎片多、状态联动频繁(比如选择了老人之后,健康数据和工单记录要联动刷新),Vue的响应式数据绑定能省掉大量手动操作DOM的代码。组件复用也直接,老人卡片、工单状态标签、图表组件都可以抽出来反复用。
2. 后端架构与核心实现要点
2.1 SpringBoot项目工程结构规划
后端工程我没有用单模块结构,而是直接按多模块Maven工程来组织,这算是从第一个版本踩坑得到的教训。早期图省事把所有代码都放在一个模块里,结果业务类互相引用没有边界,后来接口一多就乱成了线团。
参考结构如下:
parent-pom ├── common // 公共工具、统一返回体、全局异常、常量 ├── system // 登录、权限、用户管理、菜单管理 ├── elderly // 老人档案、健康数据、定位信息 ├── service // 工单服务、护工管理、服务项目 ├── message // 消息推送、通知记录 └── api // 对外接口的Controller层统一入口common模块存放Result统一返回体、BusinessException、全局异常处理器、JWT工具类、日期工具等,所有业务模块都依赖它。system模块负责Spring Security的过滤链配置和登录认证,因为所有接口都要过权限校验,所以它被其他模块依赖。
Controller层统一收口在api模块,这样做的好处是接口路由清晰,而且做接口文档(Swagger/Knife4j)扫描时不用每个模块单独配。实际编码时Service层尽量面向接口编程,Mapper层用MyBatis-Plus的BaseMapper,复杂的联表查询才写XML,这样常规单表操作几乎不用手写SQL。
2.2 核心数据表设计
表结构是一个养老平台的骨架,我前后调整过三轮才稳定下来。核心表主要有这么几张:
老人档案表elder_info
字段名 类型 说明 id bigint 主键 name varchar(32) 老人姓名 gender tinyint 性别 birth_date date 出生日期 id_card varchar(18) 身份证号 phone varchar(20) 本人电话 address varchar(255) 居住地址 lat decimal(10,6) 纬度 lng decimal(10,6) 经度 health_level tinyint 健康等级:1自理 2半自理 3失能 emergency_name varchar(32) 紧急联系人 emergency_phone varchar(20) 紧急联系电话 family_id bigint 关联家属账号 create_time datetime 创建时间注意lat和lng,这是做人位置服务的关键字段。很多系统把老人的地址只存成字符串,后面做电子围栏和地图撒点的时候就傻了,坐标必须要单独存成数值类型,配合腾讯地图或高德地图的逆地理编码使用。
健康监测记录表health_record
字段名 类型 说明 id bigint 主键 elder_id bigint 老人ID device_code varchar(64) 设备编号 heart_rate int 心率 blood_pressure_high int 收缩压 blood_pressure_low int 舒张压 blood_oxygen int 血氧饱和度 measured_at datetime 测量时间这张表是典型的高频写入表。如果设备接入量大了,建议按时间分表或分区,不然一年几百万条数据会让查询明显变慢。我项目里先做了按月分表的预留逻辑,写入时通过elder_id和measured_at双条件路由,避免后期返工。
服务工单表service_order
字段名 类型 说明 id bigint 主键 order_no varchar(32) 工单编号 elder_id bigint 老人ID service_type tinyint 服务类型:1助餐 2助浴 3助医 4陪诊 nurse_id bigint 护工ID status tinyint 状态:1待接单 2已接单 3服务中 4已完成 5已取消 appoint_time datetime 预约时间 address varchar(255) 服务地址 remark varchar(500) 备注 create_time datetime 创建时间 finish_time datetime 完成时间工单表是业务流转的中枢,状态字段的设计直接影响后续的列表筛选和统计报表。我在状态字段上加了索引,因为运营后台的工单列表基本都按状态查。
2.3 健康监测数据接入与异常告警实现
健康监测的常见误区是"设备数据到了就存库、存库就展示",这样做出来的功能只是个数据仓库,对老人和家人来说没有实际价值。真正的价值在于判断异常和触发响应。
设备端的对接方式,我用的是MQTT协议上报。智能手环等设备通过网关把数据推送到EMQX Broker,SpringBoot后端通过@MqttListener订阅主题接收。手环上报的数据格式大概是这样的:
{ "deviceCode": "HW-2024-0001", "heartRate": 128, "bloodOxygen": 91, "bloodPressureHigh": 168, "bloodPressureLow": 102, "measuredAt": "2024-09-21 08:30:00" }收到数据后,除了存库,还要过一个告警规则引擎。我没有引入独立的规则引擎组件,而是用策略模式实现了几种阈值判断:
- 心率超过120或低于50触发心率异常告警。
- 血压收缩压超过160或舒张压超过100触发高血压告警。
- 血氧饱和度低于90触发低血氧告警。
- 定位数据判断老人是否离开设定的电子围栏半径。
告警产生后,要根据老人的health_level和紧急联系人配置决定通知渠道。这里我把通知渠道拆成了短信、App站内信、企业微信应用消息三种。实际项目里企业微信的应用消息比站内信好用得多——家属一般不会每天打开App,但企业微信的提醒是能实时弹出来的。在项目里通过企业微信的JS-SDK和服务端API对接,发送文本卡片消息,家属点击卡片就能跳转到老人详情页。
2.4 从自动装配原理到自定义业务组件
SpringBoot开发到一定阶段,光会用框架是不够的,得理解它的自动装配原理,否则遇到启动报错、配置不生效就只能干瞪眼。
@SpringBootApplication实际上是@Configuration、@EnableAutoConfiguration、@ComponentScan的组合。其中@EnableAutoConfiguration会读取META-INF/spring.factories(Spring Boot 2.7之后是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)文件里的配置类列表,按条件注解@ConditionalOnClass、@ConditionalOnProperty决定是否装配对应的Bean。
我在项目中基于这套机制做了一个ElderAutoConfiguration,把健康告警规则引擎、定位围栏计算服务、护工接单状态机都封装成了自有的自动配置模块。这样在另一个项目里面如果需要复用这套养老业务逻辑,只要引入Maven依赖,配置文件里写上开关,规则引擎就自动生效了。这就是为什么面试总问自动装配原理——它不只是理论的,是可以在真实项目里拿来设计公共模块的。
3. Vue前端工程与核心页面实战
3.1 项目初始化与目录组织
前端项目我用的Vite创建,相比Vue CLI,Vite的开发冷启动速度快很多,对TypeScript的支持也更干净。创建命令:
npm create vite@latest elderly-web # 选择 Vue 3 + TypeScript 模板 cd elderly-web npm install推荐使用Vue 3的<script setup>语法。比选项式API更简洁,响应式变量直接声明,用起来跟写原生JavaScript差不多,团队上手成本低。
前端目录结构按业务模块拆:
src ├── api // 接口请求封装,按后端模块分文件 ├── assets ├── components // 公共组件 ├── layout // 主布局框架,侧边栏、顶栏、面包屑 ├── router // 路由表 + 动态路由生成逻辑 ├── store // Pinia状态管理 ├── styles └── views // 页面 ├── dashboard // 大屏首页 ├── elderly // 老人档案 ├── health // 健康监测 ├── order // 工单管理 ├── message // 消息通知 └── system // 系统管理封装axios实例是老规矩,但我这里要单独说的是token过期处理。养老平台里有大量接口是家属端和运营端共用的,Token过期后不能只弹个报错,要在拦截器里做统一的刷新逻辑。我用的是双Token方案:accessToken有效期2小时,refreshToken有效期7天,拦截器捕获401错误后自动用refreshToken换取新的accessToken,重放原请求。这个体验对家属端App特别重要,不然老人家属打开小程序看到登录过期,还要重新输手机号收验证码,损耗非常大。
3.2 适老化UI设计的几个关键点
智慧养老平台的前端跟普通管理系统有个本质区别:它除了运营人员在电脑上用,还有很大几率被老人和家属在手机、平板上用。所以UI设计不能只考虑管理端的密度和信息冗余,更要考虑适老化。
我在项目里沉淀了几条适用性原则:
- 字号最小16px。老人视力下降是普遍的,小于16px的文字在手机上看着非常费劲。
- 操作按钮高度不低于44px。这是移动端触控的基本建议值,老人的手部精细动作能力下降,太小的按钮容易误触。SOS紧急呼叫按钮我特意做成了悬浮固定在右下角,红底白字,金色描边,长按3秒触发,避免误触。
- 颜色对比度要够。浅灰字配白底,年轻开发觉得好看,老人根本看不清。正文用
#333333,辅助文字用#666666,禁用状态才用浅色。 - 关键信息卡片化+语音播报。健康数据页面我做了语音播报按钮,点击后通过Web Speech API读出血氧、心率是否正常。试运营时发现这个功能的使用率非常高,很多独居老人早上起床会点一下听数据。
3.3 动态路由与权限控制
管理后台的菜单权限是靠动态路由实现的。登录后后端返回当前用户的角色和权限标识列表,前端根据这些数据过滤出该角色可见的页面路由,通过router.addRoute动态挂载。
这里有个非常经典的坑:菜单权限和按钮权限要分开。动态路由负责控制"你能不能看到这个菜单",但一个页面上可能有"编辑"、"删除"、"审核"好几个按钮,不同角色的按钮权限不同。我在代码里用了自定义指令v-permission="'health:export'"控制按钮显隐,后端接口再做一次权限校验。这样就算前端按钮被绕过,直接调接口也会被拒,安全上才是闭环的。
动态路由刷新后白屏是高频问题。原因是addRoute进去的路由存在内存里,浏览器刷新后状态丢失。解决方法是把用户角色信息存到本地存储(localStorage),刷新后从本地读取角色ID,重新请求一次该角色的菜单配置,再动态生成路由。这个过程要放在路由守卫的beforeEach里做成异步,保证路由挂载完成后再放行。
3.4 地图定位与视频播放实现
老人定位功能,我用的是腾讯地图JavaScript API。选它而不是高德,原因很简单:之前项目里高德地图Web服务端的逆地理编码配额经常不够用,腾讯这边对个人开发者的额度更友好。初始化加载:
<!-- public/index.html 中引入 --> <script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&key=YOUR_KEY"></script>地图页面里根据经纬度渲染老人的位置标记,同时绘制以老人家为中心的电子围栏。
摄像头画面接入是这个项目里比较"进阶"的场景——老人在家里装了一个智能摄像头,家属希望能通过平台实时查看画面。摄像头推流后生成的是HLS格式的m3u8地址,Web端用hls.js播放:
import Hls from 'hls.js' export function initHls(videoElement, m3u8Url) { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(m3u8Url) hls.attachMedia(videoElement) } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { // iOS Safari 原生支持 HLS videoElement.src = m3u8Url } }这个场景的另一个问题是视频流地址通常带时效签名,后端统一通过一个接口返回带签名的播放地址,前端拿到之后再传给播放器,避免地址直接写在页面上导致泄露。
4. 前后端联调、私有化存储与容器部署
4.1 前后端分离联调中的几个关键配置
前端用Vite开发服务器,后端跑在8080端口,跨域问题绕不开。开发环境我用Vite的代理解决,避免生产环境还要处理CORS的预检请求问题:
// vite.config.ts export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, // 后端接口统一前缀为 /api,代理时不需要重写 } } } })生产环境则由Nginx统一反向代理,把/api转发到后端的SpringBoot容器。这样整个链路里浏览器始终只跟同源的Nginx通信,CORS基本用不上。
接口文档我用了Knife4j(Swagger的增强版),理由就一个:接口调试界面可以直接在线测试,而且导出的OpenAPI文档可以喂给Apifox自动生成前端类型定义文件。前后端对接的效率提升非常明显,前端同事不再拿着一份静态文档手敲TypeScript类型了。
4.2 MinIO私有化文件存储接入
项目里涉及老人照片、身份证附件、摄像头的抓拍图。本来可以直接用阿里云OSS,但考虑到养老数据敏感,而且项目可能部署在社区内网的服务器上,我选了MinIO做私有化对象存储。
MinIO的接入方式相当成熟。服务端安装后,SpringBoot里引入依赖:
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency>配置application.yml:
minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket-name: elderly-system封装上传工具类后,有两点经验值得提:一是上传时文件名要重命名,用UUID或日期+随机字符串,不要保留用户上传的中文名,否则在URL里会有一堆编码,且容易出现文件名冲突;二是上传成功后返回的对象要包含MinIO预签名的访问地址或临时凭证。桶的权限建议设为私有,服务端生成带有效期的预签名URL返回给前端直接展示图片,既保护了文件不被爬走,又不用把AccessKey暴露给前端。
4.3 Docker部署SpringBoot与Vue整套服务
部署环境我用的Docker Compose编排,整套包含四个容器:
elder-server:SpringBoot后端elder-web:Nginx + Vue构建产物minio:对象存储服务mysql:数据库
后端Dockerfile:
FROM maven:3.8-openjdk-17 AS build WORKDIR /app COPY pom.xml . COPY common/ common/ COPY system/ system/ # 其他模块省略,先复制依赖再复制源码,利用Docker构建缓存加速 COPY . . RUN mvn clean package -DskipTests -q FROM openjdk:17-jre-slim WORKDIR /app COPY --from=build /app/api/target/api.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]前端Dockerfile:
FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80关键配置在Nginx的nginx.conf里。重点处理两个问题。
一是Vue路由用history模式时,刷新页面会出现404。因为前端路由是虚拟的,Nginx不知道/elder/detail对应哪个静态文件。配置如下:
server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://elder-server:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html;这行就是解决history路由刷新404的关键。
二是前端容器要能访问到MinIO。MinIO的预签名URL在生成时如果填的是http://127.0.0.1:9000,前端在浏览器里访问不了。这里需要在MinIO配置项里把endpoint换成局域网可访问的IP或域名。这个坑非常隐蔽,很多项目部署到服务器上图片突然加载不出来了,八成就是这个原因。
4.4 多环境配置与SpringBoot配置管理
项目分了dev、prod两套环境配置文件,通过启动参数--spring.profiles.active=prod切换。数据库连接、MinIO地址、Redis地址、JWT密钥分别放到对应环境文件里。
有个小细节:application.yml里如果配置了server.port=0,SpringBoot会随机分配一个可用端口启动,多实例部署时防止端口冲突。但生产环境我一般不这么干,因为Nginx反代需要固定的上游端口。随机端口更多用于本地多实例联调的测试场景。
另一个容易被忽略的问题是banner.txt——SpringBoot启动时那个ASCII字符画。项目交付时我在resources下放了一个定制banner,启动日志一眼能看到环境标志,运维部署时判断版本和环境非常直观,算是一个低成本高体验的小技巧。
5. 常见问题与排查技巧实录
5.1 SpringBoot版本与依赖冲突
SpringBoot 3.x相对2.x变化很大,最明显的是javax包名改成了jakarta,很多老教程的代码直接搬过来会编译报错。我的建议是:小团队做新项目直接上SpringBoot 3.x + JDK 17,不要回头用2.x;但如果你依赖的某个第三方组件还没适配jakarta,那就老老实实退回2.7.x,别硬扛。
遇到过最头疼的问题是依赖冲突。比如项目中同时引入了旧版的commons-lang3和某个中间件自带的传递依赖,启动时方法找不到。排查办法我总结了一套流程:
- 用
mvn dependency:tree看依赖树,找到冲突的jar。 - 用
mvn dependency:analyze检测未使用和重复的依赖。 - 在
pom.xml中用<exclusions>排除特定传递依赖。 - 必要时用
mvn dependency:tree -Dverbose看冲突详情。
要注意的是,直接删掉不确定用途的传递依赖有可能引发NoClassDefFoundError,所以排除前最好确认当前项目的代码里是否真的用到了被排除的类。
5.2 拿到Jar包如何反编译排查线上问题
线上问题排查时经常遇到一个场景:现场环境出Bug,但是没有Git仓库权限,只能拿到一个可运行的Jar包。这时候需要反编译去看代码到底跑的是什么逻辑。
我的做法是用CFR(一个Java反编译器),社区里也有JD-GUI和Luyten可视化工具。命令行方式最通用:
java -jar cfr.jar app.jar --outputdir ./src-decompiledCFR会把Jar里的class文件反编译成Java源码文件,然后用IDEA打开这个源码目录搜索关键字。值得注意的是,反编译出来的代码在泛型、Lambda表达式、switch-case等语法结构上会有变形,定位逻辑可以,但不能直接当源码改。排查完之后,正确的修复流程还是在Git仓库里改代码、重新构建、替换线上Jar包。
5.3 Vue依赖安装失败与页面白屏
npm install失败是前端开工第一道坎。常见原因有网络问题、Node版本与依赖不兼容、缓存脏数据。在国内环境我基本把npm源换成淘宝镜像:
npm config set registry https://registry.npmmirror.com如果安装后启动报SyntaxError,先检查Node版本,Vue 3 + Vite通常要求Node 18+,版本不够就升级Node。还有一个容易被忽略的问题:package-lock.json是从其他环境带过来的,里面锁定的依赖版本跟你当前环境不匹配。这种情况我一般先删掉锁文件和node_modules,重新安装。
页面白屏的问题,先开浏览器开发者工具看Console报错。最常见的是接口跨域报错,其次是路由配置里某个组件路径写错导致加载失败。Vue Devtools插件对排查组件状态问题帮助很大,建议Chrome必装。
5.4 静态文件与多媒体播放兼容性
HLS视频流播放时,Chrome和FireFox需要hls.js,Safari原生支持直接指定src。另外,如果m3u8地址是HTTP而页面是HTTPS,会被浏览器拦截为混合内容,导致无法播放。解决方式是把视频播放地址也走HTTPS或者反代转发,这个在对接家用摄像头时特别常见。
腾讯地图API加载慢的问题,也可以用动态加载的方式规避,等用户进入定位页面时才插入script标签,而不是在入口HTML里直接引。这样首页首屏加载时间能明显下降。
5.5 数据安全与误操作恢复
养老平台涉及大量老人隐私数据,开发时可以偷懒,上线了不能偷懒。我的建议是至少做到三条:
- 操作日志。设计一张
operation_log表,记录谁在什么时间对哪条老人数据做了什么操作。用Spring AOP或MyBatis-Plus的元数据填充都行,这个功能不能在系统上线后再补。 - 回收站机制。删除老人档案时做逻辑删除(
deleted字段),而不是物理删除。试运营期间家属说老人换个地址要重新登记,结果误删了档案,如果没有逻辑删除位,数据就真的没了。 - 定时备份。MySQL每天凌晨用
mysqldump备份全库,保留最近7天。配合MinIO的版本控制,文件误删也能找回。
6. 个人实操总结
整个项目做下来,我最深的一点体会是:养老平台的技术难点不在某个单点技术上,而在业务的完整闭环上。单个模块拆出来看,健康监测是简单的阈值判断,工单管理是普通的状态机流转,地图定位是调用第三方API。但把它们串起来,覆盖"老人状态变化→系统识别→服务介入→家属知情"这条完整链路,才是这个系统真正值钱的地方。
另外,如果你也想做类似的毕设或者真实项目,建议把开发重心放在"两个打通"上:一是打通数据,把健康设备、位置设备、人工录入的数据统一到一个老人档案里;二是打通通知,让告警消息能顺畅触达家属、护工、运营人员。这两条线做好了,系统的实用价值立刻就能体现出来。SpringBoot和Vue在这套体系里只是载体,真正贯穿项目始终的,是你对养老服务场景本身的理解深度。这个认知,比任何框架知识都重要。