ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java智慧养老平台实战:Spring Boot+MyBatis-Plus构建告警与权限体系

Java智慧养老平台实战:Spring Boot+MyBatis-Plus构建告警与权限体系 简介一套基于Spring Boot的智慧养老平台完整源码包面向计算机、电子信息工程等专业学习者尤其适合毕业设计、课程设计与期末大作业场景。项目采用B/S架构与MVC模式开发语言为Java技术栈涵盖SpringBoot、Mybatis、Ajax、Vue等运行环境支持Windows/Mac需配置JDK1.8、Maven3.6、MySQL5.7及Tomcat8.0/9.0可在IDEA或Eclipse中直接导入。资源共950个文件压缩包大小17.83MB其中Java源码文件195个、Vue组件65个、JavaScript文件164个并包含HTML页面、CSS样式、图片、XML配置及数据库相关脚本前后端代码完整目录结构清晰。目前已有338人学习下载。源码经过严格测试附有安装、运行批处理脚本可帮助快速部署启动使用中遇到问题可随时与博主沟通适合作为毕业设计项目参考。1. 智慧养老平台代码在解决什么问题一次夜间告警背后的Java系统凌晨一点值班大屏弹出“3号楼2层501床 王奶奶 心率38 已低于阈值50”系统自动给家属端推送了一条语音提醒。这套能在几秒内完成采集、判断、通知的Java智慧养老平台代码不是某个演示项目而是要把老人终端、护工PAD、家属小程序和管理后台串起来的业务系统。它解决的痛点很具体独居老人没人盯着、护工巡房靠腿、告警靠喊。适合正在做养老信息化、社区居家养老服务系统的人也适合想找一个完整Java业务项目练手并真正部署上线的开发者。2. 技术选型与工程骨架Spring Boot MyBatis-Plus 的取舍2.1 为什么是 Spring Boot MyBatis-Plus而不是微服务全家桶接手智慧养老平台这类项目时团队规模通常是三五个后端业务复杂度远没到需要Spring Cloud那套服务注册、配置中心、网关的程度。常见做法是单体应用起步选Spring Boot做主框架MyBatis-Plus做ORMMySQL存业务数据Redis扛缓存和分布式锁。这套组合对养老机构信息化、社区居家养老服务系统来说足够稳招人容易出问题好排查。MyBatis-Plus相比原生MyBatis的优势在CRUD上体现得最直接单表操作不用手写SQL分页插件、逻辑删除、自动填充都是开箱即用。下面是核心pom依赖注意parent版本按你本地的Spring Boot版本对齐不要混用大版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies这里有两个点值得说。第一starter里自带了分页插件需要的拦截器但必须显式声明PaginationInnerInterceptor才会生效否则分页查询返回全表数据这是个容易被忽略的坑。第二Redis在这个阶段只做两件事缓存热点数据和存登录token不要一上来就搞Redisson分布式锁老人档案写入频率很低数据库唯一索引已经能解决绝大多数冲突。2.2 工程目录怎么分单模块 分包别一上来就拆 Maven 多模块很多新手会把项目拆成api、system、common三个module结果改一个字段要连续构建三次。智慧养老平台代码的体量单模块分包完全够用。我一般这样分src/main/java/com/eldercare ├── controller # 接口层只做参数校验和结果包装 ├── service # 业务层接口 impl实现 ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库实体和表结构一一对应 ├── dto # 入参对象和前端契约 ├── vo # 出参对象聚合展示字段 ├── config # 配置类分页插件、Redis、拦截器 ├── common # 统一返回、异常、常量、枚举 └── ElderCareApplication.java对应的application.yml里最需要关注的三个配置是数据源、Redis和Jackson时间格式。时间格式在健康数据里尤其关键设备上报的时间戳通常是毫秒而后端存的是datetime不一致会导致健康曲线整体漂移这个问题后面避坑章节会细讲。spring: datasource: url: jdbc:mysql://localhost:3306/elder_care?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpass redis: host: localhost port: 6379 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case默认就是true但写上能让接手的人一眼知道约定。logic-delete-field配置了全局逻辑删除字段后所有实体类里的deleted属性都会自动走逻辑删除不用每个Mapper方法单独写SQL。2.3 用实体类反推建表 SQL字段设计是平台的根基坊间流传的“MyBatis-Plus根据Java实体类生成建表SQL”并不是内置能力需要借助代码生成器或者自己写DDL。我的习惯是先设计实体类再对着实体写建表语句保证类型、注释、默认值三对齐。老人档案表是平台的根基字段设计马虎不得。Data TableName(elder_info) public class ElderInfo { TableId(type IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String sex; TableField(room_no) private String roomNo; TableField(bed_no) private String bedNo; private String contactPhone; private String healthCardNo; private String status; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }对应的建表SQL要体现三个设计决定。第一id用雪花算法ASSIGN_ID而不是数据库自增这样未来分库分表不用改主键策略。第二deleted字段配了TableLogic之后所有select会默认带deleted0条件但唯一索引要谨慎——逻辑删除的记录还在表里可能挡住新数据的唯一键这是典型的坑。第三createTime和updateTime配合MyBatis-Plus的MetaObjectHandler自动填充不用业务代码里手动set时间。CREATE TABLE elder_info ( id BIGINT NOT NULL COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 姓名, age INT COMMENT 年龄, sex CHAR(1) COMMENT 性别 0-女 1-男, room_no VARCHAR(20) COMMENT 房间号, bed_no VARCHAR(20) COMMENT 床位号, contact_phone VARCHAR(20) COMMENT 紧急联系电话, health_card_no VARCHAR(32) COMMENT 健康档案卡号, status VARCHAR(10) DEFAULT ACTIVE COMMENT 状态, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除标记, PRIMARY KEY (id), UNIQUE KEY uk_health_card_no (health_card_no, deleted) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个很隐蔽的细节唯一索引uk_health_card_no把deleted带进去了因为逻辑删除后同一健康卡号还想再录入时deleted1的记录不会和deleted0的新记录冲突。但如果MyBatis-Plus的逻辑删除值只有0和1删除两条同卡号记录后第二条就插不进去了。真遇到这种需求要么把logic-delete-value改成原主键id要么用单独的注销表别在唯一索引里硬扛。3. 三个核心模块的落码老人档案、健康数据与告警工单3.1 老人档案分页查询与状态变更的接口写法养老平台的管理端第一个页面就是老人档案列表。这里的关键不是CRUD本身而是分页查询的参数封装和状态变更的幂等性。先配置MyBatis-Plus分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }一旦配上分页插件Service里就可以用LambdaQueryWrapper Page轻松完成条件查询。状态变更接口则要注意老人状态从ACTIVE改为LEAVING时必须校验名下没有未处理的告警工单否则会造成“人走了告警还挂着”的脏数据。Service public class ElderInfoServiceImpl implements ElderInfoService { Override public IPageElderInfoVO pageQuery(ElderPageQuery query) { PageElderInfo page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperElderInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), ElderInfo::getName, query.getName()) .eq(StringUtils.hasText(query.getRoomNo()), ElderInfo::getRoomNo, query.getRoomNo()) .orderByDesc(ElderInfo::getCreateTime); return elderInfoMapper.selectPage(page, wrapper).convert(this::toVO); } Override Transactional(rollbackFor Exception.class) public void changeStatus(Long elderId, String targetStatus) { ElderInfo elderInfo elderInfoMapper.selectById(elderId); if (elderInfo null) { throw new BizException(老人档案不存在); } if (LEAVING.equals(targetStatus)) { Long alertCount alertMapper.selectCount( new LambdaQueryWrapperAlertRecord() .eq(AlertRecord::getElderId, elderId) .in(AlertRecord::getStatus, OPEN, PROCESSING)); if (alertCount 0) { throw new BizException(存在未处理的告警不能办理退住); } } elderInfo.setStatus(targetStatus); elderInfoMapper.updateById(elderInfo); } }这段代码里有两个高频坑。第一Service方法必须加Transactional因为状态变更涉及“读档案 查告警 更新状态”三步任何一步失败都应该回滚。第二query里如果传入的pageSize超过100PaginationInnerInterceptor的setMaxLimit会直接报错这是故意设计的防止有人拿大页码拖垮数据库——管理端的翻页需求做到100条一页封顶足够。3.2 健康数据接入上报接口的幂等与阈值判断健康设备手环、血压计、睡眠监测垫一般通过HTTP或MQTT上报数据。小规模项目用HTTP最简单设备端POST一个JSON后端着库。真正要小心的是设备断网重连后补报历史数据同一个时间点的数据可能上报两次所以接口必须做幂等。RestController RequestMapping(/api/v1/device) public class HealthRecordController { PostMapping(/report) public ResultVoid report(RequestBody HealthReportDTO dto) { healthRecordService.report(dto); return Result.success(); } }Service public class HealthRecordServiceImpl implements HealthRecordService { Override Transactional(rollbackFor Exception.class) public void report(HealthReportDTO dto) { Long exists healthRecordMapper.selectCount( new LambdaQueryWrapperHealthRecord() .eq(HealthRecord::getDeviceId, dto.getDeviceId()) .eq(HealthRecord::getReportTime, dto.getReportTime())); if (exists 0) { return; // 重复上报直接丢弃 } HealthRecord record new HealthRecord(); record.setElderId(dto.getElderId()); record.setDeviceId(dto.getDeviceId()); record.setHeartRate(dto.getHeartRate()); record.setSpo2(dto.getSpo2()); record.setReportTime(dto.getReportTime()); healthRecordMapper.insert(record); // 阈值判断心率低于50或高于130血氧低于90触发告警 checkThreshold(record); } private void checkThreshold(HealthRecord record) { boolean abnormal record.getHeartRate() 50 || record.getHeartRate() 130 || record.getSpo2() 90; if (abnormal) { alertService.createAlert(record); } } }幂等逻辑有两个细节必须同步做。第一代码层面的selectCount只是第一道防线数据库层面要给device_id report_time建唯一索引否则并发请求同时进来时两条重复数据都能通过查询。第二reportTime用的是设备本地时间不排除设备时钟不准所以唯一索引只能防重复不能替代告警去重——同一指标持续异常时应该按策略合并告警而不是每一条上报都生成新工单。注意这里的Transactional加在report方法上作用是把“插入健康数据 触发告警”绑成一个事务避免出现“数据存了但告警没生成”的中间状态。数据一致性问题在设备接入场景尤其常见面试时说的“分布式事务”在这里根本用不上一个本地事务加唯一索引就能解决。3.3 告警工单状态流转不能写成 if-else告警工单是智慧养老平台代码里最需要设计耐心的模块。一个工单从生成到关闭要经过OPEN待处理→ PROCESSING处理中→ RESOLVED已解决→ CLOSED已关闭这几个状态还可能从PROCESSING退回REOPENED。如果直接在Service里写一堆if-else判断当前状态能跳到哪个状态改一次需求就要动一处核心逻辑。常见的做法是把状态流转规则放在枚举里每个枚举持有允许到达的下一状态集合public enum AlertStatus { OPEN(Arrays.asList(PROCESSING, CLOSED)), PROCESSING(Arrays.asList(RESOLVED, REOPENED)), RESOLVED(Arrays.asList(CLOSED, REOPENED)), REOPENED(Arrays.asList(PROCESSING, RESOLVED)), CLOSED(Collections.emptyList()); private final ListString allowedTransitions; AlertStatus(ListString allowedTransitions) { this.allowedTransitions allowedTransitions; } public boolean canTransitTo(String target) { return allowedTransitions.contains(target); } }Service里每次变更状态都先做合法性校验Override public void transit(Long alertId, String targetStatus, String operatorId, String remark) { AlertRecord alert alertMapper.selectById(alertId); if (alert null) { throw new BizException(告警工单不存在); } AlertStatus current AlertStatus.valueOf(alert.getStatus()); if (!current.canTransitTo(targetStatus)) { throw new BizException(不允许从 current 流转到 targetStatus); } alert.setStatus(targetStatus); alert.setOperatorId(operatorId); alert.setHandleRemark(remark); alert.setHandleTime(LocalDateTime.now()); alertMapper.updateById(alert); // 记录流转日志后续审计要用 alertFlowMapper.insert(new AlertFlow(alertId, current.name(), targetStatus, operatorId)); }状态机的价值在后期需求变更时最能体现。比如将来要加一个“已误报”状态只需要在枚举里增加一个值和可达状态不用去Service里逐段排查哪里漏了判断。这里提醒一点枚举的name()存库虽然直观但枚举一旦改名历史数据的字符串就对不上了更稳妥的做法是给枚举加一个code字段专门落库。4. 多角色权限与值班闭环行级权限、JWT 鉴权和通知4.1 行级权限家属只能看到自己的老人护工只能看到本楼层智慧养老平台的角色至少有管理员、护工、家属三种。管理员看全部护工看自己负责的楼层或床位家属只能看绑定的老人。这就是行级权限和菜单权限是两码事。网上很多代码把菜单权限做得很重行级权限却漏了结果家属登录后能查到全机构的老人列表直接算安全事故。我一般不用MyBatis-Plus的DataPermissionInterceptor去搞自动SQL改写那套东西对复杂查询会误伤排错成本高。更可控的做法是在Service层传一个“可见范围”参数所有查询都带上。public interface DataScope { ListLong getVisibleElderIds(); }登录时根据角色构造不同的DataScope实现Component public class ElderDataScopeHandler { public ListLong resolveVisibleElderIds(LoginUser user) { if (user.isAdmin()) { return null; // null 表示不过滤 } if (NURSE.equals(user.getRole())) { return nurseMapper.selectElderIdsByFloor(user.getDeptId()); } if (FAMILY.equals(user.getRole())) { return familyBindMapper.selectElderIdsByFamilyId(user.getUserId()); } return Collections.emptyList(); } }查询函数里显式判断Override public IPageElderInfoVO pageQuery(ElderPageQuery query) { PageElderInfo page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperElderInfo wrapper new LambdaQueryWrapper(); ListLong visibleIds dataScopeHandler.resolveVisibleElderIds(loginUserHolder.get()); if (visibleIds ! null) { wrapper.in(ElderInfo::getId, visibleIds); } wrapper.like(StringUtils.hasText(query.getName()), ElderInfo::getName, query.getName()) .orderByDesc(ElderInfo::getCreateTime); return elderInfoMapper.selectPage(page, wrapper).convert(this::toVO); }这里有一个容易被忽视的边界当visibleIds为空集合时wrapper.in(...)生成的SQL是IN ()MySQL会直接报错。所以要么用CollectionUtils.isEmpty判断后返回空页要么在resolveVisibleElderIds里约定“空集合表示无权限直接返回空数据”。行级权限最怕的就是这种小细节翻车调试时看着SQL没什么问题一跑就报语法错误。4.2 登录鉴权轻量 JWT 拦截器够用就好小团队项目不太需要Spring Security那套过滤器链用JWT加一个拦截器就能覆盖日常接口鉴权需求。登录接口校验用户名密码后签发token后续请求在Authorization头里带上由拦截器统一解析。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { throw new AuthException(未登录); } LoginUser loginUser jwtUtils.parseToken(token.replace(Bearer , )); loginUserHolder.set(loginUser); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { loginUserHolder.clear(); } }登录用户用ThreadLocal保存同一线程内所有Service方法都能拿到当前操作人。这里要注意afterCompletion里必须clear否则Tomcat线程池复用时下一个请求会拿到上一个用户的登录态这是我见过最悬疑的bug之一症状是用户A偶尔能看到用户B的数据查半天查不出原因最后发现是ThreadLocal没清理。token过期怎么处理养老平台的护工端一值班就是8小时token有效期设太短会频繁掉线设太长又怕泄露。常见做法是access_token两小时过期Redis里存一个refresh_token七天有效——但这套逻辑会增加不少代码。体量小的话把access_token有效期调成12小时配合Redis里的“强制下线”标记管理员禁用账号时写一个blacklist key就够用了。4.3 告警通知站内信打底短信接口做扩展告警产生后要同时通知值班护工和家属。短信要对接第三方服务商测试环境没有真实通道所以第一版先做站内信同时留好通知接口的扩展点。站内信表结构很简单通知人、标题、内容、关联业务id、是否已读、创建时间。public interface NotifyService { void send(NotifyContext context); void retryUnsent(); }Service public class NotifyServiceImpl implements NotifyService { Override public void send(NotifyContext context) { // 1. 保存站内信状态为 UNREAD // 2. 如果配置了短信通道调用短信服务商接口 // 3. 短信发送失败只记录日志不影响站内信保存 } }短信通道做成接口而不是硬编码是血泪经验换来的项目上线三个月后机构要求换成另一家短信服务商如果当初在Service里直接调SDK光替换就要改十几处。现在只改一个实现类业务代码完全不动。值班大屏的WebSocket推送同理优先考虑用Spring Messaging的SimpMessagingTemplate复杂了再引Netty不要一上来就上重量级中间件。5. 智慧养老平台代码的 5 个典型踩坑现象、原因、解决这些坑大多不会出现在面试八股文里但会在你上线第一周准时来敲门。每条都按现象、原因、解决三步写方便你对照排查。5.1 逻辑删除与唯一索引打架同一健康卡号只能录入一次现象删除一条老人档案后再录入同卡号的新老人数据库报Duplicate entry明明那条记录已经在列表里看不到了。原因MyBatis-Plus的逻辑删除只是把deleted置为1记录还在物理表里。elder_info表上有UNIQUE KEY uk_health_card_no (health_card_no, deleted)删除前是(卡号, 0)删除后变成(卡号, 1)再次插入(卡号, 0)时MySQL检查唯一索引发现(卡号, 0)仍然存在——因为deleted1的那条不参与唯一性判断。解决我把唯一索引调整成(health_card_no, deleted)没有用这解决不了二次删除问题。最后的方案是业务上约定“健康卡号一旦分配就不允许物理删除”真需要作废时走单独的注销记录表elder_info里的逻辑删除只用于常规退住。这个决定反过来让审计逻辑更清晰注销是注销退住是退住两个动作分开记。5.2 健康曲线整体偏移 8 小时时区配置不一致现象设备上报的时间戳是正确的北京时间但管理端健康曲线显示的数据点整体比实际晚了8小时白天的心率峰跑到了凌晨。原因设备端发送的是Unix毫秒时间戳后端用LocalDateTime接收并存储。MySQL连接的serverTimezone没设置时驱动会采用JVM默认时区而服务器操作系统是UTC一来一回差了8小时。设备上报接口和MySQL连接串里的时区配置不一致是这类问题的温床。解决三个地方统一强制为Asia/Shanghai。第一JDBC连接串加serverTimezoneAsia/Shanghai第二Spring Boot的Jackson配置time-zone: Asia/Shanghai第三所有接收时间戳的DTO字段用Long类型接在Service里显式转LocalDateTime不让框架猜。改完后写一个单元测试用固定时间戳验证转换结果防止未来有人动这两个配置。5.3 管理端翻到第几百页卡死深分页查询的代价现象告警记录页从默认的20条一页改成200条一页后翻到第50页时接口耗时超过3秒数据库CPU直接飙高。原因MySQL的LIMIT 10000, 200需要先扫过前10000条再丢弃越到后面越慢。PaginationInnerInterceptor只做了maxLimit限制没有解决深分页的扫描成本。告警记录表本身就是写多读少的流水表数据量上来后问题立刻暴露。解决管理端明确“只能查最近三个月告警”的限制并加上按时间倒序的二级排序让索引能走得更快。更彻底的办法是改游标分页用上一页最后一条记录的ID作为查询条件去掉LIMIT的偏移量。对养老平台这个体量限制时间范围已经够用游标分页通常留给数据报表模块。5.4 设备断网重连后重复告警轰炸值班大屏现象一台睡眠监测垫断网两小时重连后把补报的200条数据全部推上来告警工单瞬间多了30条值班手机响个不停。原因健康数据上报接口做了幂等但阈值判断的逻辑是“每条异常数据都生成一条告警”。补报的历史数据逐条进入checkThreshold每一条都满足异常条件自然每个都触发了告警创建。解决给告警模块加上时间窗口去重——同一个老人同一类型指标在10分钟内只生成一条告警。实现上在AlertRecord表加一个唯一约束或查询条件创建前先查一下该老人最近10分钟有没有同类型的OPEN告警有就只更新告警次数和最后发生时间不新建工单。这个设计顺带解决了持续异常时的告警风暴问题值班人员只用处理一条“心率持续异常”工单而不是收到50条重复提醒。5.5 报表页面跑全表别在循环里查数据库现象大屏的“今日告警趋势”接口加载花了5秒检查发现Service里先查出全部老人再for循环每个老人查告警数量一个查询被放大成N次数据库往返。原因典型的N1查询问题。MyBatis-Plus的单表查询能力容易让人忘记SQL聚合才是报表的正确打开方式。告警表几千条数据时感觉不出来一上万就原形毕露。解决所有统计类接口一律用SQL聚合一次查询返回全部指标。GROUP BY elder_id和DATE_FORMAT(report_time, %Y-%m-%d)解决趋势图COUNT和SUM解决汇总卡片。如果统计接口还要做二次加工查出来放内存里算但绝不能再回表查明细。这条经验后来也成了团队Code Review的规定Service层禁止出现for循环里嵌查询的写法。6. 进阶大屏聚合接口与全链路验证技巧大屏是养老机构的门面但这个接口很容易被做成性能灾难。我现在的写法是一张SQL出聚合结果Redis缓存60秒再用异步刷新兜底。Override public DashboardVO getDashboardData() { String cacheKey dashboard:overview; DashboardVO vo redisTemplate.opsForValue().get(cacheKey); if (vo ! null) { return vo; } DashboardVO result new DashboardVO(); result.setTodayAlertCount(alertMapper.selectTodayAlertCount()); result.setOnlineElderCount(elderMapper.selectOnlineElderCount()); result.setAvgHandleMinutes(alertMapper.selectAvgHandleMinutes()); redisTemplate.opsForValue().set(cacheKey, result, Duration.ofSeconds(60)); return result; }缓存过期后如果有十个用户同时打开大屏回源流量会同时打到数据库上所以我会建议在方法上加Cacheable配合空值缓存或者用Redisson的锁做单飞回源。对这个体量60秒缓存已经能解决绝大多数压力不用过度设计。全链路验证有一套固定的模拟方法。写一个Java的main方法模拟设备上报随机生成心率和血氧数据其中以5%概率生成异常值持续向/report接口打流量。这个脚本留着每次改阈值逻辑都能重跑一遍比手工Postman点半天强得多。验证完数据链路再用家属账号登录确认只能看到自己绑定的老人的数据和告警护工账号只能看到本楼层的——这一步必须放在每次发版前的检查清单里行级权限一旦被改坏用户数据就裸奔了。做智慧养老平台代码这两年我最大的教训是设备接入的幂等、行级权限的边界、状态机的约束这些“看不见的设计”比页面多漂亮重要得多。养老场景里告警一旦漏掉或错报损失的是真实的安全所以宁可多写一行校验也绝不心存侥幸。希望这套方案的思路能帮你少走一段弯路。本文还有配套的精品资源点击获取
返回列表