☰
智慧养老平台Java实战:设备接入、告警引擎与多租户隔离的落地写法
2026/10/9 1:34:25 网站建设 项目流程

简介:一套基于SpringBoot的智慧养老平台Java源码,采用B/S架构与MVC分层设计,整合Mybatis、Vue、Ajax等技术,适合计算机、电子信息专业的学生用于毕业设计、课程设计或期末作业。压缩包约17.83MB,共950个文件,其中包含195个Java后端逻辑、65个Vue组件、69个HTML页面、164个JavaScript脚本与53个CSS样式等,涵盖服务端核心代码、前端界面与静态资源,还提供安装和运行批处理脚本,便于快速部署。已有338人学习浏览,所有源码均经过严格测试,可直接放心下载。代码环境为JDK1.8、Maven3.6、MySQL5.7,支持IDEA、eclipse等IDE打开,目录结构按前后端模块划分清晰,可配合Navicat导入数据库后运行,完整展现智慧养老平台的功能流程,是学习SpringBoot实际项目开发的不错参考,也能为毕业设计提供基础框架。

1. 智慧养老平台用 Java 写,难点根本不在 CRUD

一个养老平台的后台,表面上就是“老人档案 + 健康设备 + 服务工单”的增删改查,Java 做这种系统再顺手不过。但真正让项目卡住的从来不是表结构,而是三件事:设备上报的数据怎么可靠接进来,告警怎么在“该响的时候响、不该响的时候别炸”,以及多机构运营时权限和数据怎么隔离。这篇笔记就按这三个难点展开,给出一套用 Spring Boot + MyBatis-Plus + Redis 就能落地的 Java 实现思路,覆盖表设计、核心接口、权限边界和上线后的典型故障。适合正在做智慧养老、健康监测或类似 IoT 后台的 Java 工程师,也适合准备接手这类项目的技术负责人——看完你能判断方案规模,也能直接照着把主链路跑通。

2. 技术选型与数据建模:单体起步,还是直接上微服务?

2.1 单体先行的理由:Java 生态里最稳的落地组合

智慧养老平台在国内的落地形态,我接触过的绝大多数是三种:给民政或街道做的监管大屏,给养老机构做的运营管理系统,给子女端做的健康状态查看。这三种形态有一个共同特点——用户量不大,但数据链路长。一个老人一天可能上报几百条健康指标,设备类型从手环、血压计到智能床垫都有,真正考验系统的不是并发,而是接入层的稳定性和数据流的完整性。

我一般会建议单体起步,用 Spring Boot + MyBatis-Plus + MySQL 8 + Redis 这套组合。微服务不是不能用,而是养老平台的核心矛盾在业务复杂度而不在性能瓶颈。项目早期拆成微服务,光服务间调用、分布式事务、配置中心这三件事就能消耗掉大半开发资源。正确做法是先把模块边界想清楚——设备接入、健康档案、工单运营、告警中心、系统管理——代码层面按包结构隔离,将来量级上来了再按包拆服务,不用重写。

选 MyBatis-Plus 而不是纯 MyBatis,是因为这类平台有大量固定的单表操作和分页查询,MyBatis-Plus 的 BaseMapper 能省掉一半的样板代码。但要注意,复杂统计查询不要偷懒用 LambdaQueryWrapper 硬拼,该写 XML 的还是要写 XML,后面第 6 章会专门说这个问题。

2.2 三张核心表:老人档案、健康记录、服务工单的字段与索引设计

养老平台的数据模型,第一版建议聚焦四张表:机构表、老人档案表、健康指标记录表、服务工单表。机构表是所有数据隔离的根,老人档案表是业务主数据,健康指标记录表是写入量最大的表,服务工单表则是运营闭环的载体。

老人档案表的核心字段要包含:老人唯一编号、姓名、性别、年龄、机构 ID、床位号、紧急联系人电话、历史病史。这些字段里有三个特别容易忽视——机构 ID 必须建索引,因为所有查询都要带它做数据隔离;紧急联系人电话要设计成单独字段而不是塞在备注里,告警通知要直接用;床位号建议单独建一个字段,养老机构里“哪个床位”比“叫什么名字”更常用于日常定位。

健康指标记录表的写入模式是典型的时序数据特征,但量级又没到非要上时序数据库的程度。MySQL 完全能扛住,前提是索引设计要克制。推荐索引只有两个:一个是(elder_id, event_time)用于查单个老人的历史趋势,一个是(org_id, report_time)用于机构维度的报表统计。千万不要为了“以后可能用到”给每个字段都加索引,这张表写入频繁,索引过多会让插入性能明显下降。

服务工单表相对简单,核心是状态机设计。建议用状态字段加操作记录表的方式,状态字段只存当前状态(待派单、进行中、已完成、已取消),操作记录表存每次变更的操作人、操作时间和备注。不要用状态字段的字符串值直接当业务日志用,后面追溯责任时会发现信息全丢。

2.3 设备接入层选型:HTTP 上报、MQTT、Netty TCP 怎么选

设备接入是智慧养老平台里最“经验主义”的部分。市面上的养老设备厂商,协议风格大致分三类。第一类是 HTTP JSON 上报,最常见,设备定时往你的接口 POST 一条 JSON,字段里带设备编号、老人编号和各项指标值。第二类是 MQTT 上报,设备连到你的 MQTT Broker,按主题发布消息。第三类是老旧的私有 TCP 协议,需要你自己解析字节流,通常出现在床垫类或部分医疗类设备上。

接入方式优点缺点适用场景
HTTP JSON开发最快,调试方便,前后端同一套技术栈设备侧容易丢消息,无重试机制血压计、体脂秤等低频设备
MQTTQoS 机制保证消息可靠,天然支持海量设备连接需要额外部署 Broker,客户端调试略麻烦手环、胸卡等高频上报设备
Netty TCP兼容最老的一批设备,字节级可控协议解析要自己写,连接管理复杂度高床垫、生命体征监测仪等私有协议设备

我的建议是:第一版统一用 HTTP JSON 接入,但接口设计上为另两种协议留好扩展点。做法是 Controller 层只做协议转换,真正的业务逻辑下沉到 Service 层。这样后面接 MQTT 设备时,只需要新写一个 MQTT 的监听器调同一个 Service,不用动核心代码。这个设计看似多花半天时间,后面接新设备时能省下数倍的时间。

3. 用 Spring Boot 把“设备上报 → 健康档案 → 告警通知”这条主链路跑通

3.1 项目骨架:Maven 依赖与启动类的最小配置

新建一个 Spring Boot 项目,核心依赖就五个:web、mybatis-plus、mysql-connector、redis、lombok。很多人会纠结要不要引入 Spring Cloud 那一套,第一版完全不需要。依赖越少,启动越稳,排查问题越快。

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

启动类不需要额外配置,标准的@SpringBootApplication加@MapperScan扫描 Mapper 包即可。需要强调的是,MyBatis-Plus 的版本要选对,3.5.x 系列相对稳定,太老的版本对 Spring Boot 3.x 支持不好。如果你用的是 Spring Boot 3.x,注意还要引入mybatis-plus-spring-boot3-starter而不是mybatis-plus-boot-starter,这个坑在第一次建项目时最容易踩。配置里的 MySQL 时区参数一定要显式指定,否则后面时间字段全部差八小时,第 5 章会展开说。

3.2 设备数据上报接口:Controller 只做转换,Service 负责落库

设备上报接口的设计原则是“入口宽容,落库严格”。设备厂商传的字段名五花八门,有的传 heartRate,有的传 heartbeat,还有的直接传一个 JSON 字符串。Controller 层要做的就是把各种格式统一成内部数据结构,然后交给 Service。

@RestController @RequestMapping("/api/device") public class DeviceReportController { private final HealthRecordService healthRecordService; public DeviceReportController(HealthRecordService healthRecordService) { this.healthRecordService = healthRecordService; } @PostMapping("/report") public Result report(@RequestBody DeviceReportDTO dto) { // 设备编号转内部老人ID的映射逻辑,这里省略查库过程 HealthRecord record = new HealthRecord(); record.setElderId(dto.getElderId()); record.setOrgId(dto.getOrgId()); record.setHeartRate(dto.getHeartRate()); record.setBloodOxygen(dto.getBloodOxygen()); record.setEventTime(dto.getEventTime()); // 服务器接收时间独立保存,用于排查上报延迟问题 record.setReportTime(LocalDateTime.now()); healthRecordService.saveRecord(record); return Result.ok(); } }

这段代码的关键在设计了两个时间字段:event_time是设备端产生的数据时间,report_time是服务器接收时间。这两个字段必须分开存,否则排查“数据为什么延迟半小时才显示”这类问题时没有任何依据。Service 层也不建议直接调 MyBatis-Plus 的save方法,而是要包一层saveRecord,因为批量上报场景下,这里要做合并写入优化,第 6 章会给出具体优化方案。参数上,DeviceReportDTO里建议增加一个version字段,用于兼容设备端协议升级,避免上线后改字段名要通知厂商重新发布。

3.3 告警判定:规则表驱动 + Redis 去重

告警是智慧养老平台最核心的能力,也是最容易翻车的模块。第一版很容易做成“if 心率大于 120 就发短信”这种硬编码,改一条规则要重新发版。正确做法是规则存数据库,代码只做通用判定引擎。

public class AlertRuleEngine { // 规则示例:{"ruleType":"heartRate","operator":">","threshold":120,"durationMin":5} public boolean evaluate(HealthRecord record, AlertRule rule) { boolean matched = false; switch (rule.getRuleType()) { case "heartRate": matched = compare(record.getHeartRate(), rule.getOperator(), rule.getThreshold()); break; case "bloodOxygen": matched = compare(record.getBloodOxygen(), rule.getOperator(), rule.getThreshold()); break; default: break; } if (!matched) { return false; } // 规则要求持续N分钟才告警,用Redis记录首次命中时间 String counterKey = "alert:continue:" + record.getElderId() + ":" + rule.getId(); long firstHitTime = redisTemplate.opsForValue().increment(counterKey, 1); if (firstHitTime == 1) { redisTemplate.expire(counterKey, Duration.ofMinutes(rule.getDurationMin())); return false; } // 达到持续时间阈值才触发告警 if (firstHitTime >= rule.getDurationMin()) { redisTemplate.delete(counterKey); return true; } return false; } }

这段逻辑解决两个问题。第一,阈值和比较算子都从规则表读取,运营人员可以在后台调整“心率超过多少算异常”,不用改代码。第二,用 Redis 的increment做持续异常判定,要求异常状态连续维持 N 分钟才告警,避免老人起身倒杯水心率加快就触发一次短信告警。durationMin参数的设定要花心思,心率异常建议设 5 分钟以上,血氧异常可以设 1 分钟,因为血氧下降往往更紧急。告警触达后的动作可以接短信或语音通知,但通知通道必须做频率控制,这个问题会在第 5 章详细拆解。

4. 从单机构到平台化:多租户、权限与设备接入的 Java 落地写法

4.1 多租户数据隔离:MyBatis-Plus 拦截器还是手动拼条件?

智慧养老平台做到第二阶段一定会遇到多机构问题:一个平台给几十家养老机构用,每个机构的管理员只能看自己机构的老人数据。这个需求最稳妥的做法是使用 MyBatis-Plus 的多租户插件,通过 SQL 拦截器自动在语句后面追加org_id条件。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(); TenantLineHandler handler = new TenantLineHandler() { @Override public Expression getTenantId() { // 从当前登录上下文获取机构ID Long orgId = LoginContext.getCurrentOrgId(); return new LongValue(orgId == null ? 0L : orgId); } @Override public String getTenantIdColumn() { return "org_id"; } @Override public boolean ignoreTable(String tableName) { // 机构表和各机构配置表不参与租户隔离 return tableName.equals("sys_org") || tableName.equals("sys_user") || tableName.equals("sys_rule_config"); } }; tenantInterceptor.setTenantLineHandler(handler); interceptor.addInnerInterceptor(tenantInterceptor); return interceptor; } }

手动拼org_id条件虽然直观,但会有一个致命问题:开发人员只要忘记在某个查询里加条件,就会出现跨机构数据泄露,而且这种 bug 在测试阶段还发现不了,因为单机构测试时数据本来就全。用拦截器的好处是从机制上兜底,所有 SQL 自动带上租户条件。ignoreTable这个配置特别关键——像系统配置表这种全平台共享的数据表如果也被追加了org_id条件,会导致基础配置查不到,异常表现很诡异。我的经验是:新增一张表时,先确认它是机构内数据还是平台级数据,然后在拦截器里明确配置,不要留默认行为。

4.2 权限模型:Sa-Token 做认证,数据权限靠租户隔离兜底

认证授权这块,我推荐 Sa-Token 而不是 Spring Security。原因很实在:养老平台的权限模型不复杂,角色就三种——平台管理员、机构管理员、护理人员,Sa-Token 的注解式鉴权写起来比 Spring Security 省一半代码,学习成本也低。核心就三个注解:@SaCheckLogin要求登录、@SaCheckRole("org_admin")要求角色、@SaCheckPermission("elder:delete")要求权限点。

@RestController @RequestMapping("/api/elder") public class ElderController { @SaCheckPermission("elder:add") @PostMapping("/add") public Result addElder(@RequestBody ElderInfo elder) { // 当前登录用户的机构ID直接注入到实体中 elder.setOrgId(LoginContext.getCurrentOrgId()); elderService.save(elder); return Result.ok(); } }

注意代码里那一行elder.setOrgId(LoginContext.getCurrentOrgId()),这比前端传orgId要安全得多。关键原则是:机构 ID 永远从服务端登录态获取,不要信任前端传入的任何机构字段。前端可以传一个假的orgId,攻击者完全可以绕过 UI 直接调接口,拦截器虽然会追加查询条件,但插入操作如果没有正确 set 机构 ID,数据就会落到别人的租户里。

4.3 设备管理:一机一密与心跳在线状态维护

平台化之后,每家机构都会接入大量设备,设备管理就不能再靠“记一个设备编号”了。设备接入要做成一机一密:每台设备分配独立的clientId和secret,设备上报时带着这两个凭证,服务端校验通过才接受数据。clientId推荐用“机构编码加设备序号”的拼接方式,比如ORG001-DEV00012,这样即使不看数据库也能从日志里快速识别设备归属。Secret 用随机 UUID 生成,存储时做哈希,不要明文入库。

在线状态维护是设备管理里最容易被忽视的部分。养老设备种类多,有些设备只在老人离床时上报一次,有些设备异常时根本不会上报。如果只靠“设备主动上报”判断在线,会出现设备已经坏了三天、平台仍然显示“在线”的尴尬情况。我一般会做一张设备心跳表,设备每次上报数据时顺带更新心跳时间,同时起一个定时任务每隔五分钟扫描一次心跳表,把超过十分钟没有心跳的设备标记为离线。

-- 定时任务执行逻辑: -- 将 last_heartbeat_time 超过 10 分钟(可配置)的设备置为离线 UPDATE device_info SET online_status = 0, offline_time = NOW() WHERE online_status = 1 AND last_heartbeat_time < DATE_SUB(NOW(), INTERVAL 10 MINUTE);

然后对刚变为离线的设备触发一条运维工单。这里要注意状态变更的判断逻辑:只有当设备从“在线”变成“离线”时才触发通知,不能每五分钟扫描一次就通知一次,否则值班人员的手机一分钟能收到几十条短信。这个“状态翻转才告警”的思路在告警模块里也通用,第 5 章会再提到一次。

5. 避坑:智慧养老平台 Java 侧最常见的 5 个“上线翻车”现场

5.1 场景一:健康监测大屏的数据延迟,看起来像“玄学”

现象:大屏页面上的心跳曲线总是延迟十几分钟才更新,但查数据库里数据明明已经写入了。运维怀疑是网络问题,前端怀疑是后端没刷新缓存,排查半天找不到根因。

原因:大屏展示走的是聚合查询,直接查 MySQL 的health_record表做AVG、COUNT和GROUP BY。这张表数据量大,聚合查询响应时间慢,前端配置了 3 秒超时就自动降级成 15 分钟一次轮询。数据其实没问题,是查询链路扛不住实时刷新。

解决:给大屏数据加一层 Redis 缓存,定时任务每 30 秒从 MySQL 聚合一次结果写入 Redis,大屏接口只从 Redis 读,单接口响应时间从 2 秒降到 10 毫秒以内。Redis 里的聚合结果设置 1 分钟过期,即使定时任务挂掉,大屏也能显示最近一分钟的数据。顺便说一句,这个优化思路同样适用于告警统计和机构报表,凡是要展示趋势的页面,都优先考虑“预聚合 + 缓存”而不是实时查库。

5.2 场景二:告警风暴把短信通道打爆,运营被拉黑

现象:某天凌晨一台设备异常,触发了某个老人的心率告警规则,但短信通道在一个小时内发出了一百多条告警短信,直接被短信服务商限流。第二天运营投诉,家属也被打扰得不耐烦。

原因:告警触发的通知环节没有做节流。规则引擎里虽然做了持续 5 分钟判定,但判定通过后每次收到新的健康数据都会重复触发通知逻辑,没有加“同一告警规则同一老人 N 小时内只通知一次”的限制。

解决:告警记录表加一个去重维度,以elder_id + rule_id + DATE(created_at)做唯一约束,同一天内同一老人同一规则只产生一条通知。更合理的方式是为每条告警生成一个告警事件号,通知发送前先检查这个事件是否已通知过。我一般还会在通知逻辑里加一个全局阈值:同一机构一小时内最多发出 10 条短信,超过阈值自动切换成应用内推送,等运营人员上班后再处理剩余告警。要记住,告警系统的价值是让合适的人被及时打扰一次,不是让所有人被反复轰炸。

5.3 场景三:健康数据落库后时间全部差八小时

现象:机构反馈老人在凌晨测的血糖,平台显示时间却是前一天下午。设备上报的数据时间看起来全部偏移。

原因:MySQL 连接串里没配置时区参数,Java 服务默认使用 JVM 所在时区,而 MySQL 服务器的time_zone是 CST。具体表现是 JDBC 驱动将LocalDateTime转成 SQL 时间戳时,和服务器时区做了一次隐式的加减,导致时间偏移八小时。

解决:在数据库连接串里显式指定serverTimezone=Asia/Shanghai。同时建议在项目里做一条硬性约束:Java 代码里所有时间字段统一用LocalDateTime,不要混用Date和Timestamp。实体类字段类型不统一会在序列化和反序列化时出现字典序不一致的问题,排查起来比时区问题还要头疼。

5.4 场景四:机构管理员能查到别的机构的老人数据

现象:客户反馈 A 机构的护理人员,通过修改请求参数就能看到 B 机构的老人列表。还好是内部人员发现的,没有造成实际的投诉。

原因:开发人员在某个列表查询接口里,没有使用上一章说的多租户拦截器。因为ignoreTable配置里漏掉了一张业务表,或者某个接口用了自定义 SQL 但没有走 MyBatis-Plus 的拦截器链路,导致 SQL 里没有自动追加org_id条件。

解决:第一,检查TenantLineInnerInterceptor的ignoreTable配置,只忽略真正的平台级配置表。第二,用自定义 SQL 时必须手动加org_id条件,不要依赖拦截器。第三,上线前做一次数据越权测试:用低权限账号走一遍所有接口,重点盯列表查询和详情查询。这个测试脚本值得沉淀成自动化用例,因为权限 bug 属于“上线前不炸、上线后炸”的类型,光靠人工回归很难每次覆盖全。

5.5 场景五:Java 启动失败,报错提示看不懂直接慌

现象:项目部署到服务器上,nohup java -jar启动后进程秒退,查看日志看到一串 ClassNotFoundException 或 BeanCreationException。新接手项目的同事第一反应是“代码有问题”,但本地明明跑得好好的。

原因:最常见的情况是环境差异。服务器上的 JDK 版本比本地低,或者依赖包在打包时没打全。Spring Boot 项目里 spring-boot-maven-plugin 没配置的话,打出来的 jar 是普通 jar 而不是 fat jar,放到服务器上就会报“没有主清单属性”。

解决:pom.xml里显式配置spring-boot-maven-plugin的repackagegoal,确保打出来的是可执行 fat jar。同时建议在启动脚本里加一行java -version先验证服务器 JDK 版本,再跑主程序。排查启动失败不要从头到尾读日志,直接搜Caused by,Java 启动异常链里第一个Caused by就是真正的根因。

6. 一个高频接口的性能优化实例:健康数据入库从“能跑”到“扛住”

健康数据上报接口是整个平台最核心的高频接口,一个中型机构一天能产生几十万条记录。第一版实现按单条插入数据库,压测到每秒 20 条请求时,数据库连接池就满了。这里给出一个行之有效的优化路径:合并写入 + 批量提交。

先看优化前的代码。

public void saveRecord(HealthRecord record) { this.baseMapper.insert(record); }

单条插入在低并发下没什么问题,但健康设备的上报节奏往往是“齐射式”的——上百台设备在同一秒上报,数据库同一秒要处理上百次插入,每一条都要走一次 SQL 解析、事务提交、日志写入。优化方向是把“一次一条”改成“攒一批,一批一条 SQL”。

public void saveRecords(List<HealthRecord> records) { if (CollectionUtils.isEmpty(records)) { return; } // 分批插入,单批控制在 500 条以内 int batchSize = 500; for (int i = 0; i < records.size(); i += batchSize) { int end = Math.min(i + batchSize, records.size()); List<HealthRecord> batch = records.subList(i, end); this.baseMapper.insertBatchSomeColumn(batch); } }

使用 MyBatis-Plus 内置的insertBatchSomeColumn,一条 SQL 插入几百行数据,插入效率提升显著。但要注意这个方法不是默认提供的,需要在自定义的 Mapper 里继承InsertBatchSomeColumn注入。批大小选 500 有个平衡考量:批太大,单条 SQL 的执行时间和内存占用都会增加;批太小,批量插入的优势又体现不出来。实际压测中,500 在 MySQL 默认配置下是稳定区间。

这只是“入库侧”的优化,接口侧还要做削峰。设备上报接口接收到数据后先写入一个内存队列或 Redis 列表,由单独的后台任务定时拉取批量落库。这样即使瞬间来了一千条数据,接口本身也只做了内存写入,响应时间稳定在几毫秒。这个思路本质是把同步写入变成异步写,代价是数据最终一致。健康监测场景下,秒级延迟完全可接受,但订单或支付类场景不能照搬。

我第一次做智慧养老项目时,把这套优化放在了上线后的第二个星期才做,原因是一个机构反馈“下午三点的大屏卡了五分钟”。后来我养成了一个习惯:凡是设备上报类接口,第一版就按批量写入设计,绝不写单条插入“先跑起来再说”。数据库连接池被打满这种事,生产环境第一次出现就够你喝一壶的。用的是数据流最密集的接口,验证的却是系统架构的弹性——健康数据这一条链路扛住了,后面接再多的设备类型心里都不慌。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询