简介这是一套基于SSMSpringSpringMVCMyBatis框架开发的家庭健康管理系统实战项目面向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就老老实实做一件事——把每个家庭成员的健康指标血压、血糖、心率、体重、睡眠时长按天存、按人查、按周出折线图。后端用SpringSpringMVCMyBatis经典组合前端是原生HTMLjQueryBootstrap没上Vue也没React就是为了部署到家里那台老爷机2G内存Win7上也能稳稳跑。项目自带初始化数据张奶奶的30天血压记录、李爷爷的血糖日志启动即见真实业务流。适合刚学完Java Web想落地练手的新人也适合需要快速交付家庭健康模块给社区养老项目的开发同学——它不是玩具Demo而是能真正在小家庭里用起来的最小可行系统。2. 环境准备与一键启动JDK8 Tomcat8 MySQL5.7三步完成本地部署2.1 开发环境最低配置清单亲测有效这个项目对环境极其宽容但必须卡死版本——不是玄学是SSM老版本生态的真实约束。我拿某高校实验室的旧笔记本i5-3210M 4G RAM Win10 LTSC实测过以下组合是唯一能零报错跑通的JDK1.8.0_202注意JDK9会因javax.annotation包移除直接编译失败JDK8u301因安全策略升级导致MyBatis动态SQL解析异常Tomcat8.5.93Tomcat9默认启用HTTP/2而本项目web.xml中servlet-mapping未适配会触发4048.5.x是兼容性黄金版本MySQL5.7.32MySQL8默认开启caching_sha2_password认证插件而项目jdbc.properties里用的是com.mysql.jdbc.Driver旧驱动会报Client does not support authentication protocol5.7用mysql_native_password可直连提示别去官网下最新版直接用我整理好的绿色包文末资源包已内置这三件套精简版解压即用免安装免配置PATH2.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; -- 关键点1user表的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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 关键点2health_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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行步骤在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.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/family_health?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456提示serverTimezoneAsia/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 登录流程拦截器SessionMD5老派但可靠的安全链整个登录不依赖Spring Security用最原始的HttpSession自定义拦截器实现权限控制。关键代码在src/main/java/com/health/interceptor/LoginInterceptor.javapublic 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→MapperSSM标准流水线以“新增血压记录”为例看数据如何从页面流入数据库前端表单record_add.jspform action${pageContext.request.contextPath}/record/add methodpost input typehidden namerecordType valueblood_pressure input typetext namevalue1 placeholder收缩压如130 required input typetext namevalue2 placeholder舒张压如85 required input typetext namemeasureTime placeholder2024-05-20 08:30:00 required button typesubmit保存/button /formController层RecordController.javaController 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 typedatetime-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 XMLHealthRecordMapper.xmlselect idselectWeeklyData resultTypemap 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.length7严格对应Controller返回JSONRecordController.javaRequestMapping(/chartData) ResponseBody public MapString, Object getChartData(RequestParam String recordType, HttpServletRequest request) { User user (User) request.getSession().getAttribute(user); ListMapString, Object dataList recordService.selectWeeklyData(user.getId(), recordType); // 组装ECharts所需格式 MapString, 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执行了但resultTypemap未映射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_modeSTRICT_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 idcountHighBloodPressure resultTypejava.lang.Integer SELECT COUNT(*) FROM health_record WHERE user_id #{userId} AND record_type blood_pressure AND value1 160 AND measure_time #{startTime} /selectwarningService.sendWechatAlert()是预留扩展点实际可对接微信模板消息API需配置AppID/AppSecret5.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代码。这套肌肉记忆让我在三个医疗健康类项目中需求交付零返工。希望帮到你。本文还有配套的精品资源点击获取