简介:这是一套基于SSM(Spring+SpringMVC+MyBatis)框架开发的家庭健康管理系统实战项目,面向Java初学者与Web开发入门者,聚焦家庭场景下的健康指标数字化管理需求。系统涵盖家庭管理、成员档案维护、健康数据上报、指标动态跟踪及可视化健康报告生成等核心功能,兼顾业务逻辑完整性与Web前后端协作实践。资源包共107个文件,含34个Java后端类(实现Controller/Service/DAO分层)、21个JSP页面(含Bootstrap响应式前端界面)、17个XML配置文件(Spring与MyBatis整合关键),辅以JS交互脚本、CSS样式及SQL建表语句,整体仅806KB,轻量易部署。目前已有42人学习下载,读者可直接运行调试,获得完整MVC结构工程、带日期选择器的健康数据录入界面、多角色用户权限基础框架,以及清晰的模块化目录组织,是理解健康管理类Web应用从需求分析到落地实现的优质教学参考。
1. 一个家庭健康数据“中枢”:不是体检报告归档工具,而是能联动预警、支持多角色协同的轻量级健康指标管理平台
你有没有遇到过这种场景:家里老人每月测三次血压,数据记在小本子上;孩子学校发来视力筛查结果,PDF藏在手机相册里;自己健身App导出的心率曲线,和医院体检单上的空腹血糖完全对不上——所有健康数据散落在不同载体、不同时间点、不同人手里,真要查某人半年趋势,得翻三台设备、问五个人。这个基于SSM框架的家庭健康管理系统,就是为解决这个“数据孤岛+人工拼图”问题而生的。它不追求替代专业医疗系统,也不堆砌AI诊断模型,而是用极简的Java Web技术栈(Spring + SpringMVC + MyBatis),把家庭成员的体重、血压、血糖、心率、睡眠时长、运动步数等核心健康指标,统一收口到一个可登录、可分权、可图表化、可设阈值告警的Web界面里。适合社区卫生站做家庭医生签约辅助、中小学校健康管理台账、或普通家庭三代同堂的日常健康监护。项目结构清晰、无第三方云依赖、MySQL本地部署即可跑通,新手照着文档30分钟能启服务,熟手两天内可接入智能手环API或扩展慢病随访模块。
2. 技术栈与工程结构解析:SSM不是过时标签,而是可控、可调试、可落地的务实选择
这个项目没追Spring Boot自动配置的“爽感”,也没上微服务拆分的“体面”,它用最经典的SSM组合,把每个技术组件的职责边界划得清清楚楚——这恰恰是现场交付、二次开发、甚至排查线上问题时最需要的确定性。下面从源码包解压后的根目录开始,一层层剥开它的骨架。
2.1 项目目录结构:标准Maven Web工程,但关键配置全在眼皮底下
解压后你会看到典型的src/main/java、src/main/webapp、src/main/resources三层结构。重点不在层级,而在几个“一眼定位”的文件:
src/main/webapp/WEB-INF/web.xml:Servlet容器入口,定义了DispatcherServlet拦截/路径,以及字符编码过滤器——这是整个MVC链路的起点;src/main/resources/spring-mvc.xml:SpringMVC配置,明确声明了@Controller扫描包、视图解析器(前缀/WEB-INF/jsp/,后缀.jsp)、静态资源放行规则(/static/**);src/main/resources/spring-mybatis.xml:MyBatis核心配置,包含数据源(com.mysql.cj.jdbc.Driver)、连接池(org.apache.commons.dbcp.BasicDataSource)、Mapper扫描路径(mapper/**/*.xml);src/main/resources/jdbc.properties:数据库连接参数独立外置,方便部署时替换,内容就四行:jdbc.driver=com.mysql.cj.jdbc.Driver、jdbc.url=jdbc:mysql://localhost:3306/health_db?useSSL=false&serverTimezone=Asia/Shanghai、jdbc.username=root、jdbc.password=123456。
提示:
jdbc.properties里的serverTimezone=Asia/Shanghai是MySQL 8.0+必须项,漏写会导致启动时报The server time zone value 'XXX' is unrecognized——这是新手第一道墙,别跳过。
2.2 核心包结构:按业务域分层,Controller层直击高频操作
src/main/java下是标准的com.health.*包体系,但命名不玩抽象,直接对应功能:
com.health.controller:只有5个Controller类,FamilyMemberController(管理家庭成员增删改查)、HealthRecordController(核心指标录入与查询)、DashboardController(首页聚合图表)、AlertRuleController(告警阈值设置)、LoginController(登录登出);com.health.service:接口+实现分离,如HealthRecordService接口定义List<HealthRecord> getRecordsByMemberIdAndDateRange(Long memberId, String startDate, String endDate),其实现类HealthRecordServiceImpl里调用Mapper并做简单业务逻辑(如日期格式校验);com.health.dao:纯Mapper接口,无XML以外的SQL,例如HealthRecordMapper.java里只有一行List<HealthRecord> selectByMemberIdAndDate(@Param("memberId") Long memberId, @Param("startDate") String startDate, @Param("endDate") String endDate);;com.health.mapper:XML文件与DAO接口同名,SQL写得非常直白,比如HealthRecordMapper.xml中<select id="selectByMemberIdAndDate">SELECT * FROM health_record WHERE member_id = #{memberId} AND record_date BETWEEN #{startDate} AND #{endDate}</select>——没有动态SQL嵌套,没有复杂<choose>,全是可读、可调试的硬SQL。
这种结构意味着:你要改一个血压录入逻辑,路径就是HealthRecordController → HealthRecordService → HealthRecordMapper → HealthRecordMapper.xml,链路长度固定为4,没有中间代理、没有AOP横切、没有响应式流——代码在哪里、问题就在哪里。
2.3 数据库设计:6张表撑起全部功能,字段命名拒绝“玄学缩写”
建库脚本health_db.sql在src/main/resources/sql/目录下,共6张表,每张表字段数控制在10个以内:
| 表名 | 关键字段 | 说明 |
|---|---|---|
family_member | id,name,gender,age,relation,create_time | relation存“父亲/母亲/子女/配偶”,字符串而非枚举ID,降低前端渲染复杂度 |
health_record | id,member_id,record_date,systolic_bp,diastolic_bp,blood_sugar,heart_rate,weight,sleep_hours,steps | 所有健康指标字段均为DECIMAL(8,2)或INT,无VARCHAR存数字,避免后续计算转型 |
alert_rule | id,member_id,metric_type,min_value,max_value,enabled | metric_type为'systolic_bp'、'blood_sugar'等字符串,直接映射前端选项,不建字典表 |
user | id,username,password,role,member_id | role为'admin'或'family_user',权限控制粒度到页面级,非按钮级 |
login_log | id,user_id,login_time,ip_address | 记录登录行为,用于基础审计 |
system_config | id,config_key,config_value,description | 存'default_chart_days'='30'这类全局配置,方便后期不改代码调参 |
注意:
health_record.record_date类型为DATE,不是DATETIME。因为家庭健康数据按天汇总足够,存精确到秒反而增加索引体积、拖慢范围查询。我一般会强制要求所有日期类字段用DATE,除非业务真需要“某日第几次测量”。
2.4 前端资源组织:JSP+jQuery+Bootstrap,拒绝“前端黑匣子”
src/main/webapp/下没有node_modules,没有webpack.config.js,只有干净的三类资源:
/static/css/bootstrap.min.css+/static/js/jquery-3.6.0.min.js+/static/js/bootstrap.min.js:CDN下载后本地化,版本锁定,杜绝线上因CDN失效白屏;/WEB-INF/jsp/:所有JSP页面,命名即功能——dashboard.jsp(首页图表)、record_list.jsp(指标列表)、record_form.jsp(录入表单);/static/js/pages/:每个JSP配一个同名JS文件,如dashboard.js负责加载ECharts图表、record_list.js处理表格分页与搜索。
以record_form.jsp为例,表单元素<input type="text" name="systolic_bp" class="form-control" placeholder="收缩压(mmHg)">,其提交目标是HealthRecordController.addRecord(),Controller里直接用@RequestParam接收,不做DTO转换——简单,但足够。所有AJAX请求都用原生jQuery$.post(),返回JSON,错误统一走if (data.code !== 200) { alert(data.msg); },不搞Promise链或Vuex状态管理。这种“土法炼钢”方式,让前端同学改个按钮颜色、后端同学调个接口参数,都能在5分钟内完成并验证。
3. 启动与运行实操:从零部署到首页可见,绕不开的3个关键动作
项目不是解压即用,但也不需要你重装JDK或重配Tomcat。以下步骤基于Windows/Mac通用流程,Linux环境仅需将路径分隔符\\换成/,命令一致。
3.1 环境准备:JDK 8 + MySQL 5.7/8.0 + Tomcat 8.5,版本锁死是底线
- JDK:必须是JDK 8(
java -version输出含1.8.0_xxx),JDK 11+会因javax.servlet包路径变更导致编译失败。若已装高版本,请单独下载JDK 8u202(Oracle官网或Adoptium),并在IDE中为本项目指定JDK路径; - MySQL:推荐MySQL 8.0.26+,因
jdbc.url中已启用serverTimezone=Asia/Shanghai。若用5.7,请注释掉该参数,并确保MySQL时区为+08:00(执行SET GLOBAL time_zone = '+08:00';); - Tomcat:必须8.5.x(如8.5.94),Tomcat 9+默认禁用
web.xml中的<jsp-config>,会导致JSP页面无法解析。下载地址选Core: zip (pgp, sha512),解压后记住路径(如D:\apache-tomcat-8.5.94)。
提示:不要用IDE内置Tomcat!IntelliJ或Eclipse的“Add Framework Support”会自动生成
web.xml副本或覆盖context.xml,导致部署后404。务必用解压版Tomcat手动部署。
3.2 数据库初始化:执行SQL脚本,但注意字符集与引擎
进入MySQL命令行(或Navicat/HeidiSQL),执行以下三步:
-- 1. 创建数据库,显式指定字符集,避免中文乱码 CREATE DATABASE health_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 切换数据库 USE health_db; -- 3. 执行建表SQL(路径:src/main/resources/sql/health_db.sql) -- 注意:该SQL文件开头有 DROP TABLE IF EXISTS,确保每次执行都是干净状态 source D:/your-project-path/src/main/resources/sql/health_db.sql;执行后检查表是否创建成功:
SHOW TABLES; SELECT COUNT(*) FROM family_member; -- 应返回0,初始无数据关键点:utf8mb4是必须的,utf8在MySQL中实际是utf8mb3,不支持emoji和部分生僻汉字;InnoDB引擎已在SQL中指定,勿手动改成MyISAM——后者不支持事务,健康数据修改必须ACID保障。
3.3 项目部署:WAR包生成与Tomcat手动发布,拒绝IDE“一键部署”幻觉
不要点IDE的“Run on Server”。请严格按以下顺序:
清理并打包:在项目根目录(含
pom.xml处)打开终端,执行:mvn clean package -Dmaven.test.skip=true成功后,
target/目录下生成health-system-1.0.war(版本号以pom.xml中<version>为准)。复制WAR包:将
health-system-1.0.war复制到D:\apache-tomcat-8.5.94\webapps\目录下。启动Tomcat:双击
D:\apache-tomcat-8.5.94\bin\startup.bat(Windows)或./startup.sh(Mac/Linux)。观察控制台输出,直到出现:INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxxx] milliseconds INFO [main] org.apache.catalina.core.StandardContext.startInternal Container org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/health-system-1.0] has started访问首页:浏览器打开
http://localhost:8080/health-system-1.0/login.jsp。若看到登录页,输入默认账号admin/123456,成功跳转至仪表盘,则部署完成。
注意:首次访问可能较慢(约10秒),因Tomcat需解压WAR包并初始化Spring上下文。若卡在空白页,请立即查看
D:\apache-tomcat-8.5.94\logs\catalina.out,搜索ERROR或Caused by:,90%问题在此日志中。
3.4 首页功能验证:5分钟走通一条完整数据链路
登录后,按顺序验证三个核心动作,确认系统活了:
- 添加家庭成员:点击顶部导航“家庭成员” → “新增”,填入姓名“张三”、性别“男”、年龄“65”、关系“父亲”,提交;
- 录入健康指标:点击“健康记录” → “新增”,选择成员“张三”,日期选今天,填收缩压“142”、舒张压“88”、空腹血糖“6.3”,提交;
- 查看图表趋势:回到首页(“仪表盘”),选择成员“张三”,图表自动加载近7天血压折线图——若线条出现,且鼠标悬停显示数值,则后端查询、前端渲染、ECharts集成全部通畅。
这三步验证了:数据库写入(INSERT)、跨表关联查询(JOIN family_member & health_record)、前端图表渲染(AJAX → JSON → ECharts)整条链路。任何一步失败,都比“首页打不开”更精准地定位问题模块。
4. 避坑指南:那些让我重启三次Tomcat、重装两次MySQL才搞懂的5个致命细节
这个项目看似简单,但SSM老框架的“隐性契约”极多。以下是我部署时踩过的坑,按发生频率排序,每条都附带复现方式和根治方案。
4.1 现象:启动Tomcat后,访问/login.jsp报404,但/health-system-1.0/目录下确有该文件
原因:web.xml中<welcome-file-list>未配置,或Tomcat未识别web.xml为有效描述符。常见于IDE误操作导致web.xml被移到错误目录,或<web-app>根标签缺少version="3.0"属性。
解决:打开src/main/webapp/WEB-INF/web.xml,确认首行是<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_0.xsd" version="3.0">;同时检查web.xml物理路径是否为WEB-INF/web.xml(不能是WEB-INF/config/web.xml)。
4.2 现象:登录时用户名密码正确,但跳转回登录页,无任何错误提示
原因:LoginController.login()方法中,session.setAttribute("user", user)后未return "redirect:/dashboard.jsp",而是写了return "dashboard",导致SpringMVC试图找名为dashboard的视图(JSP),但实际文件是dashboard.jsp。
解决:打开LoginController.java,找到login()方法,将return "dashboard";改为return "redirect:/dashboard.jsp";。注意redirect:前缀和.jsp后缀缺一不可。
4.3 现象:健康记录列表页显示“暂无数据”,但数据库health_record表里明明有记录
原因:HealthRecordController.listRecords()方法中,@RequestParam("memberId") Long memberId接收参数,但前端JSP表单提交时,<select name="memberId">的option值是字符串(如value="1"),而Spring MVC默认不自动将字符串转Long,导致memberId为null,查询条件WHERE member_id = null永远不成立。
解决:在HealthRecordController.java的listRecords()方法参数前加@RequestParam(value = "memberId", required = false) Long memberId,并在方法内加判空逻辑:
if (memberId == null || memberId <= 0) { model.addAttribute("records", new ArrayList<>()); return "record_list"; }或者,更彻底的方案:前端<select>的option值改为value="${member.id}"(JSTL EL表达式),确保传的是数字字符串。
4.4 现象:血压图表显示为空白,浏览器F12看Network,/dashboard/data接口返回500
原因:DashboardController.getChartData()方法中,调用healthRecordService.getRecordsByMemberIdAndDateRange()时,传入的日期字符串格式为"2024-05-01",但MySQLDATE字段在BETWEEN查询中,若JDBC驱动版本低(如mysql-connector-java 5.1.x),会将字符串当作'2024-05-01 00:00:00'处理,导致时区转换错误。
解决:升级MySQL驱动。打开pom.xml,将<dependency>中mysql-connector-java版本从5.1.47改为8.0.33:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>同时确认jdbc.properties中jdbc.url包含serverTimezone=Asia/Shanghai。
4.5 现象:添加新成员后,在“健康记录”下拉框看不到该成员
原因:record_form.jsp中,<select>的<c:forEach items="${members}" var="m">循环,但HealthRecordController.toAddForm()方法未将familyMemberService.getAllMembers()结果放入Model。
解决:打开HealthRecordController.java,找到toAddForm()方法,在return "record_form";前添加:
model.addAttribute("members", familyMemberService.getAllMembers());并确认FamilyMemberService.getAllMembers()方法在FamilyMemberMapper.xml中有对应SQL:<select id="selectAll">SELECT * FROM family_member ORDER BY create_time DESC</select>。
这些坑的共同点是:错误不抛异常,不打印ERROR日志,只表现为“功能缺失”。根源在于SSM各层间的数据契约(如参数类型、视图名、SQL字段名)必须100%精确匹配,差一个字母、一个空格、一个分号,就静默失败。所以我的血泪经验是:第一次跑通后,立刻用Postman逐个测试所有Controller接口,看返回JSON是否符合预期,而不是等前端点不动了再回头查。
5. 核心模块代码精讲:从血压录入到阈值告警,看SSM如何把业务逻辑“钉”在代码里
现在我们聚焦两个最高频、最体现业务价值的模块:健康指标录入与健康阈值告警。它们不是CRUD堆砌,而是SSM框架下“控制层接收、服务层编排、DAO层执行”的典型范式。我会逐行拆解关键代码,告诉你为什么这么写、参数怎么调、哪里能改。
5.1 健康指标录入:HealthRecordController.addRecord()——一行@Valid背后的防御式编程
这是用户每天点一次的功能,代码必须健壮。打开HealthRecordController.java,找到addRecord()方法:
@PostMapping("/record/add") @ResponseBody public Result addRecord(@Valid HealthRecord record, BindingResult result, HttpSession session) { // 1. 参数校验:@Valid触发HealthRecord实体上的@NotNull、@Min等注解 if (result.hasErrors()) { List<String> errors = result.getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.toList()); return Result.fail("参数错误:" + String.join("; ", errors)); } // 2. 业务校验:防止录入未来日期 LocalDate today = LocalDate.now(); if (record.getRecordDate().isAfter(today)) { return Result.fail("记录日期不能晚于今天"); } // 3. 权限校验:确保当前登录用户有权为该成员录入 User currentUser = (User) session.getAttribute("user"); FamilyMember member = familyMemberService.getById(record.getMemberId()); if (!currentUser.getMemberId().equals(member.getId()) && !"admin".equals(currentUser.getRole())) { return Result.fail("无权为该成员录入数据"); } // 4. 执行保存 int rows = healthRecordService.addRecord(record); if (rows > 0) { return Result.success("录入成功"); } else { return Result.fail("录入失败,请重试"); } }逻辑说明与参数说明:
@Valid HealthRecord record:Spring MVC自动将POST表单数据绑定到HealthRecord对象,并触发其字段上的JSR-303校验注解(如@NotNull(message="收缩压不能为空"))。这是防御第一关,比在Service里写if (record.getSystolicBp() == null)更优雅;BindingResult result:必须紧跟@Valid参数之后,否则校验结果无法捕获。result.hasErrors()判断是否有校验失败项;record.getRecordDate().isAfter(today):HealthRecord.recordDate类型为LocalDate(Java 8时间API),避免Date类的线程安全问题。LocalDate.now()获取系统本地日期,无需考虑时区;session.getAttribute("user"):从Session取当前用户,User实体中memberId字段存储该用户对应的家庭成员ID,用于判断“子女能否录父母数据”——这是家庭健康系统的特有权限逻辑;Result是项目自定义的统一返回类,结构为{code:200, msg:"success", data:null},前端AJAX统一处理data.code。
改造提示:若要支持“批量导入Excel”,只需在此方法内加一个
@RequestParam("file") MultipartFile file参数,用Apache POI解析,再循环调用healthRecordService.addRecord()。不用动Service层,因为addRecord()本身已设计为单条插入。
5.2 告警阈值管理:AlertRuleController.saveRule()——动态SQL与业务规则的硬核结合
告警不是简单“超了就发邮件”,而是针对不同成员、不同指标、不同年龄段的差异化规则。AlertRule表的设计决定了它的灵活性,而AlertRuleMapper.xml里的SQL则体现了SSM对动态条件的掌控力。
AlertRuleController.java中saveRule()方法:
@PostMapping("/alert/rule/save") @ResponseBody public Result saveRule(@RequestBody AlertRule rule, HttpSession session) { User user = (User) session.getAttribute("user"); // 强制绑定当前用户可管理的成员 rule.setMemberId(user.getMemberId()); // 检查指标类型是否合法(防前端篡改) List<String> validTypes = Arrays.asList("systolic_bp", "diastolic_bp", "blood_sugar", "heart_rate", "weight"); if (!validTypes.contains(rule.getMetricType())) { return Result.fail("不支持的指标类型"); } int rows = alertRuleService.saveRule(rule); return rows > 0 ? Result.success("保存成功") : Result.fail("保存失败"); }关键在AlertRuleMapper.xml的SQL:
<insert id="insertRule" parameterType="AlertRule"> INSERT INTO alert_rule (member_id, metric_type, min_value, max_value, enabled, create_time) VALUES ( #{memberId}, #{metricType}, <choose> <when test="minValue != null and minValue != ''">#{minValue}</when> <otherwise>NULL</otherwise> </choose>, <choose> <when test="maxValue != null and maxValue != ''">#{maxValue}</when> <otherwise>NULL</otherwise> </choose>, #{enabled}, NOW() ) </insert>逻辑说明与参数说明:
<choose>标签是MyBatis的动态SQL,相当于Java的if-else。这里处理minValue和maxValue可能为null或空字符串的情况,避免插入''到DECIMAL字段报错;#{memberId}是预编译参数占位符,防SQL注入;NOW()是MySQL函数,插入当前时间,不依赖Java端时间;AlertRule实体中minValue和maxValue类型为BigDecimal,确保精度(如血糖6.35不能存成float丢失精度);enabled字段为TINYINT(1),Java中用Boolean接收,MyBatis自动映射true→1、false→0。
改造提示:若要增加“告警通知方式”(短信/微信/邮件),只需在
alert_rule表加notify_method VARCHAR(20)字段,AlertRule实体加String notifyMethod,AlertRuleMapper.xml中<insert>SQL加#{notifyMethod},Controller里加校验if (!Arrays.asList("sms","wechat","email").contains(rule.getNotifyMethod()))——改动集中在DAO和Controller,Service层无感知。
5.3 告警触发逻辑:HealthRecordService.checkAlerts()——定时任务与实时校验的双保险
告警不是“用户录入时立刻发短信”,而是分两层:实时校验(录入时检查并返回提示)+定时扫描(每小时扫一遍,补发遗漏)。checkAlerts()是后者的核心:
// 在HealthRecordService.java中 @Transactional public void checkAlerts() { // 1. 获取所有启用的告警规则 List<AlertRule> rules = alertRuleMapper.selectEnabledRules(); for (AlertRule rule : rules) { // 2. 查询该成员最近一条记录 HealthRecord latest = healthRecordMapper.selectLatestByMemberId(rule.getMemberId()); if (latest == null) continue; // 3. 根据指标类型取值,并与阈值比较 BigDecimal currentValue = getCurrentValue(latest, rule.getMetricType()); boolean alertTriggered = false; if (rule.getMinValue() != null && currentValue.compareTo(rule.getMinValue()) < 0) { alertTriggered = true; } if (rule.getMaxValue() != null && currentValue.compareTo(rule.getMaxValue()) > 0) { alertTriggered = true; } // 4. 若触发且未发送过,插入告警记录并通知 if (alertTriggered) { AlertLog log = new AlertLog(); log.setRuleId(rule.getId()); log.setRecordId(latest.getId()); log.setAlertValue(currentValue); log.setCreateTime(new Date()); alertLogMapper.insert(log); // 此处调用通知服务(伪代码) notificationService.sendAlert(rule.getMemberId(), rule.getMetricType(), currentValue); } } } // 工具方法:根据指标类型从HealthRecord取值 private BigDecimal getCurrentValue(HealthRecord record, String metricType) { switch (metricType) { case "systolic_bp": return record.getSystolicBp(); case "diastolic_bp": return record.getDiastolicBp(); case "blood_sugar": return record.getBloodSugar(); case "heart_rate": return record.getHeartRate(); case "weight": return record.getWeight(); default: return BigDecimal.ZERO; } }逻辑说明与参数说明:
@Transactional保证整个扫描过程原子性,避免部分规则执行成功、部分失败导致状态不一致;alertRuleMapper.selectEnabledRules()SQL很简单:SELECT * FROM alert_rule WHERE enabled = 1;healthRecordMapper.selectLatestByMemberId()SQL用ORDER BY record_date DESC LIMIT 1,确保取最新一条,而非最大ID(ID不等于时间顺序);currentValue.compareTo(rule.getMinValue()) < 0:BigDecimal必须用compareTo()比较大小,==或>会比较引用;notificationService.sendAlert()是预留扩展点,目前为空实现,你可接入短信网关(如阿里云SMS)或企业微信机器人。
这个方法会被Spring的
@Scheduled(cron = "0 0 * * * ?")每小时执行一次。若要改成“每15分钟”,把cron改为"0 0/15 * * * ?"即可。注意:生产环境务必加分布式锁,避免多实例重复扫描——这是你接手后的第一个优化点。
6. 二次开发实战:从接入智能手环API到支持多家庭隔离,我的3个落地改造技巧
这个项目不是终点,而是你搭建家庭健康应用的“最小可行底盘”。我接手过两个真实需求:某社区卫生站要对接华为手环API自动同步数据;某国际学校要求每个班级独立健康空间,互不可见。下面分享我落地时的3个具体技巧,不讲虚的,全是抄作业就能用的代码和配置。
6.1 技巧一:用Spring RestTemplate无缝接入手环API,避免“手动HTTP请求”玄学
华为手环开放平台提供GET /v1/user/{userId}/health-data接口,返回JSON格式的当日步数、心率、睡眠。传统做法是写HttpURLConnection,但容易出错。用Spring RestTemplate,5行代码搞定:
- 添加依赖(
pom.xml):
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.3.31</version> <!-- 与项目Spring版本一致 --> </dependency>- 配置RestTemplate Bean(
spring-mvc.xml):
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate"> <constructor-arg> <bean class="org.springframework.http.client.HttpClientHttpRequestFactory"> <property name="httpClient"> <bean class="org.apache.http.impl.client.CloseableHttpClient" factory-bean="httpClientBuilder" factory-method="build"/> </property> </bean> </constructor-arg> </bean> <bean id="httpClientBuilder" class="org.apache.http.impl.client.HttpClients" factory-method="custom"/>- 在HealthRecordService中写同步方法:
@Autowired private RestTemplate restTemplate; public void syncHuaweiData(String userId, String accessToken) { String url = "https://api.huawei.com/v1/user/" + userId + "/health-data"; HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); HttpEntity<String> entity = new HttpEntity<>(headers); try { ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); JSONObject json = new JSONObject(response.getBody()); // 解析JSON,构造HealthRecord对象 HealthRecord record = new HealthRecord(); record.setMemberId(getMemberIdByHuaweiUserId(userId)); // 你自己的映射逻辑 record.setRecordDate(LocalDate.now()); record.setSteps(json.optInt("steps", 0)); record.setHeartRate(new BigDecimal(json.optString("heartRate", "0"))); record.setSleepHours(new BigDecimal(json.optString("sleepHours", "0"))); healthRecordService.addRecord(record); // 复用原有录入逻辑 } catch (Exception e) { log.error("同步华为数据失败,userId:{}", userId, e); } }关键点:
RestTemplate自动处理JSON序列化、HTTP状态码、连接池复用;JSONObject来自org.json:json,轻量无依赖。不要用Jackson,会与Spring MVC的@ResponseBody冲突。
6.2 技巧二:用数据库Schema隔离实现“多家庭空间”,比ShardingSphere简单10倍
某学校要求:A班学生数据,B班老师看不到。SSM项目没上分库分表,但可以用MySQL Schema(数据库)隔离。原理:每个家庭(或班级)一个独立数据库,如health_db_class_a、health_db_class_b,共用同一套代码,只切换数据源。
- 修改
spring-mybatis.xml,支持多数据源:
<!-- 定义两个数据源 --> <bean id="dataSourceA" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}_class_a"/> <!-- 动态拼接 --> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="dataSourceB" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}_class_b"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置AbstractRoutingDataSource --> <bean id="dynamicDataSource" class="com.health.config.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="class_a" value-ref="dataSourceA"/> <entry key="class_b" value-ref="dataSourceB"/> </map> </property> < <p> <a href="https://download.csdn.net/download/XiongWinds/90270820" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>