简介一套基于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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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 时间戳时和服务器时区做了一次隐式的加减导致时间偏移八小时。解决在数据库连接串里显式指定serverTimezoneAsia/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 byJava 启动异常链里第一个Caused by就是真正的根因。6. 一个高频接口的性能优化实例健康数据入库从“能跑”到“扛住”健康数据上报接口是整个平台最核心的高频接口一个中型机构一天能产生几十万条记录。第一版实现按单条插入数据库压测到每秒 20 条请求时数据库连接池就满了。这里给出一个行之有效的优化路径合并写入 批量提交。先看优化前的代码。public void saveRecord(HealthRecord record) { this.baseMapper.insert(record); }单条插入在低并发下没什么问题但健康设备的上报节奏往往是“齐射式”的——上百台设备在同一秒上报数据库同一秒要处理上百次插入每一条都要走一次 SQL 解析、事务提交、日志写入。优化方向是把“一次一条”改成“攒一批一批一条 SQL”。public void saveRecords(ListHealthRecord 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()); ListHealthRecord batch records.subList(i, end); this.baseMapper.insertBatchSomeColumn(batch); } }使用 MyBatis-Plus 内置的insertBatchSomeColumn一条 SQL 插入几百行数据插入效率提升显著。但要注意这个方法不是默认提供的需要在自定义的 Mapper 里继承InsertBatchSomeColumn注入。批大小选 500 有个平衡考量批太大单条 SQL 的执行时间和内存占用都会增加批太小批量插入的优势又体现不出来。实际压测中500 在 MySQL 默认配置下是稳定区间。这只是“入库侧”的优化接口侧还要做削峰。设备上报接口接收到数据后先写入一个内存队列或 Redis 列表由单独的后台任务定时拉取批量落库。这样即使瞬间来了一千条数据接口本身也只做了内存写入响应时间稳定在几毫秒。这个思路本质是把同步写入变成异步写代价是数据最终一致。健康监测场景下秒级延迟完全可接受但订单或支付类场景不能照搬。我第一次做智慧养老项目时把这套优化放在了上线后的第二个星期才做原因是一个机构反馈“下午三点的大屏卡了五分钟”。后来我养成了一个习惯凡是设备上报类接口第一版就按批量写入设计绝不写单条插入“先跑起来再说”。数据库连接池被打满这种事生产环境第一次出现就够你喝一壶的。用的是数据流最密集的接口验证的却是系统架构的弹性——健康数据这一条链路扛住了后面接再多的设备类型心里都不慌。希望帮到你。本文还有配套的精品资源点击获取