news 2026/9/12 21:50:56

智慧养老平台Java实战:规则引擎+Redis报警去重全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智慧养老平台Java实战:规则引擎+Redis报警去重全解析

简介:基于SpringBoot的智慧养老平台Java源码,面向计算机、电子信息等专业学生,适合毕业设计、课程设计及期末大作业。项目采用B/S架构与MVC模式,整合SpringBoot、MyBatis、MySQL、Vue等技术栈,涵盖前台展示与后台管理功能,可帮助学习者快速理解JavaWeb项目整体结构。压缩包共950个文件,包含195个Java源文件、164个JavaScript脚本、若干Vue组件与HTML页面,以及SQL/XML映射等配套资源,包体17.83MB,便于本地部署调试。已有335人学习下载,源码经过严格测试,解压后可按说明运行,遇到问题可与博主沟通。通过阅读源码可掌握前后端分离开发、接口调用与数据库交互的完整实现思路,是一份可直接运行的实践项目。

1. 智慧养老平台代码的本质是数据驱动的规则引擎

搜“智慧养老平台代码”的人,多半是想找一套能直接跑的Java项目。但这类系统真正难的从来不是权限管理和增删改查,而是如何把老人健康数据的异常判断、报警生成和护工调度串成一条有业务含义的链路。我见过不少开源模块,表面都是Spring Boot + MySQL + Redis + Vue,实际拉开差距的是规则怎么定义:心率超过160持续3分钟才报警,还是单次超标就报警;报警去重靠数据库还是Redis;工单是不是能自动流转。养老平台的核心不是界面,是一套由实时数据驱动、能快速调参的规则引擎。

本文按一个Java后端工程师最常走的落地路径,从ER建模到Spring Boot接口实现,再到Redis报警去重和调度,最后给一组联调验证技巧。代码可以直接抄改,涉及的高频坑点会单独标注。适合正在做物联网后端、智慧社区项目,或准备Java面试时需要“系统设计”素材的人。

2. 搭建Java项目的数据骨架:elder、vital_record与task_order的MySQL建模

2.1 智慧养老平台需要哪几张核心表

先划边界。一套最小可运行的Java智慧养老平台代码,至少需要五类数据:老人档案表(elder)、护工表(caregiver)、设备绑定表(device_binding)、设备采集记录表(vital_record)、任务工单表(task_order)。有的项目会拆机构、楼层、床位,但最小业务闭环就这五张,再多就是给权限和报表做扩展。

数据库建议MySQL 8.0,引擎InnoDB,字符集utf8mb4。老人档案主键用雪花ID,不要用自增。原因是设备上报、工单引用、跨库分表时,雪花ID能全局唯一,且不暴露业务量。设备采集记录表用自增主键,因为它是纯流水,没有跨表引用需求。

CREATE TABLE `elder` ( `id` BIGINT NOT NULL COMMENT '雪花ID', `name` VARCHAR(32) NOT NULL COMMENT '老人姓名', `age` TINYINT DEFAULT NULL, `room_no` VARCHAR(32) DEFAULT NULL COMMENT '房间号', `health_level` TINYINT DEFAULT 2 COMMENT '1低风险 2中风险 3高风险', `contact_phone` VARCHAR(20) DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '1在住 0退住', PRIMARY KEY (`id`), KEY `idx_room` (`room_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `vital_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `elder_id` BIGINT NOT NULL COMMENT '老人ID', `type` VARCHAR(16) NOT NULL COMMENT 'heart_rate/blood_pressure/spo2', `value` VARCHAR(32) NOT NULL COMMENT '读数,血压格式120/80', `collected_at` DATETIME NOT NULL COMMENT '设备采集时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_elder_time` (`elder_id`, `collected_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:value字段故意用VARCHAR。因为血压是“120/80”复合值,心率是整数,血氧是小数,统一字符串可以降低接口校验成本。但这样查询时要拆字符串,比如统计收缩压大于180的记录,就要用SUBSTRING_INDEX(value, '/', 1)。这也是Java后端查MySQL搜索语句时最常见的做法之一。如果嫌字符串拆分麻烦,可以改成value_minvalue_max两个数值列,写入时前端解析好。我一般更倾向数值列方案,除非设备协议已经固定了字符串拼装,不能改。

vital_record必须建立(elder_id, collected_at)联合索引。设备采集每分钟一条,一个老人一天1440条,100个老人一个月就是430万条流水。没有这个索引,任何按老人和时间范围查最近记录或统计趋势的SQL都会全表扫描,慢是必然的。

2.2 ER图怎么画才不会被老手挑错

这五张表的关系是:eldercaregiver多对多,通过一张elder_caregiver关系表关联,一位老人可以被多个护工监护,一个护工也负责多位老人。device_bindingelder多对一,一台设备绑定一位老人,但一位老人可能有手环、血压计等多台设备。vital_recordelder多对一。task_order分别和eldercaregiver多对一。

绘制ER图时,常见错误是把vital_recorddevice_binding直接连接。正确的设计是设备先绑定老人,采集数据只认老人ID,不认设备ID。这样换设备时不影响历史数据连续性。另外elder_caregiver关系表通常要带relation_type字段,区分“主责护工”和“协助护工”。报警分派时优先给主责护工。

查询某位老人的最新心率记录,面试里经常让手写SQL,实际业务也有这个需求:

SELECT e.name, vr.type, vr.value, vr.collected_at FROM elder e LEFT JOIN vital_record vr ON vr.elder_id = e.id WHERE e.id = 123 AND vr.type = 'heart_rate' AND vr.collected_at = ( SELECT MAX(collected_at) FROM vital_record WHERE elder_id = 123 AND type = 'heart_rate' );

这段SQL隐含一个坑:子查询里如果漏掉type = 'heart_rate',而最新一条恰好是血压记录,主查询的心率数据就查不到,返回空行。很多Java项目在联调时遇到“设备明明上报了,接口查不到”的怪问题,根源就在这类SQL漏条件。所以写关联子查询时,外层条件和子查询的过滤条件要一一对应,这是判断一个后端熟不熟的细节标准。

2.3 工单表状态机设计

task_orderstatus建议用TINYINT:0待接单,1进行中,2已完成,3已取消,4已超时。不要用字符串,状态流转要收拢到Java枚举统一校验,禁止在业务代码里直接UPDATE task_order SET status = 2绕过规则。工单表还需要source_type字段,标记是设备报警自动生成、巡检生成还是手动创建,否则月底统计“报警响应及时率”时,口径对不上。

CREATE TABLE `task_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL, `elder_id` BIGINT NOT NULL, `caregiver_id` BIGINT DEFAULT NULL, `type` TINYINT NOT NULL COMMENT '1巡检 2报警 3用药 4护理', `status` TINYINT NOT NULL DEFAULT 0, `source_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1设备报警 2人工创建', `remark` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `finish_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

order_no生成规则常见的是“前缀+yyyyMMddHHmmss+elder_id+三位随机数”。在Java里不要用UUID全量做订单号,太长且没法排序。即使拼接出的序号可能在并发下重复,有唯一索引兜底就已经足够了,重试一次就行。

3. Spring Boot实现设备上报与健康规则判断的代码路径

3.1 上报接口怎么设计才不堵

设备上报是高频写接口,性能目标是单个接口响应时间不超过100ms。常见做法是HTTP POST JSON,鉴权放在Header的token,参数放Body。Controller只做基础校验,业务处理走异步,不能让设备端一直等到报警判断和工单生成全部完成。

@RestController @RequestMapping("/api/vitals") public class VitalRecordController { @Resource private VitalRecordService vitalRecordService; @PostMapping("/report") public Result<String> report(@RequestBody VitalReportRequest request) { if (request.getElderId() == null || !StringUtils.hasText(request.getType()) || !StringUtils.hasText(request.getValue())) { return Result.error("参数不完整"); } // 异步处理落库、阈值判断、报警生成 vitalRecordService.asyncHandle(request); return Result.success("ok"); } }

逻辑说明:asyncHandle内部建议用Spring@Async标注,落到独立线程池执行。设备端收到ok就认为上报成功,真正业务逻辑在后台跑。这里有一个容易踩的点:@Async在类内部调用不会生效,必须从另一个Bean注入才能触发代理。所以不要在VitalRecordController里直接调本类的asyncHandle方法。

请求参数类里type字段建议用枚举名,value用字符串。这样造数据、排错都方便。比如{"elderId":1, "type":"heart_rate", "value":"185"},一眼能看出是心率185。

3.2 健康阈值判断:不要堆魔法数字

很多智慧养老平台代码里的健康规则写得很粗糙,比如在Service里写十几行if (hr > 160)。临时跑通没问题,但业务人员过几天就会提:“普通老人160报警,卧床老人140就要报。”这种需求如果靠改代码发版,周期太长。成熟的方案是把规则变成配置。

先用一个简单的Java类包装阈值:

@Component public class HealthRuleEngine { @Value("${vital.threshold.heart-rate:160}") private int defaultHeartRateLimit; @Value("${vital.threshold.systolic:180}") private int defaultSystolicLimit; public boolean judge(String type, String value, ElderProfile profile) { if ("heart_rate".equals(type)) { int hr = Integer.parseInt(value); int limit = profile.getHealthLevel() >= 3 ? defaultHeartRateLimit - 20 : defaultHeartRateLimit; return hr >= limit; } if ("blood_pressure".equals(type)) { String[] parts = value.split("/"); int systolic = Integer.parseInt(parts[0]); return systolic >= defaultSystolicLimit; } return false; } }

参数说明:@Value冒号后面的数字是默认值,可以从application.yml覆盖。高风险老人心率阈值比默认低20,这是一种简单粗暴的差异化策略。生产环境建议用Nacos或Apollo配置中心,把规则做成JSON动态下发,格式类似于:[{"type":"heart_rate","min":40,"max":160,"duration":3}]。这样机构可以自定义,平台代码不用发版。

这里也正好回应“lambda函数 java”和“java 八股文”里的知识点:当规则多了之后,可以给每个规则定义Predicate<VitalRecord>,用List收集,再用stream().anyMatch()执行判断。比一长串if-else可读性强,也方便测试覆盖。

3.3 高频采集记录的批量写入

设备采集数据一条条insert,MySQL扛不住。假设1000台设备每10秒上报一次,每秒就是100条写请求。常见做法是业务层攒批,达到100条或500ms间隔就批量写入。用JdbcTemplatebatchUpdate比MyBatis的foreach效率更高,它底层走JDBC的batch。

@Repository public class VitalRecordBatchDao { @Resource private JdbcTemplate jdbcTemplate; public void batchInsert(List<VitalRecord> records) { jdbcTemplate.batchUpdate( "INSERT INTO vital_record(elder_id, type, value, collected_at) VALUES(?,?,?,?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { VitalRecord r = records.get(i); ps.setLong(1, r.getElderId()); ps.setString(2, r.getType()); ps.setString(3, r.getValue()); ps.setObject(4, r.getCollectedAt()); } @Override public int getBatchSize() { return records.size(); } } ); } }

参数说明:setValues在每一批次中调用,records.get(i)为当前要写入的记录。setObject(4, ...)可以写入java.time.LocalDateTime,但MySQL驱动要8.0以上,否则会报不支持的类型。batchSize建议压在200以内,太大时会超过MySQLmax_allowed_packet限制。

攒批组件可以用LinkedBlockingQueue做本地缓冲,后台线程每500ms拉取一次。要注意:服务重启时会丢内存里没写完的数据,所以不能对100%可靠性有要求。如果设备数据不能丢,就上Kafka或RocketMQ,平台代码里用Spring的@KafkaListener消费,再做批量落库。

3.4 最近一条记录怎么查最快

养老平台界面上经常要展示“老人的最新体征”。高频接口直接查表的话,最怕遇到“上一个最新值比当前最新值还大”的情况。更合理的做法是把老人的最新体征存到一个单独的小表elder_latest_vital,或者用Redis hash存。推荐Redis,天然带过期策略,而且读性能远高于MySQL。

-- 查询最近一条心率记录的备用方案 SELECT id, elder_id, type, value, collected_at FROM vital_record WHERE elder_id = 123 AND type = 'heart_rate' ORDER BY collected_at DESC LIMIT 1;

这个SQL必须配合type字段一起走索引。如果只加(elder_id, collected_at)索引,type条件会让MySQL先按时间倒序扫,再过滤类型,当某个老人采集特别频繁时,会有几万行排序。所以我建议把联合索引建为(elder_id, type, collected_at),这样一次命中。

4. Redis与定时任务协作的报警去重和任务分派

4.1 用Redis计数做连续异常报警去重

老人心率一次185不能立刻报警,有可能是设备接触不良或老人活动后的瞬时值。常规策略是“连续N次异常才触发报警”。这里就会用到StringRedisTemplate.opsForValue().increment()

@Service public class AlarmDeduplicator { @Resource private StringRedisTemplate stringRedisTemplate; public boolean reachThreshold(Long elderId, String type, int threshold) { String key = "alarm:count:" + elderId + ":" + type; Long count = stringRedisTemplate.opsForValue().increment(key); if (count != null && count == 1L) { // 首次计数,设置过期时间,避免异常状态一直累加 stringRedisTemplate.expire(key, Duration.ofSeconds(60)); } return count != null && count >= threshold; } }

逻辑说明:increment()在没有key时会先创建并返回1,此时马上设置60秒过期。如果老人持续异常,每分钟上报一次,计数会不断累加,达到3就返回true,触发报警。如果中途恢复正常,60秒后key自动消失,下次再异常则重新计数。这比查数据库统计连续多少次要快得多,也省SQL。

热词里提到“java使用redistemplate将redis的数减一”,其实反向场景也常见。比如报警处理后想清除计数,就用delete(key)。注意increment()返回的是Long,如果Redis里这个key存的不是整数,比如被人为写成了字符串abc,就会抛出“value is not an integer or out of range”异常。排查思路是先redis-cli TYPE key看类型,再GET key看内容。这个错误在redisTemplate使用中排前三。

4.2 定时扫描未处理报警:线程等待的正确姿势

报警落库后,需要定时任务扫描未被分派的报警,给护工推送通知。单机下用Spring@Scheduled足够。分布式部署时,要让每个任务只在一个节点跑,常见方案是Redis分布式锁或ShedLock

@Component public class AlarmScheduleTask { @Resource private AlarmRecordMapper alarmRecordMapper; @Resource private TaskOrderService taskOrderService; private final ExecutorService notifyExecutor = Executors.newFixedThreadPool(8); @Scheduled(fixedDelay = 5000) public void scanUnhandledAlarm() { List<AlarmRecord> unhandled = alarmRecordMapper.findUnhandled(100); if (unhandled.isEmpty()) { return; } // 并发推送,减少总耗时 List<Future<?>> futures = unhandled.stream() .map(alarm -> notifyExecutor.submit(() -> taskOrderService.createAndNotify(alarm))) .collect(Collectors.toList()); for (Future<?> future : futures) { try { future.get(3, TimeUnit.SECONDS); } catch (Exception e) { Thread.currentThread().interrupt(); log.error("等待报警处理线程被中断", e); } } } }

代码说明:fixedDelay = 5000表示上一次执行完成后间隔5秒再跑下一次,不会出现任务重叠。future.get(3, TimeUnit.SECONDS)等待每个线程完成,最多等3秒,避免单个报警推送卡死导致整个调度卡住。Java面试里常问“怎么让多线程都完成再继续”,CountDownLatchFuture.getCompletableFuture.allOf是三个方向,这里用的是Future方式,最直接也最容易控制超时。

4.3 护工抢单不能先查再改

报警生成并通过推送发给护工后,护工端会显示待接单列表。多个护工同时抢一个单时,如果代码写成“先查询订单状态,再update”,就会出现两个人都看到“待接单”,然后都去更新的问题。最稳妥的方案是用SQL原子更新。

@Update("UPDATE task_order SET caregiver_id = #{caregiverId}, status = 1, " + "finish_time = NULL WHERE id = #{orderId} AND status = 0") int grabOrder(@Param("orderId") Long orderId, @Param("caregiverId") Long caregiverId);

这段SQL的核心是AND status = 0。数据库行锁会保证只有一个事务能让status从0变成1,第二个更新返回影响行数为0,接口层拿到结果就能提示“手慢了,订单已被接走”。比先SELECTUPDATE少了临界区,也不需要额外引入数据库悲观锁。

这里有个扩展点:抢单成功后,如果要给其他护工发“订单已接”的广播,可以把这条消息推到Redis的pub/sub或者WebSocket通道。Java里用SimpMessagingTemplate做实时消息推送,代码逻辑不复杂,核心仍然是抢单的原子性。

4.4 报警状态流转的字段设计

报警表alarm_record建议增加notify_statusprocess_status两个字段。notify_status表示是否已推送给护工,process_status表示报警是否已闭环。不要用一个大状态字段同时表达推送和处置,否则查“已通知但未处理”的报警会写出纠结的SQL。

CREATE TABLE `alarm_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `elder_id` BIGINT NOT NULL, `alarm_type` VARCHAR(16) NOT NULL COMMENT 'heart_rate/blood_pressure', `alarm_value` VARCHAR(32) NOT NULL, `count_value` TINYINT NOT NULL DEFAULT 1 COMMENT '连续异常次数', `notify_status` TINYINT DEFAULT 0 COMMENT '0未通知 1已通知', `process_status` TINYINT DEFAULT 0 COMMENT '0未处理 1处理中 2已完成', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_notify_status` (`notify_status`, `process_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

定时任务扫描的是notify_status = 0的数据。等推送结果返回成功,再update成1。process_status由护工在App上关闭工单时更新。这样状态分离后,就算推送失败,也能根据create_time老化重试,不会影响整个业务流。

5. 智慧养老平台代码的联调验证:模拟上报、去重校验与Redis排错

5.1 用一条命令模拟连续异常上报

本地联调最快的方式是curl循环模拟设备,不需要等真实手环。脚本向/api/vitals/report连续发10次心率185的请求,观察报警是否在第三次之后生成。

for i in $(seq 1 10); do curl -s -X POST http://localhost:8080/api/vitals/report \ -H "Content-Type: application/json" \ -d '{"elderId":1,"type":"heart_rate","value":"185"}' echo "" sleep 1 done

发完之后查表:

SELECT id, elder_id, alarm_type, alarm_value, count_value, notify_status FROM alarm_record ORDER BY id DESC LIMIT 5;

预期结果是只有一条报警记录,且count_value = 3。如果看到十条报警,先检查Redis里alarm:count:1:heart_rate这个key是否在第一次上报后被意外删掉,或者increment后的计数被重置。

5.2 验证抢单并发

用两个线程同时执行抢单SQL,Java测并发可以用CountDownLatch模拟同时开始:

CountDownLatch latch = new CountDownLatch(2); executor.submit(() -> { latch.await(); taskOrderService.grab(1L, 101L); }); executor.submit(() -> { latch.await(); taskOrderService.grab(1L, 102L); }); latch.countDown();

执行后查task_orderid=1的记录,caregiver_id只能是其中一个,另一个线程返回0。这个场景验证了AND status = 0的条件更新。

5.3 Redis常见报错怎么快速定位

如果看到“ERR value is not an integer or out of range”,第一反应不是查代码,而是先检查key的类型。用redis-cli执行TYPE alarm:count:1:heart_rate,如果是string且值是数字字符串,那可能是StringRedisTemplateRedisTemplate混用导致的序列化不一致。比如一个地方用StringRedisTemplate写入,另一个地方用带JDK序列化的RedisTemplate读取,字节内容就变成带类型描述符的对象头,在Java里一强转就报错。

解决办法是统一使用StringRedisTemplate操作计数类key,或者把RedisTemplate的valueSerializer明确配成StringRedisSerializer。不要依赖默认的JDK序列化,这是很多Java项目Redis存储混乱的根源。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 21:50:33

Java智慧养老平台代码实战:工单闭环、Redis防抖与高并发优化

简介&#xff1a;一套基于SpringBoot的智慧养老平台完整Java源码&#xff0c;面向计算机、电子信息工程等专业学习者&#xff0c;适合作为毕业设计、课程设计及期末大作业。系统采用B/S架构与MVC模式&#xff0c;整合SpringBoot、Mybatis、Ajax、Vue等技术栈&#xff0c;覆盖前…

作者头像 李华
网站建设 2026/9/12 21:50:30

基于YOLOv8的无人机射频信号频谱图目标检测与部署实践

简介&#xff1a;面向无人机目标检测与频射信号识别任务的图像数据集&#xff0c;适合从事无人机监测、反制或视觉识别研究的开发者、算法工程师及相关专业学生使用。数据集共728个文件&#xff0c;包括364张JPG原始图片和364个配套的Pascal VOC XML格式标注文件&#xff0c;压…

作者头像 李华
网站建设 2026/9/12 21:48:44

2025年AI论文检测工具评测与降AI率指南

1. 论文AI率检测的现状与挑战2025年&#xff0c;学术诚信问题在全球范围内持续受到关注。随着AI写作工具的普及&#xff0c;各大高校和期刊编辑部纷纷引入AI率检测系统&#xff0c;Turnitin、iThenticate等主流查重平台均已升级AI内容识别功能。根据最新统计&#xff0c;超过60…

作者头像 李华
网站建设 2026/9/12 21:47:21

基于Python与PCA的异常检测算法:从原理到工程实践

简介&#xff1a;这是一份基于Python与PCA的异常检测算法设计与实现资源&#xff0c;面向具备一定Python基础、希望掌握数据降维与异常检测技术的学习者&#xff0c;也适合需要在真实数据集上快速搭建检测模型的研究者或工程师。压缩包内共5个Python脚本&#xff0c;涵盖基于Nu…

作者头像 李华
网站建设 2026/9/12 21:47:16

机械工程师转型编程:思维碰撞与技术迁移实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华