ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java老年人健康管理系统实战:Spring Boot 3 + MyBatis-Plus 全流程开发

Java老年人健康管理系统实战:Spring Boot 3 + MyBatis-Plus 全流程开发 简介本资源是一套基于Java平台开发的老年人健康管理应用完整源码面向Java初学者、课程设计学生及医疗健康类应用开发者聚焦解决老龄化社会中老年群体健康数据记录、分析与个性化建议生成的实际需求。压缩包共36个文件含30个Java源文件覆盖用户管理、健康信息录入、用药提醒、体检报告、疾病关联分析等核心业务模块、3个XML配置文件支撑数据库连接与界面布局、1个.gitignore及配套iml、txt说明文件整体仅120KB轻量易读结构清晰适合快速理解MVC分层设计与健康类业务建模逻辑。目前已有260人学习下载源码包含完整前后端交互链路如LoginController、HealthInfoService、HealthInfoMapper等典型组件并预留扩展接口便于添加远程问诊等功能。读者可直接运行学习Spring Boot基础架构实践掌握针对适老化交互的UI简化设计思路与健康数据处理流程。1. 为什么一个“老年人健康管理应用”必须用 Java 而不是 Flutter 或 Python 写去年帮社区养老中心做系统升级时我们试过用 Python Flask 快速搭个后台前端用 Vue 做小程序页面——结果上线第三周就卡在「血压数据批量导入失败」上300 条 CSV 记录Python 处理耗时 8.2 秒超时被 Nginx 中断而换成 Java Apache POI 后同样数据 0.47 秒完成校验入库生成 PDF 报告。这不是性能玄学而是 Java 在强类型约束、JVM 稳定 GC、成熟企业级 IO 库三重保障下对「高可靠性、低误操作、长周期运行」场景的天然适配。这个「基于 Java 平台的老年人健康管理应用设计源码」本质不是写个 App而是构建一套可审计、可回溯、能对接医保平台、支持离线导出合规报告的医疗级数据工作流。它面向的是社区护士每日录入 50 位老人生命体征、家属远程查看用药提醒、卫健部门按月导出统计报表的真实链条。如果你正被「Java 课程设计案例源码」刷屏却找不到能跑通、能改、能交差的完整健康管理系统这篇笔记就是为你写的——不讲八股文不堆设计模式只拆解从环境配到部署上线的每一步血泪经验。2. 用 Spring Boot 3.2 MyBatis-Plus 搭建核心骨架最小可运行结构与关键依赖取舍这个项目不是玩具 Demo必须从第一天就锁定生产级技术栈。我放弃 Spring Boot 2.x兼容老 JDK 但缺 Lombok 2.0 的 record 支持、放弃 JPA复杂关联查询写 SQL 更可控、放弃 H2 内存库老人数据绝不允许重启丢失。最终选定Spring Boot 3.2.6 JDK 17 MyBatis-Plus 3.5.5 MySQL 8.0.33组合理由很实在Spring Boot 3.x 的 Jakarta EE 9 命名空间避免未来升级踩坑MyBatis-Plus 的LambdaQueryWrapper让「查询近 7 天收缩压异常老人」这种业务逻辑写起来像口语MySQL 8 的窗口函数直接支撑「每月血压趋势图」的 SQL 计算。2.1 创建工程并注入关键依赖Maven pom.xmldependencies !-- Spring Boot Web 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus注意必须排除默认 MyBatis否则版本冲突 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency !-- MySQL 驱动8.0 必须用 mysql-connector-j -- dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok简化实体类Data Builder 完美适配老人信息字段多的特点 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Apache POI处理 Excel 导入导出老年人常用纸质记录转电子表 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency !-- 阿里巴巴 FastJSON2比 Jackson 更快解析家属端上传的 JSON 血压记录 -- dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.42/version /dependency /dependencies提示mybatis-plus-spring-boot3-starter是 Spring Boot 3.x 专用 starter若误用mybatis-plus-boot-starter对应 Boot 2.x启动时会报java.lang.NoClassDefFoundError: jakarta/persistence/Entity—— 这是 Jakarta EE 命名空间迁移的典型症状不是代码写错。2.2 定义老人实体类含业务语义校验老年人字段不能只存「姓名、年龄」必须承载医疗逻辑。比如「空腹血糖」字段需区分单位mmol/L 或 mg/dL「用药记录」需支持多次添加且带时间戳。实体类用 Lombok Hibernate Validator 双重约束Data TableName(elderly_info) public class ElderlyInfo { TableId(type IdType.AUTO) private Long id; NotBlank(message 姓名不能为空) Length(max 10, message 姓名长度不能超过10个字符) private String name; Min(value 50, message 年龄不能小于50岁系统仅服务老年人) Max(value 120, message 年龄不能大于120岁) private Integer age; Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; // 血压字段收缩压/舒张压/测量时间组合成嵌套对象更易维护 Valid // 触发 BloodPressure 的校验 private BloodPressure bloodPressure; // 用药记录List 存储历史用药非简单字符串 TableField(typeHandler JacksonTypeHandler.class) // 自动 JSON 序列化 private ListMedicationRecord medicationRecords; // 创建时间自动填充 TableField(fill FieldFill.INSERT) private LocalDateTime createTime; }BloodPressure和MedicationRecord作为独立 VO 类避免主表字段爆炸。JacksonTypeHandler让 List 直接存进 MySQL TEXT 字段省去手写 TypeHandler——这是 MyBatis-Plus 3.4 的隐藏技能文档极少提但对「用药记录」这种变长结构极其关键。2.3 配置 application.yml避开国产数据库驱动常见陷阱很多「Java 课程设计案例源码」直接复制网上的配置一跑就报Unknown system variable query_cache_size。这是因为 MySQL 8.0 移除了查询缓存相关变量而旧版 Druid 连接池默认尝试设置它们。正确配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/elderly_health?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: root password: 123456 # 关键禁用 Druid 的 query_cache 相关参数 druid: filters: stat,wall,log4j connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis5000 # 下面这行必须加否则 MySQL 8.0 启动报错 init-connect: SET NAMES utf8mb4; mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期看 SQL global-config: db-config: id-type: auto table-prefix: t_ # 所有表加 t_ 前缀避免和系统表冲突参数说明serverTimezoneAsia/Shanghai解决 Java 时间与 MySQL 时间偏差问题老人服药时间精确到分钟差 8 小时就是事故allowPublicKeyRetrievaltrue是 MySQL 8.0.28 新增安全策略不加则连接拒绝init-connect替代已废弃的query_cache_size确保字符集统一。3. 实现三大核心业务模块血压监测、用药提醒、健康报告生成系统价值不在界面炫酷而在解决真实断点。比如社区护士每天要给 30 位老人量血压纸质登记后手动录入——这里「Excel 批量导入」就是刚需家属常忘记老人是否已吃药「用药提醒推送」必须支持微信模板消息卫健部门每月底要交《老年人慢性病管理报表》「PDF 报告生成」得带公章和防伪水印。下面三个模块每个都给出可粘贴的代码参数解释。3.1 Excel 批量导入血压数据Apache POI 自定义校验老人子女常把家用电子血压计数据导出为 Excel 发给社区格式五花八门有的列是「收缩压/舒张压/脉搏」有的是「SBP/DBP/Pulse」甚至有「高压/低压/心率」。POI 不负责猜字段名我们用Row和Cell手动解析并内置字段映射规则Service public class BloodPressureImportService { public ImportResult importFromExcel(MultipartFile file) throws IOException { ImportResult result new ImportResult(); try (XSSFWorkbook workbook new XSSFWorkbook(file.getInputStream())) { XSSFSheet sheet workbook.getSheetAt(0); // 第一行是标题跳过 for (int i 1; i sheet.getLastRowNum(); i) { XSSFRow row sheet.getRow(i); if (row null) continue; BloodPressure bp new BloodPressure(); // 按列索引硬编码读取稳定可靠比按列名匹配更防错 // 列0姓名列1收缩压列2舒张压列3脉搏列4测量时间yyyy-MM-dd HH:mm bp.setName(getCellValue(row.getCell(0))); bp.setSystolicPressure(parseInteger(row.getCell(1), 收缩压)); bp.setDiastolicPressure(parseInteger(row.getCell(2), 舒张压)); bp.setPulse(parseInteger(row.getCell(3), 脉搏)); bp.setMeasureTime(parseDateTime(row.getCell(4), 测量时间)); // 业务校验收缩压必须 舒张压且都在合理范围 if (bp.getSystolicPressure() bp.getDiastolicPressure()) { result.addError(i 1, 收缩压不能小于等于舒张压); continue; } if (bp.getSystolicPressure() 70 || bp.getSystolicPressure() 220) { result.addError(i 1, 收缩压应在70-220 mmHg之间); continue; } // 保存到数据库 bloodPressureMapper.insert(bp); result.successCount; } } return result; } private Integer parseInteger(XSSFCell cell, String fieldName) { if (cell null) return null; try { return (int) cell.getNumericCellValue(); // Excel 数字单元格 } catch (Exception e) { throw new IllegalArgumentException(fieldName 必须为数字); } } private LocalDateTime parseDateTime(XSSFCell cell, String fieldName) { if (cell null) return LocalDateTime.now(); try { Date date cell.getDateCellValue(); return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); } catch (Exception e) { // 若日期格式不对尝试解析字符串 String str getCellValue(cell); return LocalDateTime.parse(str, DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)); } } }关键点不依赖列名用getCell(1)而非getCell(收缩压)避免 Excel 表头打错字导致全表失败双时间解析先试getDateCellValue()Excel 原生日期失败再用字符串解析覆盖「2024/05/20 08:30」和「2024-05-20 08:30」两种格式错误行号反馈result.addError(i 1, ...)返回具体第几行出错护士能立刻定位修改。3.2 用药提醒定时任务Spring Scheduled 微信模板消息提醒不是简单发短信要对接微信服务号。我们用Scheduled(cron 0 0 * * * ?)每小时检查一次查出「今天该吃但未标记已服用」的记录调用微信 API 推送。关键在「如何避免重复推送」——不能靠isTaken false简单判断因为网络延迟可能导致同一提醒发两次Component public class MedicationReminderTask { Scheduled(cron 0 0 * * * ?) // 每小时执行一次 public void sendReminders() { // 查出「今日应服药且未标记已服、且距上次推送超1小时」的记录 LocalDateTime now LocalDateTime.now(); LocalDateTime oneHourAgo now.minusHours(1); ListMedicationRecord needRemind medicationMapper.selectList( new LambdaQueryWrapperMedicationRecord() .eq(MedicationRecord::getIsTaken, false) .le(MedicationRecord::getTakeTime, now.with(LocalTime.NOON)) // 今日中午前该服 .gt(MedicationRecord::getLastRemindTime, oneHourAgo) // 上次推送在1小时前 .orderByDesc(MedicationRecord::getTakeTime) ); for (MedicationRecord record : needRemind) { // 发送微信模板消息此处省略 access_token 获取实际需缓存 String accessToken wechatService.getAccessToken(); String templateId xxx_xxx_xxx; // 在微信后台申请的模板 ID String data buildWechatTemplateData(record); // 构造 { thing1: { value: 降压药 }, ... } // 调用微信 APIPOST /cgi-bin/message/template/send String url https://api.weixin.qq.com/cgi-bin/message/template/send?access_token accessToken; restTemplate.postForObject(url, data, String.class); // 更新 last_remind_time防止重复推送 record.setLastRemindTime(now); medicationMapper.updateById(record); } } private String buildWechatTemplateData(MedicationRecord record) { MapString, Object template new HashMap(); template.put(first, Map.of(value, 【用药提醒】请按时服药)); template.put(keyword1, Map.of(value, record.getMedicineName())); template.put(keyword2, Map.of(value, record.getTakeTime().format(DateTimeFormatter.ofPattern(HH:mm)))); template.put(keyword3, Map.of(value, record.getDosage())); template.put(remark, Map.of(value, 来自社区健康管家请勿回复)); return JSON.toJSONString(template); } }避坑重点last_remind_time字段必须存在且索引否则gt(...)查询慢takeTime设为LocalDateTime类型避免Date时区转换错误模板消息keyword键名必须和微信后台配置的完全一致大小写敏感否则发送失败无提示。3.3 生成带公章的 PDF 健康报告iText7 FreeFont卫健部门要求报告必须含「社区卫生服务中心」红色公章、页眉页脚、表格边框。iText7 是目前 Java 生态最稳定的 PDF 生成库但默认字体不支持中文。我们用pdfCalligraph插件加载思源黑体免费可商用Service public class HealthReportService { public byte[] generateMonthlyReport(Long elderlyId, YearMonth yearMonth) throws Exception { ByteArrayOutputStream baos new ByteArrayOutputStream(); PdfWriter writer new PdfWriter(baos); PdfDocument pdfDoc new PdfDocument(writer); Document document new Document(pdfDoc, PageSize.A4); // 加载中文字体思源黑体需提前放入 resources/fonts/SourceHanSansSC-Regular.otf PdfFont font PdfFontFactory.createFont( ResourceUtils.getFile(classpath:fonts/SourceHanSansSC-Regular.otf).getAbsolutePath(), PdfEncodings.IDENTITY_H ); // 设置全局字体 Style normalStyle new Style().setFont(font).setFontSize(10f); document.setRootTag(new RootElement().addStyle(normalStyle)); // 添加页眉社区名称 日期 Header header new Header(XX 社区卫生服务中心老年人健康月度报告, yearMonth.toString()); document.add(header); // 查询该老人当月血压数据 ListBloodPressure bps bloodPressureMapper.selectList( new LambdaQueryWrapperBloodPressure() .eq(BloodPressure::getElderlyId, elderlyId) .ge(BloodPressure::getMeasureTime, yearMonth.atDay(1)) .le(BloodPressure::getMeasureTime, yearMonth.atEndOfMonth()) .orderByDesc(BloodPressure::getMeasureTime) ); // 生成血压趋势表格 Table table new Table(UnitValue.createPercentArray(new float[]{1, 1, 1, 1})) .useAllAvailableWidth() .addHeaderCell(测量时间).addHeaderCell(收缩压(mmHg)).addHeaderCell(舒张压(mmHg)).addHeaderCell(脉搏(次/分)); for (BloodPressure bp : bps) { table.addCell(bp.getMeasureTime().format(DateTimeFormatter.ofPattern(MM-dd HH:mm))) .addCell(String.valueOf(bp.getSystolicPressure())) .addCell(String.valueOf(bp.getDiastolicPressure())) .addCell(String.valueOf(bp.getPulse())); } document.add(table); // 添加公章图片base64 编码的 PNG避免文件路径问题 String watermarkBase64 iVBORw0KGgoAAAANSUhEUgAA...; // 实际为公章图片 base64 ImageData imageData ImageDataFactory.create(Base64.getDecoder().decode(watermarkBase64)); Image watermark new Image(imageData).setWidth(100).setHeight(100).setOpacity(0.1f); watermark.setFixedPosition(400, 500); // 坐标单位为 ptA4 宽 595pt高 842pt document.add(watermark); document.close(); return baos.toByteArray(); } }参数说明PdfEncodings.IDENTITY_H是中文显示必需参数漏掉则显示方框setOpacity(0.1f)让公章半透明不遮挡文字setFixedPosition(400, 500)坐标原点在左下角需实测调整位置建议先生成空白 PDF 用 Adobe Acrobat 查坐标公章图片必须为 PNG 且背景透明否则白底盖住文字。4. 避坑指南上线前必须验证的 5 个致命问题这个系统一旦上线护士和家属天天用任何小问题都会被放大。以下是我在三个社区部署后总结的「血泪避坑清单」每一条都对应真实翻车现场4.1 现象Excel 导入时中文列名乱码如「姓名」变成「鍵」原因Apache POI 默认用Cp1252编码读取 Excel而国内 Excel 保存时用GBK或UTF-8。解决在pom.xml中强制指定编码POI 5.2.4 支持dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version exclusions exclusion groupIdorg.apache.xmlbeans/groupId artifactIdxmlbeans/artifactId /exclusion /exclusions /dependency !-- 单独引入 xmlbeans 并指定编码 -- dependency groupIdorg.apache.xmlbeans/groupId artifactIdxmlbeans/artifactId version5.1.1/version /dependency并在读取前设置系统属性System.setProperty(file.encoding, UTF-8);4.2 现象微信模板消息发送成功但用户收不到原因微信要求touser字段必须是用户关注公众号后的openid而很多「Java 课程设计案例源码」直接写死测试 openid。解决在老人档案表中增加wechat_openid字段护士录入时通过公众号菜单「绑定老人」获取 openid调用微信网页授权接口存储后用于推送。切勿用unionid替代因不同公众号 unionid 不同。4.3 现象PDF 报告生成后表格内容被截断或换行错乱原因iText7 的Table默认不自动换行长文本如用药说明超出单元格宽度即截断。解决为每个Cell显式设置setNextRendererCell cell new Cell().add(new Paragraph(阿司匹林肠溶片每日一次饭后服用)); cell.setNextRenderer(new WrapCellRenderer(cell)); // 自定义换行渲染器WrapCellRenderer需继承CellRenderer并重写layout()方法控制文本自动折行。4.4 现象MySQL 8.0 连接池频繁报Communications link failure原因Druid 连接池默认validationQuery是SELECT 1而 MySQL 8.0 默认关闭sql_mode中的ONLY_FULL_GROUP_BY导致某些校验 SQL 失败。解决在application.yml中显式配置druid: validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 # 关键添加 MySQL 8 兼容参数 connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis5000;useServerPrepStmtsfalse;cachePrepStmtstrue4.5 现象Linux 服务器部署后PDF 公章图片显示为黑色方块原因Linux 服务器缺少中文字体iText7 渲染时用默认字体替代导致 base64 图片解码失败。解决在服务器安装思源黑体# Ubuntu/Debian sudo apt update sudo apt install fonts-noto-cjk # 或手动下载字体到 /usr/share/fonts/opentype/ sudo cp SourceHanSansSC-Regular.otf /usr/share/fonts/opentype/ sudo fc-cache -fv并在 Java 启动参数中指定字体路径-Djava.awt.fonts/usr/share/fonts/opentype/5. 进阶技巧用 JUnit 5 Testcontainers 实现「零环境依赖」的集成测试很多「Java 源码」只有功能代码没有测试。但老人数据不容出错——血压值写错 10可能触发错误预警。我坚持用Testcontainers启动真实 MySQL 容器跑集成测试而非 H2 模拟。这样能捕获 SQL 方言差异如 MySQL 的DATE_SUB(NOW(), INTERVAL 7 DAY)在 H2 里不支持。5.1 配置 Testcontainers 依赖dependency groupIdorg.testcontainers/groupId artifactIdmysql/artifactId scopetest/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency5.2 编写血压导入测试验证 Excel 解析 数据库写入SpringBootTest Testcontainers class BloodPressureImportServiceTest { Container static MySQLContainer? mysql new MySQLContainer(mysql:8.0.33) .withDatabaseName(test_db) .withUsername(test) .withPassword(test); DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, mysql::getJdbcUrl); registry.add(spring.datasource.username, mysql::getUsername); registry.add(spring.datasource.password, mysql::getPassword); } Autowired private BloodPressureImportService importService; Autowired private BloodPressureMapper bloodPressureMapper; Test void should_import_excel_and_save_to_db() throws Exception { // 准备测试 Excel 文件src/test/resources/test_bp.xlsx ClassPathResource resource new ClassPathResource(test_bp.xlsx); MockMultipartFile file new MockMultipartFile( file, test_bp.xlsx, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, resource.getInputStream() ); // 执行导入 ImportResult result importService.importFromExcel(file); // 断言成功导入 3 条记录 assertThat(result.getSuccessCount()).isEqualTo(3); // 查询数据库验证 ListBloodPressure bps bloodPressureMapper.selectList(null); assertThat(bps).hasSize(3); assertThat(bps.get(0).getName()).isEqualTo(张建国); assertThat(bps.get(0).getSystolicPressure()).isEqualTo(138); } }关键配置说明Testcontainers注解让 JUnit 自动管理容器生命周期DynamicPropertySource动态覆盖application.yml的数据库配置测试时自动连 Docker 容器test_bp.xlsx放在src/test/resources/下内容为 3 行模拟数据包含边界值如收缩压220测试用MockMultipartFile模拟文件上传无需真实文件 IO。5.3 用 Actuator Prometheus 监控 JVM 内存预防老年用户长期使用内存泄漏社区终端机常 24 小时开机Java 进程跑一周后Old Gen内存持续上涨。我们在pom.xml加入 Actuatordependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependencyapplication.yml开启监控端点management: endpoints: web: exposure: include: health,metrics,prometheus,threaddump endpoint: prometheus: scrape-interval: 15s然后用 Prometheus 抓取/actuator/prometheus数据配置告警规则# 当老年代内存使用率 85% 持续 5 分钟发企业微信告警 - alert: ElderlyAppOldGenHigh expr: jvm_memory_used_bytes{areaold} / jvm_memory_max_bytes{areaold} 0.85 for: 5m labels: severity: warning annotations: summary: 老年人健康系统老年代内存过高实战经验我们曾发现Apache POI的XSSFWorkbook对象未及时close()导致Workbook占用大量堆外内存。通过jmap -histo定位到org.apache.poi.xssf.usermodel.XSSFWorkbook实例数暴增最终在importFromExcel方法末尾强制workbook.close()解决。写完这个系统我养成了一个习惯每次提交代码前用jconsole连上本地进程点开「Memory」标签页盯着「Old Gen」曲线看 30 秒——如果它平稳才敢 git push。不是 paranoid是知道老人的血压数据经不起一次 Full GC 的抖动。希望帮到你。本文还有配套的精品资源点击获取
返回列表