
简介面向老年健康管理场景的Java应用设计源码回应老龄化背景下慢性病与日常健康监护需求适合Java Web开发者学习或快速搭建同类健康管理原型也可作为课程设计或毕业设计的参考。资源包共36个文件压缩后仅120KB其中30个Java源文件承担核心业务逻辑3个XML配置文件负责数据库连接与运行参数设置并包含Git忽略文件、项目说明等目录结构清晰。源码围绕老年用户健康监护实现用户登录、健康数据录入、血压血糖心率分析、个性化健康建议与异常提醒等流程按用户、健康信息、用药、检查等业务模块拆分为Controller、Service、Mapper和实体Bean便于逐层阅读与二次开发。已有260人学习下载对想掌握Java分层开发或理解健康管理需求建模的读者这份源码提供了完整可参考的工程范例与业务模块拆解思路。1. 基于Java平台的老年人健康管理应用它到底解决什么问题一个常见场景家人在外地工作家里的长辈每天要量血压、吃几种药偶尔血糖高了也没人及时知道。手机上的健康App不少但字体小、操作层级深、数据没人帮忙看长辈用两天就放弃了。基于Java平台的老年人健康管理应用正是针对这类需求做的后端设计与源码实现——它把健康档案、体征记录、用药提醒、异常预警这些功能串成一个完整系统让子女端和长辈端都能各取所需。这篇笔记要讲清楚这个标题背后的技术栈该选什么、数据模型怎么建、核心功能怎么实现、实际落地有哪些坑以及做完之后往哪个方向延展性价比最高。适合准备做健康类毕业设计、中小型养老项目或者想快速搭建一个可演示原型的人。2. 技术选型与数据模型Spring Boot MyBatis-Plus 这套组合的取舍理由2.1 框架选型为什么优先选 Spring Boot 而不是其他做老年人健康管理这类业务系统第一诉求不是性能极致而是开发效率、生态成熟度和排错成本。常见做法是用 Spring Boot 2.7 MyBatis-Plus MySQL这套组合能覆盖绝大部分中小型项目的 CRUD、权限、定时任务和报表需求。为什么不用 Spring Cloud单体应用阶段引入微服务治理纯属加重负担健康管理系统的用户量在没有商业化之前单机部署完全够用。为什么不用 PostgreMySQL 在 Windows/Linux 下的运维资料最多学生和中小团队遇到问题检索速度更快。如果团队里有人熟悉 MyBatis 原生写法MyBatis-Plus 提供 BaseMapper 后可以让单表操作不用写 SQL节省大量样板代码。一个容易忽略的点是 Java 版本。JDK 8 仍然是最稳妥的选择不要因为标题里带“Java”就上 JDK 17 或 21。很多老的 Linux 服务器默认环境还是 JDK 8且 Spring Boot 2.7 官方支持周期内对 JDK 8 的兼容性最好。选 JDK 8 Spring Boot 2.7 MyBatis-Plus 3.5是我在这个项目上的推荐组合后续要升级再平滑过渡。2.2 数据库设计核心表结构与字段约束老年人健康管理的数据模型核心要处理好三个实体老人、家庭成员子女/监护人、健康数据记录。我设计时拆成了 8 张表下面这张表格列出最关键的表和字段。表名核心字段关键约束userid, phone, password, real_name, role, create_timerole: 1-老人, 2-家属elderid, user_id, name, age, gender, chronic_disease慢性病用逗号分隔存储health_recordid, elder_id, blood_pressure_high, blood_pressure_low, blood_sugar, heart_rate, record_time数值类型, 允许空medicationid, elder_id, drug_name, dosage, unit, frequency_type, remind_timefrequency_type: 1-每日, 2-每周remind_logid, medication_id, remind_time, is_ackedis_acked: 0-未确认abnormal_alarmid, elder_id, alarm_type, alarm_content, alarm_time, is_readalarm_type: 1-血压, 2-血糖family_bindingid, elder_id, family_id, relation, bind_time唯一约束 elder_id family_iddevice_tokenid, user_id, client_type, token, update_time用于推送设备标识这里有一个重要的设计决策health_record 表不做冗余的 status 字段血压是否偏高通过查询时实时计算。原因是老人的身体指标阈值会变化医生可能调整标准实时计算保证调整阈值后历史数据自动适配。如果入库时直接写死偏高/偏低后面改阈值就要写批量更新脚本很讨厌。elder 表里 chronic_disease 用逗号分隔存储是有意的妥协。虽然这违反了第三范式但对健康管理系统来说慢性病种类是固定枚举不需要针对每种病做聚合查询拆成关联表反而让页面展示多两次 JOIN。如果你的需求是统计某类慢性病患者的占比那就拆表这个看具体报表要求决定。2.3 工程目录结构一个能直接落地的分层方案后端工程我习惯按功能模块分包而不是按技术层分包否则加一个新功能要同时动 controller、service、mapper 三层效率太低。下面是目录结构src/main/java/com/health/ ├── config/ │ ├── MybatisPlusConfig.java │ ├── WebMvcConfig.java │ └── RedisConfig.java ├── controller/ │ ├── ElderController.java │ ├── HealthRecordController.java │ ├── MedicationController.java │ └── AlarmController.java ├── service/ │ ├── impl/ │ ├── HealthRecordService.java │ └── MedicationRemindTask.java ├── mapper/ │ ├── ElderMapper.java │ └── HealthRecordMapper.java ├── entity/ │ ├── Elder.java │ └── HealthRecord.java ├── dto/ │ ├── BindFamilyRequest.java │ └── HealthRecordQuery.java ├── common/ │ ├── Result.java │ ├── BizException.java │ └── GlobalExceptionHandler.java └── util/ ├── JwtUtil.java └── HealthThresholdUtil.javacommon 包里的 Result 统一返回结构必不可少它决定了前端能不能少写一堆 if else。我一般定义成 code message data 三段式code 为 0 时表示成功非 0 时业务错误。GlobalExceptionHandler 要单独写否则业务异常直接抛一堆堆栈给前端体验很糟糕。3. 核心功能模块的实现健康档案、用药提醒、异常预警的代码与参数说明3.1 健康档案模块绑定家属关系与基础信息维护健康档案是整个应用的地基老人要先建档家属绑定后才能查看数据。档案接口的关键代码在绑定关系这一段因为这里面涉及权限归属问题。先看代码RestController RequestMapping(/api/elder) public class ElderController { PostMapping(/bind) public Result bindFamily(RequestBody BindFamilyRequest request) { // 1. 校验当前登录用户角色必须是家属 User currentUser userService.getById(JwtUtil.getUserId()); if (!currentUser.getRole().equals(2)) { return Result.error(只有家属账号才能发起绑定); } // 2. 校验老人账号是否存在 Elder elder elderService.getByUserId(request.getElderUserId()); if (elder null) { return Result.error(老人账号不存在); } // 3. 检查是否已经绑定过 LambdaQueryWrapperFamilyBinding wrapper new LambdaQueryWrapper(); wrapper.eq(FamilyBinding::getElderId, elder.getId()) .eq(FamilyBinding::getFamilyId, currentUser.getId()); if (familyBindingService.count(wrapper) 0) { return Result.error(已绑定该老人请勿重复操作); } // 4. 插入绑定关系 FamilyBinding binding new FamilyBinding(); binding.setElderId(elder.getId()); binding.setFamilyId(currentUser.getId()); binding.setRelation(request.getRelation()); binding.setBindTime(new Date()); familyBindingService.save(binding); return Result.success(绑定成功); } }绑定接口的核心逻辑是“先校验再写入”。第 2 步的 elder.getByUserId 是为了防止家属乱填一个不存在的老人用户 ID第 3 步的去重检查如果没有会调用 save 直接插入导致重复绑定。第 4 步的 relation 字段比如“父子”“母女”“其他”前端下拉选择即可。这里有一个边界要提醒实际项目中一个老人可能被多个家属绑定这是合理的不需要做唯一限制。但前端展示时要按绑定时间排序默认展示最近绑定的家属视角。如果是家庭内部争抢数据的场景可以直接在 family_binding 表加一个 is_main 字段标记主绑定人只有主绑定人有修改档案的权限。3.2 用药提醒模块定时任务与消息触达的实现用药提醒是老年人健康管理里使用频率最高的功能几乎每个老人都需要每天定时服药。实现方案选择 Quartz 还是 Spring 自带 Scheduled我建议用 Spring 自带的原因很简单需求是固定时间的定时提醒不需要 Cron 表达式的动态调度Quartz 在这里属于杀鸡用牛刀。核心任务是每分钟扫描一次 medication 表找到当前时间应该提醒的记录。代码如下Component public class MedicationRemindTask { Autowired private MedicationService medicationService; Autowired private RemindLogService remindLogService; Scheduled(cron 0 * * * * ?) public void scanRemind() { // 1. 查询所有启用的用药记录 ListMedication list medicationService.list( new LambdaQueryWrapperMedication() .eq(Medication::getStatus, 1) ); String nowTime LocalTime.now().format(DateTimeFormatter.ofPattern(HH:mm)); for (Medication medication : list) { // 2. 判断当前时间是否匹配提醒时间 if (nowTime.equals(medication.getRemindTime().toString())) { // 3. 检查这一分钟是否已经提醒过 LocalDate today LocalDate.now(); LambdaQueryWrapperRemindLog logWrapper new LambdaQueryWrapper(); logWrapper.eq(RemindLog::getMedicationId, medication.getId()) .eq(RemindLog::getRemindDate, today) .eq(RemindLog::getRemindTime, nowTime); if (remindLogService.count(logWrapper) 0) { continue; } // 4. 写入提醒日志避免重复提醒 RemindLog log new RemindLog(); log.setMedicationId(medication.getId()); log.setElderId(medication.getElderId()); log.setRemindDate(today); log.setRemindTime(nowTime); log.setIsAcked(0); remindLogService.save(log); // 5. 发送推送这里通过 device_token 表查询老人绑定设备 pushService.sendRemind(medication); } } } }这段代码有两个关键参数Scheduled 的 cron 表达式为0 * * * * ?表示每秒的第 0 秒触发即每分钟执行一次compareTime 用 HH:mm 格式做字符串等值匹配。这个设计允许提醒时间精确到分钟级对用药场景来说已经绰绰有余不需要秒级精度。第 3 步去重检查是整个定时任务不出乱子的核心。如果没有这一步任务每分钟执行一次而某条用药提醒时间是 08:00任务在 08:00:00 扫到一次08:00:01 又扫到一次同一分钟内会推送多次。写入 remind_log 后再检查保证已提醒过的记录被跳过。第 5 步的推送用 device_token 表存设备标识客户端类型支持小程序和 Android具体推送厂商通道不同但公共接口可以抽象成 pushService。3.3 异常指标预警阈值判断与告警生成健康指标预警的价值在于“发现问题不是等老人说而是系统主动报警”。这里我封装了一个 HealthThresholdUtil把判断逻辑集中到工具类里方便后续调整和单元测试。核心代码如下public class HealthThresholdUtil { // 血压阈值mmHg private static final int HIGH_BLOOD_PRESSURE_HIGH 140; private static final int HIGH_BLOOD_PRESSURE_LOW 90; private static final int LOW_BLOOD_PRESSURE_HIGH 90; private static final int LOW_BLOOD_PRESSURE_LOW 60; // 血糖阈值mmol/L空腹 private static final double BLOOD_SUGAR_HIGH 7.0; private static final double BLOOD_SUGAR_LOW 3.9; public static AlarmType checkHealthRecord(HealthRecord record) { int high record.getBloodPressureHigh(); int low record.getBloodPressureLow(); if (high HIGH_BLOOD_PRESSURE_HIGH || low HIGH_BLOOD_PRESSURE_LOW) { return AlarmType.HIGH_BLOOD_PRESSURE; } if (high LOW_BLOOD_PRESSURE_HIGH || low LOW_BLOOD_PRESSURE_LOW) { return AlarmType.LOW_BLOOD_PRESSURE; } double sugar record.getBloodSugar(); if (sugar BLOOD_SUGAR_HIGH) { return AlarmType.HIGH_BLOOD_SUGAR; } if (sugar BLOOD_SUGAR_LOW) { return AlarmType.LOW_BLOOD_SUGAR; } return AlarmType.NORMAL; } }阈值参数用 static final 常量写死是最直观的但对生产环境不友好。我一般建议把阈值配置到 application.yml 里用 ConfigurationProperties 读取毕竟不同医生对血压血糖的控制标准有差异写死在类里改起来要重新编译。但如果你只是做设计源码交付写常量反而更好因为用户能直接在代码里看懂阈值从哪里来不需要翻配置文件。补充一个细节health_record 表允许血压或血糖其中一个字段为空比如老人只测了血压没测血糖。这时候阈值判断要注意空值上面代码里如果 bloodSugar 为 null调用 doubleValue 会空指针。实际实现我会在工具类入口先判空或者用类型包装类 Double 并在拆箱前判断。这个点在演示时不容易暴露但真实用户输入时很容易触发。4. 接口鉴权与配置JWT、Redis 缓存和关键 yaml 参数4.1 JWT 登录鉴权与拦截器配置健康管理涉及敏感隐私数据接口不能裸奔。JWT 是当前 Java 后端最常见的鉴权方案无状态、易扩展适合移动端和 Web 端共用。我用的方案是登录成功后签发 token前端每次请求带在 Header 的 Authorization 字段后端通过拦截器解析 token 并放入当前用户上下文。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 允许预检请求否则前端跨域会失败 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); UserContext.setUserId(claims.get(userId, Integer.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }JWT 的关键参数是过期时间。健康管理 App 的家属可能几天才打开一次token 过期太短比如 30 分钟会导致频繁重新登录老年人手机操作困难会很烦躁。我这里设置的过期时间是 7 天并且不主动刷新用户拿着旧 token 来访问如果过期了就重新登录。另一种做法是双 token 刷新机制但对这个体量的项目明显过度设计。JwtUtil 里生成签名的 secret 必须足够长至少 32 个字符否则 HS256 签名算法的安全性会打折扣。密钥不要硬编码在代码里放在 application.yml 的 jwt.secret 配置项中git 提交时用 .gitignore 忽略本地配置文件避免密钥泄密。签发 token 时在 claims 里只放 userId不要放手机号、密码等敏感信息因为 token 是可以被解码的只是签名无法伪造。拦截器注册用 WebMvcConfig 里 addInterceptors 方法要排除登录注册接口和健康检查路径。这个配置很容易被漏掉一旦漏了所有接口都无法访问表现为登录成功但列表页永远 401。4.2 Redis 缓存策略与 MyBatis-Plus 配置参数健康管理系统的数据特点是读多写少尤其是体征记录列表和家属首页的健康概览。用 Redis 缓存热点数据可以显著降低数据库压力。我的做法是家属首页的聚合数据缓存 5 分钟health_record 最近 10 条缓存 1 分钟。缓存时间不能太长否则老人测完血糖后家属端看到的还是旧值会产生信息误导。Redis 缓存的 key 设计要遵循可读性原则spring: redis: host: localhost port: 6379 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这些参数里值得专门说的是 logic-delete-field。健康档案删除应该走逻辑删除而不是物理删除因为医疗记录需要留痕。一旦配置了逻辑删除全局字段MyBatis-Plus 所有的查询会自动带上 deleted 0 条件调用 removeById 时自动转成 update 语句。这个功能很实用但要记住自定义 SQL 里如果用了 QueryWrapper 没问题原生 SQL 就需要手动加条件否则会查出已删除数据。map-underscore-to-camel-case 设置为 true 是强制要求。数据库字段习惯用下划线命名比如 elder_idJava 属性用驼峰命名 elderId不开启这个配置的话所有查询结果映射都会失败常见的坑是查出来全部为 null但 SQL 执行没有报错。StdOutImpl 是开发环境的 SQL 日志输出上线前要关掉或者换成 Slf4j 实现否则控制台会打满 SQL 语句且日志文件增长速度很快。Redis 的 lettuce.pool 参数中max-active 8 是默认推荐值小项目 16 个连接纯属浪费连接数过大会增加 Redis 服务器的文件描述符占用。4.3 跨域配置与全局异常处理前端如果单独部署在不同的端口比如 Vue 跑 8081后端跑 8080跨域问题一定会遇到。WebMvcConfig 里加跨域配置是必须的而且要注意 allowedOriginPatterns 不能写*因为 JWT 模式下前端请求带 Authorization header*无法处理带认证信息的请求。常见配置如下Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*, http://127.0.0.1:*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }全局异常处理用 RestControllerAdvice 统一捕获业务异常返回 code 非 0 和错误消息未知异常返回“系统繁忙”而不是把异常栈抛给前端。这个做法能保护后端细节不泄露同时前端拿到的错误信息是友好的。我见过很多项目把数据库连接失败的堆栈直接返回给前端这是很危险的。5. 老年人健康管理项目避坑5 条踩过的真实坑与解决5.1 现象用药剂量在小数位出现错位一次测试时发现某条用药记录的剂量是 0.5 片数据库里看到的是 0.5但接口返回给前端显示成 0.50 片。这不是大问题但反过来如果查询汇总时出现 0.499999就是 Java 浮点计算的经典问题。用 double 存储药物剂量在做累加或比较时会产生二进制浮点的精度误差。原因是 Java 的 double/float 是二进制浮点数0.1 在二进制里是无限循环小数任何涉及精确计算的数值都不能用浮点。解决方式是剂量字段用 BigDecimal 类型数据库字段用 decimal(5,2)所有剂量计算用 BigDecimal 的 add/compareTo 方法而不是直接用运算符。注意 BigDecimal 构造时要用字符串构造new BigDecimal(0.1)仍然是错的必须写new BigDecimal(0.1)。5.2 现象定时提醒任务在部署后不触发本地开发环境跑的时候用药提醒正常部署到 Linux 服务器后当天 08:00 没有推送。查日志发现任务根本没有执行。排查后发现是服务器时区是 UTC而本地是 CST。Spring Boot 的 Scheduled 基于 JVM 默认时区解析 cron 表达式服务器用 UTC 时08:00 对应的就是本地的 16:00所以一整天都不会触发。解决方式是在启动脚本 JVM 参数里加-Duser.timezoneAsia/Shanghai或者在 application.yml 里配置spring.jackson.time-zone: GMT8还不够因为定时任务时区由 JVM 时区决定。另外数据库连接串里加serverTimezoneAsia/Shanghai防止 JDBC 驱动时间转换偏移。这三处是分开的改一处不代表另外两处自动好建议三处都配一致。5.3 现象家属端显示的老人 ID 数据对不上小程序端展示老人列表时出现编号乱跳的问题。查了后发现是 MyBatis-Plus 默认的雪花 ID 是 Long 类型而 JavaScript 的 Number 类型无法精确表示大于 2^53 的整数前端拿到 ID 后精度丢失后 3 位变成 0导致查询时匹配到错误记录或查不到数据。解决方式有两种一种是把主键 ID 改成数据库自增让后端返回给前端的 ID 都是小整数不触发精度问题另一种是保留雪花 ID在后端做 JSON 序列化时把 Long 转成 String使用 Jackson 的 ToStringSerializer。考虑到健康管理系统的数据量不大我直接改成数据库自增最省事。如果将来要分库分表再换雪花 ID 也不迟。5.4 现象分页查询数据量越大响应越慢健康记录分页接口数据量到几万条时变慢接口响应从 200ms 涨到 2 秒。用数据库客户端执行同样的 SQL 却很快说明问题出在 MyBatis-Plus 分页插件的配置。检查发现代码里没有配置 PaginationInnerInterceptor导致调用 selectPage 时实际执行了全表查询然后在内存里分页数据全量加载后 JVM 又频繁 GC性能自然下降。解决方式是在 MybatisPlusConfig 里注册分页插件。另一个相关参数是分页查询时不要做 count 优化MyBatis-Plus 默认会单独执行一条 count 语句如果查询的表非常大count 也会慢。可以设置 selectPage 的第二个参数为 false 跳过 count对纯滚动加载场景很实用。5.5 现象老人重复提交体征记录导致数据重复用户快速点击保存按钮两次数据库里出现两条一模一样的数据。这不是偶发现象老年人手机触屏操作不灵活经常误触。解决方式是在后端接口做幂等处理前端生成一个 requestId 随请求提交后端用 Redis 的 setnx 命令判断 requestId 是否已处理过处理过的直接返回上一次结果。更简单的做法是在 health_record 表加一个唯一索引 (elder_id, record_time)同一位老人在同一秒不可能产生两条真实记录用唯一索引兜底。这个方案不需要改前端上线成本低对误触和网络重试都能拦截。但注意如果业务允许老人补录历史数据同一秒就可能出现合法记录这时候还是 requestId 方案更靠谱。6. 进阶把项目往设备对接和报表方向延展基础版源码跑通后最值得投入的方向是 IoT 设备数据对接和健康报表。先说设备对接很多老人家里有血压计、血糖仪带蓝牙或 Wi-Fi 模块数据不落地就浪费了。常见做法是设备厂商提供云接口后端定时拉取或者设备将数据推送到我们自己的 MQTT 服务后端订阅后清洗入库。MQTT 部分用 Eclipse Paho 客户端接入消息体设计成 JSON格式要跟 health_record 表字段对齐否则设备数据还要做字段映射。另一个高价值点是周报/月报。健康管理系统的粘性来自“持续的反馈”老人每周看到自己的血压趋势变化才有动力继续记录。报表接口可以用一个简单的定时任务每周一统计上周的平均血压、血糖生成趋势数据给家属端推送。这里不需要引入重量级的报表引擎用 JDK 的 DateTimeFormatter 做时间分组配合 Elasticsearch 的时间直方图反而是杀鸡用牛刀。做一个简单的折线图接口前端用小程序生态的图表库渲染两周工作量。还有一个容易被忽略但实际体验提升明显的功能用药漏服提醒的二次安抚。老人没有点确认服药消息时系统可以在 30 分钟后发一条温和的提醒内容不要用“您今天还没吃药”这种责备语气而是“如果有需要家里的药箱里有按时服用的提醒卡片”。这种细节设计对老年用户的体验影响很大而且后端实现完全依赖已有的 remind_log 的 is_acked 字段不需要新表。我自己在这个项目上最大的教训是不要试图在第一版就把所有功能做完先把建档、测记录、提醒、预警这一条主链路打磨顺再考虑设备对接和报表。我最初花了大量时间纠结于智能手环的数据协议结果核心功能的验收延迟了两周。先交付能解决核心问题的最小闭环用户有了真实反馈后再迭代。希望这篇笔记能帮你少走一些弯路把精力用在真正影响体验的地方。本文还有配套的精品资源点击获取