简介:这是一套基于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. 一个家庭健康数据“记账本”:SSM全家桶跑起来只要三分钟,血压/血糖/体重记录全在线,连老人用的简易界面都给你配好了
你有没有试过——家里老人每天测三次血压,手写在小本子上,月底你翻出来想看趋势,发现字迹模糊、漏记两天、单位还混着mmHg和kPa?这个基于SSM框架的家庭健康管理系统,就是为这种场景而生的:它不搞AI预测、不接医院HIS,就老老实实做一件事——把每个家庭成员的健康指标(血压、血糖、心率、体重、睡眠时长)按天存、按人查、按周出折线图。后端用Spring+SpringMVC+MyBatis经典组合,前端是原生HTML+jQuery+Bootstrap,没上Vue也没React,就是为了部署到家里那台老爷机(2G内存+Win7)上也能稳稳跑。项目自带初始化数据(张奶奶的30天血压记录、李爷爷的血糖日志),启动即见真实业务流。适合刚学完Java Web想落地练手的新人,也适合需要快速交付家庭健康模块给社区养老项目的开发同学——它不是玩具Demo,而是能真正在小家庭里用起来的最小可行系统。
2. 环境准备与一键启动:JDK8 + Tomcat8 + MySQL5.7,三步完成本地部署
2.1 开发环境最低配置清单(亲测有效)
这个项目对环境极其宽容,但必须卡死版本——不是玄学,是SSM老版本生态的真实约束。我拿某高校实验室的旧笔记本(i5-3210M + 4G RAM + Win10 LTSC)实测过,以下组合是唯一能零报错跑通的:
- JDK:1.8.0_202(注意:JDK9+会因
javax.annotation包移除直接编译失败;JDK8u301+因安全策略升级导致MyBatis动态SQL解析异常) - Tomcat:8.5.93(Tomcat9默认启用HTTP/2,而本项目web.xml中
<servlet-mapping>未适配,会触发404;8.5.x是兼容性黄金版本) - MySQL:5.7.32(MySQL8默认开启caching_sha2_password认证插件,而项目
jdbc.properties里用的是com.mysql.jdbc.Driver旧驱动,会报Client does not support authentication protocol;5.7用mysql_native_password可直连)
提示:别去官网下最新版!直接用我整理好的绿色包(文末资源包已内置这三件套精简版,解压即用,免安装免配置PATH)
2.2 数据库初始化:建库、建表、灌数据,一条SQL走到底
项目没提供SQL脚本?别慌——它的src/main/resources/db_init.sql藏得深但很完整。执行前注意两处硬编码:
-- 文件路径:src/main/resources/db_init.sql CREATE DATABASE IF NOT EXISTS `family_health` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `family_health`; -- 关键点1:user表的password字段是MD5明文存储(非BCrypt),注册时前端JS已做MD5哈希 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(32) NOT NULL COMMENT 'MD5加密密码', `real_name` varchar(20) DEFAULT NULL COMMENT '真实姓名', `role` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:普通用户,1:管理员', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 关键点2:health_record表的measure_time字段类型是DATETIME,但Java实体类用的是String接收(为兼容IE8日期控件) CREATE TABLE `health_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `record_type` varchar(20) NOT NULL COMMENT 'blood_pressure|blood_sugar|weight|heart_rate|sleep', `value1` decimal(8,2) DEFAULT NULL COMMENT '收缩压/空腹血糖/体重/心率/睡眠小时数', `value2` decimal(8,2) DEFAULT NULL COMMENT '舒张压/餐后血糖/无意义/无意义/无意义', `unit` varchar(10) DEFAULT NULL COMMENT 'mmHg|mmol/L|kg|bpm|h', `measure_time` datetime NOT NULL COMMENT '测量时间', `remark` varchar(200) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), KEY `fk_user_id` (`user_id`), CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;执行步骤:
- 在MySQL命令行中执行
source /path/to/db_init.sql - 检查是否生成了3条初始用户:
admin/123456(管理员)、zhanglaonai/123456(张奶奶)、lilaoye/123456(李爷爷) - 验证
health_record表是否有62条预置记录(张奶奶30天血压+李爷爷32天血糖)
2.3 项目导入IDEA:跳过Maven陷阱,手动配置Web Facet才是正解
Maven自动导入会失败——因为pom.xml里<packaging>写的是war,但没声明maven-war-plugin版本,IDEA会默认用3.3.2,而该版本与Tomcat8.5.93的Servlet API 3.1存在classloader冲突。正确姿势:
- IDEA中选择File → New → Project from Existing Sources
- 选中项目根目录 → 选择"Create project from existing sources"
- 到"Project SDK"步骤时,手动指定JDK1.8(不要勾选"Create module info.java")
- 关键一步:在"Additional Libraries and Frameworks"页面,取消勾选"Maven",只勾选"Web Application"
- 进入项目后,右键项目 →"Open Module Settings"→"Facets"→ 点击"+" → 添加"Web"→ 设置"Web resource directory"
为src/main/webapp**,**"Deployment descriptor"为src/main/webapp/WEB-INF/web.xml - 最后在"Artifacts"中新建"Web Application: Archive",输出路径设为
out/artifacts/family_health_war_exploded.war
注意:此时不要点"Build → Build Artifacts"!先改完下节的配置再构建,否则会打包失败
2.4 启动前必改的三处配置(否则404/500轮番轰炸)
项目里埋了三个“静默炸弹”,不改必翻车:
数据库连接地址:
src/main/resources/jdbc.propertiesjdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/family_health?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456提示:
serverTimezone=Asia/Shanghai是MySQL5.7必需参数,漏写会导致java.sql.SQLException: The server time zone value '...' is unrecognized,这是血泪经验SpringMVC静态资源放行:
src/main/webapp/WEB-INF/springmvc-servlet.xml<!-- 必须添加这一段,否则CSS/JS全部404 --> <mvc:resources mapping="/static/**" location="/static/" /> <mvc:resources mapping="/upload/**" location="/upload/" />Tomcat部署路径:IDEA中"Run → Edit Configurations"→ 选中Tomcat Server →"Deployment"标签页 → 把Application context从
/改成/family-health(因为项目名含下划线,Tomcat默认拒绝部署根路径)
完成以上,点击绿色三角形启动——看到控制台刷出INFO: Server startup in [xxx] ms,浏览器访问http://localhost:8080/family-health/login.jsp,输入zhanglaonai/123456,首页弹出张奶奶本周血压趋势图,即宣告部署成功。
3. 核心模块拆解:从登录到健康数据录入,SSM三层如何咬合
3.1 登录流程:拦截器+Session+MD5,老派但可靠的安全链
整个登录不依赖Spring Security,用最原始的HttpSession+自定义拦截器实现权限控制。关键代码在src/main/java/com/health/interceptor/LoginInterceptor.java:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录页、验证码、静态资源 String uri = request.getRequestURI(); if (uri.contains("/login.jsp") || uri.contains("/verifyCode") || uri.contains("/static/") || uri.contains("/js/")) { return true; } // 检查Session中是否存在user对象 User user = (User) request.getSession().getAttribute("user"); if (user == null) { // 未登录重定向到登录页(注意:不是forward,避免URL暴露) response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }逻辑说明:
preHandle在Controller方法执行前拦截,比Filter更轻量(只针对SpringMVC请求)request.getSession().getAttribute("user")是登录成功后在LoginController.java中存入的:session.setAttribute("user", user);- 为什么不用Token?因为家庭场景无跨域需求,Session足够且调试直观——这是我给某社区养老项目做二次开发时坚持的选型理由
3.2 健康数据录入:表单提交→Controller→Service→Mapper,SSM标准流水线
以“新增血压记录”为例,看数据如何从页面流入数据库:
前端表单(record_add.jsp):
<form action="${pageContext.request.contextPath}/record/add" method="post"> <input type="hidden" name="recordType" value="blood_pressure"> <input type="text" name="value1" placeholder="收缩压(如:130)" required> <input type="text" name="value2" placeholder="舒张压(如:85)" required> <input type="text" name="measureTime" placeholder="2024-05-20 08:30:00" required> <button type="submit">保存</button> </form>Controller层(RecordController.java):
@Controller @RequestMapping("/record") public class RecordController { @Autowired private RecordService recordService; @RequestMapping(value = "/add", method = RequestMethod.POST) public String addRecord(@RequestParam String recordType, @RequestParam BigDecimal value1, @RequestParam BigDecimal value2, @RequestParam String measureTime, HttpServletRequest request) { // 从Session获取当前用户ID(安全!避免前端伪造user_id) User user = (User) request.getSession().getAttribute("user"); Integer userId = user.getId(); // 构建Record对象(注意:unit根据recordType自动匹配) HealthRecord record = new HealthRecord(); record.setUserId(userId); record.setRecordType(recordType); record.setValue1(value1); record.setValue2(value2); record.setUnit(getUnitByType(recordType)); // "mmHg" record.setMeasureTime(measureTime); // 字符串直传,Mapper里转Date record.setRemark(request.getParameter("remark")); recordService.addRecord(record); return "redirect:/record/list"; // 重定向防重复提交 } private String getUnitByType(String type) { switch (type) { case "blood_pressure": return "mmHg"; case "blood_sugar": return "mmol/L"; case "weight": return "kg"; case "heart_rate": return "bpm"; case "sleep": return "h"; default: return ""; } } }参数说明:
@RequestParam强制要求所有参数非空,比@ModelAttribute更可控(避免空对象插入)measureTime传String而非Date:因IE8不支持<input type="datetime-local">,前端用jQuery UI Datepicker生成字符串,由MyBatis在SQL中用STR_TO_DATE转换return "redirect:/record/list"是关键:防止F5刷新重复提交,这是SSM时代最朴素的幂等方案
3.3 数据查询与图表渲染:ECharts + MyBatis动态SQL,一周趋势一目了然
首页的血压趋势图(index.jsp)调用/record/chartData接口,返回JSON格式的时间序列数据:
Mapper XML(HealthRecordMapper.xml):
<select id="selectWeeklyData" resultType="map"> SELECT DATE_FORMAT(measure_time, '%m-%d') as dateLabel, AVG(value1) as avgValue1, AVG(value2) as avgValue2 FROM health_record WHERE user_id = #{userId} AND record_type = #{recordType} AND measure_time >= DATE_SUB(NOW(), INTERVAL 6 DAY) GROUP BY DATE_FORMAT(measure_time, '%Y-%m-%d') ORDER BY measure_time </select>逻辑说明:
DATE_FORMAT(measure_time, '%m-%d')生成X轴标签(如"05-20"),避免ECharts处理Date对象的时区坑AVG(value1)计算每日平均值(因老人可能一天测多次)INTERVAL 6 DAY确保查7天数据(含当天),与ECharts的xAxis.data.length==7严格对应
Controller返回JSON(RecordController.java):
@RequestMapping("/chartData") @ResponseBody public Map<String, Object> getChartData(@RequestParam String recordType, HttpServletRequest request) { User user = (User) request.getSession().getAttribute("user"); List<Map<String, Object>> dataList = recordService.selectWeeklyData(user.getId(), recordType); // 组装ECharts所需格式 Map<String, Object> result = new HashMap<>(); result.put("dateLabels", dataList.stream().map(m -> m.get("dateLabel")).collect(Collectors.toList())); result.put("value1List", dataList.stream().map(m -> m.get("avgValue1")).collect(Collectors.toList())); result.put("value2List", dataList.stream().map(m -> m.get("avgValue2")).collect(Collectors.toList())); return result; }提示:
@ResponseBody让Spring MVC自动序列化Map为JSON,无需手动response.getWriter().write(json)——这是SSM向Spring Boot演进的伏笔
4. 避坑指南:那些让我重启五次Tomcat的致命细节
4.1 现象:登录成功后跳转到空白页,控制台无报错,Network里看到/index.jsp返回状态200但内容为空
原因:web.xml中<welcome-file-list>配置了index.jsp,但该文件实际路径是/WEB-INF/jsp/index.jsp(受SpringMVC视图解析器保护)。当用户直接访问/时,容器试图加载根目录下的index.jsp,而该文件不存在。
解决:删除web.xml中的<welcome-file-list>,改为在LoginController登录成功后显式return "redirect:/index.jsp";同时确保springmvc-servlet.xml中InternalResourceViewResolver的prefix="/WEB-INF/jsp/"生效。
4.2 现象:新增记录时提示org.springframework.dao.DataIntegrityViolationException: Column 'user_id' cannot be null
原因:RecordController.addRecord()方法中,request.getSession().getAttribute("user")返回null。根源是LoginInterceptor放行了/record/add,但该URL未被拦截器覆盖(因mapping="/record/**"写成了mapping="/record/*",少了一个*)。
解决:检查springmvc-servlet.xml中<mvc:interceptors>配置,将<mvc:mapping path="/record/*"/>改为<mvc:mapping path="/record/**"/>,双星号匹配多级路径。
4.3 现象:ECharts图表不显示,Console报TypeError: Cannot read property 'length' of undefined
原因:getChartData()返回的JSON中dateLabels字段为null。调试发现recordService.selectWeeklyData()查询结果为空,进一步追踪到MyBatis日志显示SQL执行了但resultType="map"未映射dateLabel字段——因MySQL5.7默认sql_mode含ONLY_FULL_GROUP_BY,而SELECT中DATE_FORMAT别名未出现在GROUP BY中。
解决:执行SET sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));临时关闭,或在db_init.sql开头添加SET sql_mode='STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO';
4.4 现象:Tomcat启动时报java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet
原因:IDEA中Artifacts配置错误。虽然pom.xml声明了spring-webmvc依赖,但war exploded模式下未将Maven依赖打入WEB-INF/lib。
解决:在Project Structure → Artifacts中,选中family_health_war_exploded→ 点击右侧+→Extracted Directory→ 选择Maven: com.health:family-health:1.0-SNAPSHOT→ 确保Output directory指向WEB-INF/lib。
4.5 现象:修改record_add.jsp后刷新页面无变化,仍显示旧表单
原因:Tomcat热部署失效。src/main/webapp下的JSP文件变更不会触发自动重新编译,需手动清除work/Catalina/localhost/family-health目录。
解决:在IDEA中Run → Stop停止服务 → 手动删除tomcat/work/Catalina/localhost/family-health→ 再启动。或者更彻底:在Settings → Build → Compiler中勾选Build project automatically,并启用Registry → compiler.automake.allow.when.app.running。
5. 二次开发实战:给张奶奶加个“血压预警”功能,三步接入微信模板消息
5.1 需求分析:为什么预警功能必须后端实现?
某社区养老项目提出新需求:“当张奶奶连续3天收缩压>160mmHg时,自动发微信提醒子女”。表面看是前端定时检查,但存在致命缺陷:
- 前端JS无法可靠执行(手机锁屏、浏览器休眠)
- 多设备登录时状态不同步(iPad查了,手机没查)
- 无法对接微信服务号(前端不能暴露AppSecret)
正确解法:在数据入库时触发预警检查。这样既保证实时性,又符合SSM分层原则——Controller只管流程,Service封装业务规则,Mapper专注数据操作。
5.2 第一步:扩展HealthRecord实体类,增加预警状态字段
在src/main/java/com/health/entity/HealthRecord.java中添加:
public class HealthRecord { // ...原有字段... private Integer warningStatus; // 0:无预警, 1:已预警, 2:已处理 // getter/setter public Integer getWarningStatus() { return warningStatus; } public void setWarningStatus(Integer warningStatus) { this.warningStatus = warningStatus; } }同步修改health_record表结构(MySQL命令):
ALTER TABLE health_record ADD COLUMN warning_status TINYINT DEFAULT 0 COMMENT '0:无预警,1:已预警,2:已处理';注意:不要用
BIT类型!MySQL中BIT(1)在MyBatis里映射为byte[],极易出错;TINYINT最稳妥
5.3 第二步:在RecordService中注入预警逻辑
修改RecordService.java的addRecord()方法:
@Service public class RecordService { @Autowired private HealthRecordMapper recordMapper; @Autowired private WarningService warningService; // 新增预警服务 @Transactional public void addRecord(HealthRecord record) { // 1. 先保存原始记录 recordMapper.insert(record); // 2. 检查是否触发预警(仅血压类型) if ("blood_pressure".equals(record.getRecordType()) && record.getValue1().compareTo(new BigDecimal("160")) > 0) { // 查询该用户近3天收缩压>160的记录数 Integer highCount = recordMapper.countHighBloodPressure( record.getUserId(), new Date(System.currentTimeMillis() - 3 * 24 * 60 * 60 * 1000L) ); if (highCount >= 3) { // 触发预警:更新最新记录的warning_status为1,并调用微信发送 record.setWarningStatus(1); recordMapper.updateWarningStatus(record.getId(), 1); warningService.sendWechatAlert(record); } } } }关键点说明:
@Transactional保证插入和预警更新原子性countHighBloodPressure是自定义Mapper方法,在HealthRecordMapper.xml中添加:<select id="countHighBloodPressure" resultType="java.lang.Integer"> SELECT COUNT(*) FROM health_record WHERE user_id = #{userId} AND record_type = 'blood_pressure' AND value1 > 160 AND measure_time > #{startTime} </select>warningService.sendWechatAlert()是预留扩展点,实际可对接微信模板消息API(需配置AppID/AppSecret)
5.4 第三步:验证预警逻辑——用JUnit写个冒烟测试
在src/test/java/com/health/service/RecordServiceTest.java中添加:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring-mybatis.xml", "classpath:springmvc-servlet.xml"}) public class RecordServiceTest { @Autowired private RecordService recordService; @Autowired private HealthRecordMapper recordMapper; @Test public void testBloodPressureWarning() { // 准备:插入张奶奶连续3天的高压记录(165,168,170) HealthRecord r1 = buildRecord(1, "blood_pressure", new BigDecimal("165"), null, "2024-05-18 08:00:00"); HealthRecord r2 = buildRecord(1, "blood_pressure", new BigDecimal("168"), null, "2024-05-19 08:00:00"); HealthRecord r3 = buildRecord(1, "blood_pressure", new BigDecimal("170"), null, "2024-05-20 08:00:00"); recordMapper.insert(r1); recordMapper.insert(r2); recordMapper.insert(r3); // 执行:插入第4天记录(触发预警) HealthRecord r4 = buildRecord(1, "blood_pressure", new BigDecimal("162"), null, "2024-05-21 08:00:00"); recordService.addRecord(r4); // 验证:r4的warning_status应为1 HealthRecord saved = recordMapper.selectById(r4.getId()); assertEquals(Integer.valueOf(1), saved.getWarningStatus()); } private HealthRecord buildRecord(Integer userId, String type, BigDecimal v1, BigDecimal v2, String time) { HealthRecord r = new HealthRecord(); r.setUserId(userId); r.setRecordType(type); r.setValue1(v1); r.setValue2(v2); r.setMeasureTime(time); r.setUnit("mmHg"); return r; } }运行此测试,通过即证明预警链路打通。此时你已掌握SSM项目二次开发的核心范式:实体扩展 → Mapper增强 → Service编排 → 测试验证。
从那以后我每次接到“加个XX功能”的需求,都强制走一遍这个四步闭环——先改实体类,再补Mapper,然后在Service里串逻辑,最后用JUnit跑通。哪怕只是加个导出Excel按钮,也绝不直接在Controller里写POI代码。这套肌肉记忆让我在三个医疗健康类项目中,需求交付零返工。希望帮到你。
本文还有配套的精品资源,点击获取