ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue智慧养老监护平台实战:从前后端分离到工单闭环

SpringBoot+Vue智慧养老监护平台实战:从前后端分离到工单闭环 做社区信息化项目这些年我接触过不少养老相关的系统需求从早期用PHP拼出来的单体应用到后来SpringBootVue前后端分离的完整平台踩过不少坑也沉淀了不少经验。今天要聊的这套社区智慧养老监护管理平台就是一个很典型的Java全栈实战项目后端SpringBootMyBatis操作MySQL前端Vue做交互界面覆盖老人档案、健康监测、异常告警、护工工单、排班和家属查看等完整闭环。它适合两类人看一类是想系统学习前后端分离项目如何落地的Java开发者另一类是有养老信息化需求、想快速搭建参考原型的团队。这篇文章我尽量把项目拆透了讲从需求分析到表结构从核心代码到常见坑一次性说清楚。先说说这套平台存在的意义。社区养老场景下护工人手有限老人健康数据散落在血压计、心率手环和纸质记录本里一旦出现异常从发现到处警的链路很长。平台要做的就是把分散的数据统一收拢用规则自动识别风险再把处置任务以工单形式派给对应护工同时让家属端能实时看到情况。说白了就是用一条可追溯的线上流程替代原先靠喊、靠跑、靠人肉记录的管理方式。1. 项目拆解社区养老平台到底要解决什么问题1.1 从线下纸质流程看系统的价值锚点我见过不少社区日间照料中心规模不大几十张床位三四个护工。每天早晨的工作是给老人量血压、测心率数据记在一个本子上谁高谁低全凭脑子记。真有老人状态不好护工先喊同事再找值班负责人最后打电话通知家属中间任何一环人不在响应就断了。这套流程的问题不在于人不够敬业而在于数据不集中、流程不标准、责任不可追溯。所以做智慧养老监护平台第一个要解决的就是数据在线化。老人入住后建立完整电子档案包括基础信息、既往病史、过敏史、健康评估等级每天的体征数据通过设备采集或护工录入自动落到健康记录表系统按预设规则判断数据是否异常异常时生成告警再转成工单指派给当班护工。整个链路从数据产生到处置完成都有时间戳和操作人记录出了问题能回溯家属的知情权也有了保障。第二个要解决的是资源调度问题。社区养老最怕的是老人突发状况找不到人。通过排班模块管理员可以提前安排好每个时段的护工力量工单系统则确保每一个告警都有明确责任人。如果护工超时未处理系统自动升级提醒避免以为有人管、实际没人管的情况。1.2 核心角色与业务流程梳理这个平台涉及四类角色权限边界很清楚超级管理员负责系统配置、账号分配、全局数据查看社区运营人员管老人档案、护工排班、工单调度和告警确认护工通过移动端或PC端接收工单、录入健康数据、反馈处置结果家属查看老人健康趋势、接收告警通知、了解护理记录。业务流程我梳理一下这条主链贯穿了系统所有核心表老人入住登记 → 健康评估与等级划分 → 分配床位和设备绑定 → 日常健康数据采集 → 规则引擎判断异常 → 生成告警记录 → 关联排班匹配护工 → 生成工单 → 护工处理并反馈 → 运营人员审核关闭 → 家属端同步查看 → 周期性健康报告生成这套流程下来技术上其实没有什么高深算法难的是把每个环节的状态管理清楚。比如告警状态有未确认、处理中、已关闭工单状态有待派单、处理中、已完成、超时升级每一步流转都要有对应的表字段和接口逻辑。1.3 功能模块地图模块主要功能点说明老人管理档案CRUD、状态流转、健康评估、家属绑定状态含待入住/已入住/暂停/退住健康监测体征数据录入、设备数据导入、趋势图、异常标记心率/血压/血氧/体温告警管理规则配置、告警生成、确认处理、升级机制阈值可配置分级管理工单管理工单生成、派单、处理反馈、超时提醒与排班联动自动匹配护工排班管理日历排班、周排班、班次管理护工和时段的映射关系系统管理用户、角色、菜单、日志RBAC权限模型统计报表老人分布、健康趋势、工单完成率ECharts图表展示2. 技术选型解析为什么是SpringBootVueMyBatisMySQL这套组合2.1 版本选型SpringBoot 2.7还是3.xVue 2还是Vue 3这套项目能流行很大程度上因为技术栈足够主流学习资料多遇到问题好查。但版本选择上我建议别盲目追新。后端如果做毕业设计或者给中小企业做落地项目我个人倾向SpringBoot 2.7.x JDK 8原因很简单生态兼容性最稳。SpringBoot 3.x 强制要求JDK 17虽然性能有提升但很多老项目的依赖要跟着升版比如Springfox迁移到springdoc、MyBatis-Plus要用新版折腾成本不小。当然如果这是全新项目而且团队对JDK 17已经很熟那直接用3.x也没问题代码写法和2.7差别不大。前端这边Vue 3 Element Plus已经是新项目的默认选择。Vue 2虽然还有大量存量项目但官方维护已进入末期。Vue 3的组合式API写业务逻辑比选项式API更清晰一个setup函数里就能把数据、方法、生命周期组织在一起不用再data和methods两头跳。配套的Vite构建速度快开发体验比webpack时代舒服很多。MySQL版本建议8.0以上默认字符集utf8mb4支持JSON类型和窗口函数。8.0的窗口函数在做健康趋势报表、工单统计时非常有用比如计算每位老人的体征均值、排名等一条SQL就能搞定不用在Java里循环处理。2.2 MyBatis与MyBatis-Plus的组合拳为什么不用JPA社区养老这类管理系统复杂统计报表多类似按周统计每位老人的平均心率并对比上周的查询用MyBatis的XML写SQL更有掌控感。JPA虽然能自动建表但一旦涉及多表关联、动态条件、复杂聚合要么写JPQL要么用原生SQL反而不如MyBatis直观。项目实际开发中我建议直接用MyBatis-Plus它相当于是MyBatis的增强工具包内置了单表CRUD、分页插件、逻辑删除、乐观锁和代码生成器。单表操作不用写Mapper XML直接继承BaseMapper调用selectById、insert等现成方法复杂SQL还是保留XML手写两边互补。一个典型的MyBatis-Plus配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); // 设置最大单页限制防止全表扫 pagination.setMaxLimit(500L); // 溢出总页数后是否回到第一页 pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }; } }这里有个细节分页拦截器加了最大单页限制防止前端把pageSize改成999999导致数据库压力骤增。MetaObjectHandler自动填充创建时间和更新时间省去每张表手动set当前时间的重复劳动。2.3 数据库设计健康记录表的核心思路表结构是这类系统的灵魂。我挑数据量最大、设计最关键的health_record表说。CREATE TABLE health_record ( id bigint NOT NULL AUTO_INCREMENT, elder_id bigint NOT NULL COMMENT 老人ID, heart_rate int DEFAULT NULL COMMENT 心率 次/分, sbp int DEFAULT NULL COMMENT 收缩压 mmHg, dbp int DEFAULT NULL COMMENT 舒张压 mmHg, blood_oxygen decimal(4,1) DEFAULT NULL COMMENT 血氧饱和度 %, temperature decimal(4,1) DEFAULT NULL COMMENT 体温 ℃, source_type tinyint DEFAULT 1 COMMENT 数据来源 1设备 2手工录入, device_code varchar(32) DEFAULT NULL COMMENT 设备编号, measured_at datetime NOT NULL COMMENT 测量时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder_measured (elder_id, measured_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康体征记录;这里最关键的是联合索引idx_elder_measured因为查询几乎都是按老人ID和时间范围来做的比如查某位老人最近7天的血压趋势或者查某个时段所有异常数据。如果不建这个索引数据量上来以后这种查询会频繁触发全表扫描。告警规则表我另建一张rule_config字段大致是measure_type指标类型比如心率、min_value、max_value、alarm_level黄色/橙色/红色、enable。这样要调整阈值不用改代码运营人员直接在管理端改了就能生效。实际项目里血氧低于92、收缩压高于140、心率低于50或高于100这些都是常见的默认阈值。3. 核心业务模块与关键代码实现3.1 老人档案与状态流转老人不只是简单的新增和修改状态流转是整个模块最需要谨慎处理的地方。老人状态我设计了四种待入住、已入住、暂停服务、已退住。比如老人请假一星期去子女家住不能直接退住应该进入暂停服务状态对应的排班和设备绑定暂时解绑回来再恢复。状态变更不能只改一个字段就完事要记录变更日志。我专门建了status_change_log表字段包含elder_id、from_status、to_status、change_reason、operator_id、create_time。这样家属或者管理人员问起这位老人为什么从上个月开始状态变成暂停了能直接查日志给出答案而不是靠人工回忆。状态变更的核心Service方法大概是这个思路Transactional(rollbackFor Exception.class) public void changeElderStatus(Long elderId, Integer targetStatus, String reason, Long operatorId) { Elder elder elderMapper.selectById(elderId); if (elder null) { throw new BusinessException(老人档案不存在); } Integer oldStatus elder.getStatus(); // 校验状态流转是否合法比如退住后不能直接恢复为已入住必须走重新入住流程 validateStatusTransition(oldStatus, targetStatus); elder.setStatus(targetStatus); elderMapper.updateById(elder); StatusChangeLog log new StatusChangeLog(); log.setElderId(elderId); log.setFromStatus(oldStatus); log.setToStatus(targetStatus); log.setChangeReason(reason); log.setOperatorId(operatorId); statusChangeLogMapper.insert(log); }状态机校验这里容易踩坑如果不在Service层做合法性校验就会出现数据错乱比如退住老人还在接工单。实际开发中可以用枚举定义状态的允许流转矩阵写起来清晰很多。3.2 健康数据采集与告警规则引擎健康数据是平台的心脏采集频率通常是每5分钟一批。如果一个社区有100位老人、几十台设备一天下来就是上万条记录。直接逐条insert很浪费连接资源我用的是MyBatis-Plus的批量插入能力配合分批提交每次1000条左右。设备端推送的数据一般走消息队列或者HTTP接口。为了保持项目简单我用了WebSocket主动推送让前端大屏实时刷新健康数据如果团队想控制复杂度也可以用前端定时轮询接口这个项目采用长轮询加定时刷新方案实现起来简单量级也能支撑。告警规则引擎是核心。规则配置存在数据库判断逻辑放在Service里。每次采集到新数据后不再用一堆if-else在业务代码里写死阈值而是遍历启用的规则配置逐条匹配public void evaluateRules(HealthRecord record) { ListAlarmRule enabledRules alarmRuleMapper.selectList( new LambdaQueryWrapperAlarmRule().eq(AlarmRule::getEnable, true) ); for (AlarmRule rule : enabledRules) { Double value extractMeasureValue(record, rule.getMeasureType()); if (value null) { continue; } boolean trigger false; if (rule.getMinValue() ! null value rule.getMinValue()) { trigger true; } if (rule.getMaxValue() ! null value rule.getMaxValue()) { trigger true; } if (trigger) { createAlarm(record.getElderId(), rule); } } }告警生成后系统会做一次去重同一老人在同一时间段内同一指标连续异常只生成一条告警防止告警风暴。这个去重逻辑我放在告警生成前查询是否有未关闭的同类型告警有则合并。3.3 工单闭环与排班联动告警只是发现问题处置才是核心。我按工单状态流转来管理处置链路待派单 → 处理中 → 已完成 → 已关闭。如果再细分还可以有超时升级状态。自动派单的逻辑是根据告警老人的床位所在区域查询当天该区域的排班记录找到当班护工生成工单并推送。如果排班没有匹配到护工则工单进入待派单池由运营人员手动指派。排班和工单联动这块有个非常实用的点默认告警级别是黄色需要在30分钟内确认橙色15分钟红色5分钟。系统定时扫工单如果超时未确认自动通知运营人员并给当班护士长生成一条升级任务。这个功能有效解决了线下人不在岗没人管的痛点。我用SpringBoot自带的Scheduled实现定时扫描代码示例Component public class WorkOrderTimeoutTask { Scheduled(cron 0 */1 * * * ?) public void checkTimeoutOrders() { ListWorkOrder pendingOrders workOrderMapper.selectList( new LambdaQueryWrapperWorkOrder() .in(WorkOrder::getStatus, Arrays.asList(0, 1)) ); LocalDateTime now LocalDateTime.now(); for (WorkOrder order : pendingOrders) { Integer level order.getAlarmLevel(); Integer limitMinutes getLimitMinutesByLevel(level); if (now.isAfter(order.getCreateTime().plusMinutes(limitMinutes))) { // 升级处理 upgradeWorkOrder(order.getId()); } } } }注意这里的cron表达式是每分钟执行一次生产环境建议用分布式锁保证多实例部署时只有一个节点执行避免重复扫描。简单方案可以引入Redis的setnx锁或者用ShedLock这类专门给定时任务加锁的库。4. 从零跑通项目的实战记录4.1 环境准备与项目初始化先说环境JDK 8或JDK 17、Maven 3.6、Node.js 16、MySQL 8.0。开发工具我习惯用IntelliJ IDEA前端用VS Code。数据库连接工具推荐Navicat或DBeaver。后端工程创建直接在IDEA里用Spring Initializr选择SpringBoot 2.7.x依赖选Lombok、Spring Web、MyBatis Framework、MySQL Driver。注意MyBatis Framework这里先别选MyBatis-Plus因为Spring Initializr默认生成的是官方starter后面需要手动引入MyBatis-Plus依赖。pom.xml里加上关键依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId /dependency前端工程用Vite创建npm create vitelatest elder-web -- --template vue cd elder-web npm install npm install element-plus axios vue-router4 pinia echarts4.2 application.yml关键配置spring: datasource: url: jdbc:mysql://localhost:3306/elder_care?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 30000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里三个细节值得留意。第一是数据库连接URL一定要加serverTimezoneAsia/Shanghai不然8小时时差问题会让前端展示的时间全错。第二是jackson的日期格式和时间时区要显式配置否则LocalDateTime默认会被序列化成数组或ISO格式。第三是逻辑删除字段deleted所有业务表统一加上这个字段删除操作自动变成update数据保留可追溯。4.3 后端核心接口实现后端我按controller、service、mapper三层组织。业务接口的Controller只做参数接收和结果封装不写业务逻辑。统一返回结构我用了一个Result类code、message、data三个字段前端拦截器统一判断。健康数据查询接口是高频接口做了一个典型的分页查询GetMapping(/health-records) public ResultIPageHealthRecordVO pageHealthRecords( RequestParam Long elderId, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 20) Integer pageSize, RequestParam(required false) String startTime, RequestParam(required false) String endTime) { LambdaQueryWrapperHealthRecord wrapper new LambdaQueryWrapper(); wrapper.eq(HealthRecord::getElderId, elderId) .between(StringUtils.isNotBlank(startTime) StringUtils.isNotBlank(endTime), HealthRecord::getMeasuredAt, startTime, endTime) .orderByDesc(HealthRecord::getMeasuredAt); IPageHealthRecord page healthRecordMapper.selectPage( new Page(pageNum, pageSize), wrapper); return Result.success(page); }注意这里的between条件用了一个布尔表达式作为第一个参数这是MyBatis-Plus支持的条件组装方式。当startTime和endTime为空时这个条件自动不拼接非常方便不用再手写一堆if判断。4.4 前端Vue页面实现前端这边Axios封装是重点。拦截器统一处理token注入和401跳转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] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )路由守卫控制页面权限router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })页面层面健康数据大屏是最出效果的模块。用ECharts做趋势折线图选某个老人后查询最近7天的血压数据横坐标是测量时间纵坐标是血压值超出阈值范围的数据点用红色标记。Vue 3里用echarts的写法是先在setup里引入echarts再通过ref绑定的DOM实例做初始化。图上还可以叠加一个深色警戒区间比如收缩压140到160之间的区域用浅黄色背景让家属一眼看出哪段数据偏高。5. 项目落地时的常见坑与排查手册5.1 分页插件不生效八成是这个原因MyBatis-Plus的分页插件要生效拦截器必须作为内置拦截器加入。常见问题有两个一是直接把PaginationInnerInterceptor单独new出来放进List没有通过MybatisPlusInterceptor包裹导致分页不走二是多个拦截器的顺序不对如果同时用了动态表名或者数据权限拦截器注意顺序。排查方法很简单打开SQL日志看执行的语句是否带了LIMIT关键字。如果带了limit但没有正确解析说明Count语句生成了但又被覆盖了如果完全没有limit那就是插件没生效。检查的时候顺便确认一下mybatis-plus的版本3.5.x和3.4.x在配置写法上略有差异网上很多老教程写的还是3.4之前的配置方式。5.2 健康数据高频写入的数据库压力健康数据批量插入是这类系统最容易出性能问题的点。我实测5分钟一批1000条记录单表单条插入和批量插入的性能差距在百倍级别。解决方案有三个方向一是批量插入合并为一条INSERT语句MyBatis-Plus的saveBatch默认就是先拆成多条再逐条执行需要配置rewriteBatchedStatementstrue才能真正批量。我在MySQL连接串上加了这个参数之后耗时从十几秒降到一秒以内。二是考虑引入Redis做缓冲。设备数据先推到Redis的List或者Stream里定时任务批量消费写入MySQL。这样即使设备瞬时上报量很大数据库也不会被直接打满。对于中小项目每批次1000条已经够用不一定非上Redis。三是对历史数据做归档。health_record表越来越大后按月分表或者定期把超过6个月的冷数据迁移到history表。分表方案对查询逻辑会有一点侵入需要根据路由字段处理初期建议先做归档。5.3 跨域、时区、序列化三件套前后端分离项目跨域问题几乎必踩。如果后端接口在8080前端Vite在5173浏览器会拦截。后端的处理方式我一般配置一个CorsFilter允许指定前端的来源访问而不用全局*号。SpringBoot 2.7里可以用WebMvcConfigurer的addCorsMappings统一配置。同时在Spring Security的配置里也要同步放行OPTIONS预检请求否则前端请求直接被拦截。时区问题前面提过连接URL里的serverTimezoneAsia/Shanghai务必加上。日期返回格式如果实体里用的是LocalDateTimeJackson需要加jackson-datatype-jsr310依赖然后配置date-format。否则前端拿到的可能是一大串数组或者格式是2025-01-15T10:30:00而不带毫秒精度页面显示很不友好。序列化的另一个大坑是MyBatis-Plus返回的分页对象Page用JSON序列化后会把records和total等字段正常返回但前端如果用了这套项目自带的类型定义要注意Page里的records字段在小写开头的JSON里是否被正确解析。我建议统一用VO对象做转换不要在Controller里直接把实体类返回前端这样字段可控、也避免把不该暴露的内网字段泄漏出去。5.4 部署阶段的注意事项部署上后端打jar包用java -jar运行前端构建后放nginx。有几个细节值得说第一个是nginx需要配置反向代理转发/api路径到后端地址location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }前端请求的baseURL是/api后端Controller的路径统一以/api开头这个约定在联调时非常省心。注意proxy_pass末尾的斜杠它会替换掉匹配到的/api前缀如果配置不当会出现404。第二个是数据库连接池。MyBatis-Plus默认使用HikariCP我建议显示配置maximum-pool-size一般20个连接足够支撑几百个老人规模的社区。如果高峰时段连接不够看日志里有没有Connection is not available, request timed out的报错。第三个是SpringBoot的打包。用spring-boot-maven-plugin打包但注意排除测试类不然测试代码里如果连了开发库打包过程中会有中断风险。建议deploy前跑一遍完整测试没问题再跳过测试打包。最后分享几点我的实操体会这套项目做完我最深的感受是一个管理系统能不能被运营人员真正用起来关键不在于界面多炫、技术多新而在于流程是否符合实际工作习惯。健康监测和告警做得再漂亮如果工单派发不合理护工看得到却处理不了最终这个功能会被弃用。所以后来我调整设计时把排班和工单的联动放在第一优先级宁可放弃一些花哨的图表也要保证告警发出来有人接、工单生成后看得见、处理完有记录这三点稳定可靠。另外想提醒一点养老行业的数据合规和隐私保护要提前考虑老人健康数据属于敏感个人信息系统里照片、身份证号、健康记录等字段都需要做权限控制和脱敏展示。技术层面做到字段权限隔离管理层面做好账号审计日志这是这类项目真正能不能落地的一个隐藏门槛。如果你准备拿这套项目做二次开发我的建议是先跑通老人档案、健康数据、告警和工单这条主线其他模块比如排班、报表、家属端可以后续迭代。主线跑通系统的骨架就立住了再往上面加房间管理、设备管理、收费管理都会很顺。
返回列表