资讯详情

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

📅 2026/9/12 21:51:30 | 华诺云谱 👁 阅读
智慧养老平台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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明value字段故意用VARCHAR。因为血压是“120/80”复合值心率是整数血氧是小数统一字符串可以降低接口校验成本。但这样查询时要拆字符串比如统计收缩压大于180的记录就要用SUBSTRING_INDEX(value, /, 1)。这也是Java后端查MySQL搜索语句时最常见的做法之一。如果嫌字符串拆分麻烦可以改成value_min和value_max两个数值列写入时前端解析好。我一般更倾向数值列方案除非设备协议已经固定了字符串拼装不能改。vital_record必须建立(elder_id, collected_at)联合索引。设备采集每分钟一条一个老人一天1440条100个老人一个月就是430万条流水。没有这个索引任何按老人和时间范围查最近记录或统计趋势的SQL都会全表扫描慢是必然的。2.2 ER图怎么画才不会被老手挑错这五张表的关系是elder和caregiver多对多通过一张elder_caregiver关系表关联一位老人可以被多个护工监护一个护工也负责多位老人。device_binding和elder多对一一台设备绑定一位老人但一位老人可能有手环、血压计等多台设备。vital_record和elder多对一。task_order分别和elder、caregiver多对一。绘制ER图时常见错误是把vital_record和device_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_order的status建议用TINYINT0待接单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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_no生成规则常见的是“前缀yyyyMMddHHmmsselder_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 ResultString 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内部建议用SpringAsync标注落到独立线程池执行。设备端收到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 八股文”里的知识点当规则多了之后可以给每个规则定义PredicateVitalRecord用List收集再用stream().anyMatch()执行判断。比一长串if-else可读性强也方便测试覆盖。3.3 高频采集记录的批量写入设备采集数据一条条insertMySQL扛不住。假设1000台设备每10秒上报一次每秒就是100条写请求。常见做法是业务层攒批达到100条或500ms间隔就批量写入。用JdbcTemplate的batchUpdate比MyBatis的foreach效率更高它底层走JDBC的batch。Repository public class VitalRecordBatchDao { Resource private JdbcTemplate jdbcTemplate; public void batchInsert(ListVitalRecord 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 定时扫描未处理报警线程等待的正确姿势报警落库后需要定时任务扫描未被分派的报警给护工推送通知。单机下用SpringScheduled足够。分布式部署时要让每个任务只在一个节点跑常见方案是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() { ListAlarmRecord unhandled alarmRecordMapper.findUnhandled(100); if (unhandled.isEmpty()) { return; } // 并发推送减少总耗时 ListFuture? 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面试里常问“怎么让多线程都完成再继续”CountDownLatch、Future.get、CompletableFuture.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接口层拿到结果就能提示“手慢了订单已被接走”。比先SELECT再UPDATE少了临界区也不需要额外引入数据库悲观锁。这里有个扩展点抢单成功后如果要给其他护工发“订单已接”的广播可以把这条消息推到Redis的pub/sub或者WebSocket通道。Java里用SimpMessagingTemplate做实时消息推送代码逻辑不复杂核心仍然是抢单的原子性。4.4 报警状态流转的字段设计报警表alarm_record建议增加notify_status和process_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;定时任务扫描的是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 验证抢单并发用两个线程同时执行抢单SQLJava测并发可以用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_order里id1的记录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且值是数字字符串那可能是StringRedisTemplate和RedisTemplate混用导致的序列化不一致。比如一个地方用StringRedisTemplate写入另一个地方用带JDK序列化的RedisTemplate读取字节内容就变成带类型描述符的对象头在Java里一强转就报错。解决办法是统一使用StringRedisTemplate操作计数类key或者把RedisTemplate的valueSerializer明确配成StringRedisSerializer。不要依赖默认的JDK序列化这是很多Java项目Redis存储混乱的根源。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。