
简介基于SSM框架的家庭健康管理系统完整源码包面向Java后端初学者、课程设计与毕业设计人群覆盖家庭管理、成员管理、健康指标管理、指标上报、健康报告等核心业务模块。系统前端采用HTML、JSP、JavaScript后端以Java/SpringMVC为主并包含典型XML配置与SQL初始化脚本可帮助理解SSM项目从需求分析到界面实现的完整脉络。压缩包共107个文件其中主要包含34个Java源码、21个JSP页面、17个XML配置、7个JS、6个CSS样式及1个SQL数据库脚本整体仅806KB结构紧凑、便于快速下载与本地部署。已有42人学习适合用作毕业设计参考或SSM整合开发练习既能学习家庭成员健康指标上报与报告生成逻辑也能直接借鉴前端布局与后端分层设计。1. 家庭健康管理系统到底解决了什么家里老人的血压、血糖数据今天记在本子上明天存在手机备忘录里去医院复查时医生问起近三个月的变化谁也说不清。这套基于SSM框架的家庭健康管理系统就是把散落的健康指标统一收进数据库用Java后端加HTML页面完成登录、家庭成员管理、健康指标录入与查询核心价值是让记录变成可追溯的数据。它不是什么炫技项目麻雀虽小但SSM整合的完整链路都在前端页面发请求、SpringMVC接参数、Service层做校验、MyBatis落库。适合正在学Java Web想找完整项目练手的人也适合做内部健康数据管理工具但不想上重框架的从从业者——跑通它你对SSM的理解会比只看教程扎实很多。2. 技术选型与运行准备SSM三件套的分工与项目骨架2.1 SSM三件套分工为什么这类项目仍然选它SSM是Spring、SpringMVC、MyBatis三件套的合称。Spring管理对象和事务SpringMVC负责请求路由和参数绑定MyBatis把SQL和Java方法映射起来。虽然Spring Boot已经简化了大量配置但这类家庭健康管理系统用SSM有一个实际好处配置过程全部暴露在眼前。拦截器怎么注册、事务管理器怎么生效、Mapper扫描路径怎么配每一步都看得见出了问题也能定位到具体环节。如果你的目标是快速搭生产级微服务Spring Boot更合适但如果你想彻底理解Java Web的请求流转过程SSM依然是最佳教材。这套代码里没有自动配置帮你兜底DispatcherServlet、ContextLoaderListener、SpringMVC配置文件都是手写的这对排查问题能力的提升帮助很大。2.2 项目目录结构与运行环境拿到资源后先看目录标准Maven结构核心代码集中在src/main/java和src/main/resources下。Java包按controller、service、mapper、entity四层分开页面放在WEB-INF/views下。运行环境我建议按这个版本搭配组件版本建议说明JDK1.8SSM项目用JDK8最稳切到更高版本容易遇到兼容问题Maven3.6依赖管理用配好国内镜像下载更快Tomcat8.5或9.0Servlet版本3.1兼容别用Tomcat 10包名变了MySQL5.7或8.0注意驱动版本差异8.0要用com.mysql.cj.jdbc.Driverpom.xml里几个关键依赖必须检查到位spring-context、spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、jackson-databind。jackson用于Ajax返回JSON少了它下面要讲的健康指标异步加载会直接报错。连接池我一般用druid监控做的比较全资源里没带的话补上这段依赖也不复杂。2.3 核心配置web.xml与Spring配置文件职责边界web.xml里两个重点ContextLoaderListener负责启动Spring根容器DispatcherServlet负责SpringMVC容器。很多人忽略一个细节——SpringMVC配置里的组件扫描要么只扫controller要么让两个容器扫描范围不重叠否则会出现事务失效或Bean重复创建。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueutf-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Spring根容器 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- SpringMVC前端控制器 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app这段配置里filter放DispatcherServlet之前是为了让请求先进编码过滤器否则POST中文参数会乱码这是后面避坑章节会细说的重灾区。url-pattern用/会接管所有请求静态资源需要额外放行SpringMVC配置里加mvc:default-servlet-handler/即可。如果你看到资源里用的是.do后缀那也可以只是风格更老一些。applicationContext.xml放数据源、事务管理器、MyBatis配置这些根容器内容spring-mvc.xml只放注解驱动、视图解析器、controller扫描包。分这么细的好处是数据库层面的问题去applicationContext找请求层的问题去spring-mvc找不用在一个文件里大海捞针。3. 核心流程实现登录拦截、健康指标录入与列表查询3.1 登录态控制拦截器怎么守住健康数据入口家庭健康管理系统里存的是家庭成员的真实健康数据不能裸奔。这里用拦截器实现登录校验原理是在请求进入Controller之前检查session里有没有用户标识没有就重定向到登录页。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object userId request.getSession().getAttribute(userId); if (userId null) { // 未登录重定向到登录页面 String contextPath request.getContextPath(); response.sendRedirect(contextPath /login); return false; } return true; } }这段代码逻辑很直白从session取出userId为空就重定向并中断请求。注意sendRedirect要带contextPath直接写/login在部署到子路径时会404。在spring-mvc.xml里注册拦截器时我一般只拦/indicator/**和/member/**这些需要登录的路径登录请求本身放行避免死循环。mvc:interceptors mvc:interceptor mvc:mapping path/indicator/**/ mvc:mapping path/member/**/ mvc:exclude-mapping path/login/ bean classcom.health.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这里的mapping路径要和Controller的RequestMapping对上才算生效。如果你发现拦截器不执行先看这里配的路径对不对。3.2 健康指标录入Controller到Mapper三层怎么协作录入是系统的核心操作。前端表单要传的字段一般有成员ID、指标类型血压、血糖、心率、体重、指标值、单位、测量时间和备注。Controller层不写业务逻辑只做参数接收和结果包装。Controller RequestMapping(/indicator) public class HealthIndicatorController { Autowired private HealthRecordService recordService; ResponseBody PostMapping(/add) public Result addRecord(RequestParam(memberId) Integer memberId, RequestParam(type) String type, RequestParam(value) Double value, RequestParam(unit) String unit, RequestParam(value measureTime, required false) String measureTime, RequestParam(value remark, required false) String remark) { HealthRecord record new HealthRecord(); record.setMemberId(memberId); record.setType(type); record.setValue(value); record.setUnit(unit); record.setMeasureTime(parseTime(measureTime)); record.setRemark(remark); return recordService.saveRecord(record); } }参数上用RequestParam逐个接收比直接绑对象更可控因为前端可能传空字符串直接绑DTO会出现类型转换异常。measureTime用requiredfalse是因为很多场景允许不传后端取当前时间兜底。Service层是校验的主战场Service public class HealthRecordServiceImpl implements HealthRecordService { Autowired private HealthRecordMapper recordMapper; Override public Result saveRecord(HealthRecord record) { // 基础校验成员是否属于当前登录用户 if (record.getMemberId() null || record.getValue() null) { return Result.error(参数不完整); } // 数值范围校验 if (!validValue(record.getType(), record.getValue())) { return Result.error(指标值超出合理范围); } record.setCreateTime(new Date()); recordMapper.insert(record); return Result.success(record.getId()); } }validValue这里其实做了健康指标管理里很关键的一件事——每个指标类型有合理区间比如血压高压超过250、心率超过250就是录入错误。校验放Service层而不是Controller层是因为Service可能被多个入口调用防的是业务数据污染不只是防HTTP参数。MyBatis的Mapper接口和XML映射写起来最机械public interface HealthRecordMapper { int insert(HealthRecord record); ListHealthRecord selectByMemberIdAndDate(Param(memberId) Integer memberId, Param(startTime) String startTime, Param(endTime) String endTime); }insert idinsert parameterTypecom.health.entity.HealthRecord useGeneratedKeystrue keyPropertyid INSERT INTO health_record (member_id, type, value, unit, measure_time, remark, create_time) VALUES (#{memberId}, #{type}, #{value}, #{unit}, #{measureTime}, #{remark}, #{createTime}) /insertuseGeneratedKeystrue要记得写不然插入后拿不到自增主键ID后面做录入完成跳转到详情这类需求时会卡住。XML文件路径要和applicationContext里的mapperLocations匹配比如classpath:mapper/*.xml路径写错启动不会立刻报错调用Mapper时才抛BindingException。3.3 指标列表与趋势查询分页和Ajax怎么配合列表页最常用的是按成员、按指标类型、按时间范围筛选。分页我直接用手写的PageHelper插件避免传参混乱。前端通过Ajax请求JSON数据动态渲染表格不刷新页面。$.ajax({ url: ctx /indicator/list, type: GET, data: { memberId: currentMemberId, type: selectedType, pageNum: pageNum, pageSize: 10 }, dataType: json, success: function(res) { if (res.code 0) { renderTable(res.data.list); } else { alert(res.msg); } }, error: function(xhr, status, error) { console.error(error); } });这里有个坑ctx如果是JSP里的${pageContext.request.contextPath}在纯HTML静态页面里取不到需要后端在每个页面渲染时把这个值写进JS变量里。列表数据返回的结构建议固定成{code, msg, data:{list, total, pageNum}}前端只认这一种格式切换后端逻辑时前端不用改。Service层做分页查询时排序规则要固定否则翻页过程中数据顺序会跳动PageHelper.startPage(pageNum, pageSize); ListHealthRecord list recordMapper.selectByCondition(condition); PageInfoHealthRecord pageInfo new PageInfo(list);PageHelper.startPage和selectByCondition必须是紧挨着的两行中间不能有其他MyBatis操作否则分页参数会串到别的查询上。这个细节坑过很多人后面的避坑章节还会再提。4. 数据库设计与关键校验健康指标怎么存才不翻车4.1 表结构设计用户、成员、指标记录三层关系健康管理系统的表结构比普通CRUD多一点讲究。用户表存账号家庭成员表和用户是多对一关系健康指标记录表和成员表是多对一关系。为什么不把成员直接挂在用户表下面因为同一个家庭成员可能被多个用户共同管理拆开方便后续做权限和共享。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE member ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, name varchar(50) NOT NULL, relation varchar(20) DEFAULT NULL COMMENT 与用户关系父亲/母亲/配偶/子女, gender tinyint(1) DEFAULT 0, birth_date date DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE health_record ( id int(11) NOT NULL AUTO_INCREMENT, member_id int(11) NOT NULL, type varchar(20) NOT NULL COMMENT 指标类型blood_pressure/glucose/heart_rate/weight, value decimal(8,2) NOT NULL, unit varchar(10) NOT NULL, measure_time datetime NOT NULL, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_member_time (member_id, measure_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;type字段用varchar存英文标识而不是直接用中文是为了前端i18n和后端代码判断都方便。value用decimal而不是float避免浮点误差——血压139.5存成139.49999会被健康指标管理规则直接误判成异常。measure_time单独建联合索引是因为列表页最常见的查询就是某成员某时间段的数据这个索引是查询性能的地基。4.2 指标边界校验数值合理区间和单位换算健康数据的质量取决于录入质量。边界校验分两层合理区间校验和医学预警校验。合理区间是把明显的手误挡在外面比如血压高压250、心率300、血糖30这些数值测量不出来基本是录入错误。预警校验是告诉用户数值偏高偏低比如血压超过140/90。public class HealthValueValidator { private static final MapString, double[] RANGE_MAP new HashMap(); static { RANGE_MAP.put(blood_pressure_high, new double[]{50, 250}); RANGE_MAP.put(blood_pressure_low, new double[]{30, 150}); RANGE_MAP.put(glucose_fasting, new double[]{1.0, 30.0}); RANGE_MAP.put(glucose_random, new double[]{1.0, 40.0}); RANGE_MAP.put(heart_rate, new double[]{20, 250}); RANGE_MAP.put(weight, new double[]{2.0, 300.0}); } public static boolean validRange(String type, double value) { double[] range RANGE_MAP.get(type); return range ! null value range[0] value range[1]; } }RANGE_MAP里的上下限要结合单位来看。血压单位是mmHg血糖单位是mmol/L体重单位是kg。如果后续加了一个用mg/dL的血糖录入入口必须做单位换算再进同一个校验方法否则边界规则直接失效。这就是为什么存储时要把unit字段一起存——不仅是给前端展示用更是给后续加工逻辑用的。4.3 按时间分页查询的SQL写法与性能注意列表查询最常见的SQL是按成员和时间条件过滤再用PageHelper分页。这里有个索引生效的写法要求select idselectByCondition resultTypecom.health.entity.HealthRecord SELECT id, member_id, type, value, unit, measure_time, remark FROM health_record where if testmemberId ! null AND member_id #{memberId} /if if testtype ! null and type ! AND type #{type} /if if teststartTime ! null and startTime ! AND measure_time gt; #{startTime} /if if testendTime ! null and endTime ! AND measure_time lt; #{endTime} /if /where ORDER BY measure_time DESC, id DESC /selectwhere标签会自动处理掉第一个条件前面的AND比手动加WHERE 11干净。排序用measure_time DESC, id DESC双字段是因为同一秒内可能插入多条记录只看时间会乱序。gt;和lt;是XML里的转义写法直接写会报XML解析错误。数据量大了之后这个查询的瓶颈在member_id measure_time联合索引上如果索引没建或者建反了页面上翻到后面几页会明显变慢。我一般会在explain里看一眼type字段如果是index而不是ref说明索引没走对。5. 避坑指南跑通这套系统的五个高频翻车点5.1 中文乱码过滤器顺序不对查了半小时才发现根源现象录入健康指标时备注里的中文传到后端变成???页面列表显示一片问号。原因CharacterEncodingFilter虽然在web.xml里配了但放在了DispatcherServlet后面请求先被SpringMVC处理了一遍参数才走到编码过滤器这时候乱码已经产生了。另一个原因是forceEncoding没设成true只改了请求的编码没改响应的。解决把CharacterEncodingFilter放在web.xml所有Filter的最前面并且同时设置request和response的编码。注意检查JSP页面顶部的contentTypetext/html; charsetutf-8三层编码保持一致页面、过滤器、数据库连接URL里的characterEncodingutf8。从那以后我每次配完编码都会故意输一条带血压偏高注意休息的备注测试一遍乱码问题当场现形。5.2 Mapper注入失败启动不报错调用时抛BindingException现象应用启动正常点击健康指标查询时后台报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。原因两个层面要么Mapper接口和XML文件的namespace没对齐要么applicationContext里的mapperLocations路径没扫到XML。很多项目把Mapper.xml放在src/main/java的包里但没配资源过滤Maven打包时XML没进target/classes运行期自然找不到。解决先看编译后的target/classes目录里有没有对应XML文件没有就在pom.xml里加resources配置把**/*.xml包含进构建有的话再检查namespace的值是不是接口的全限定名。我排查这类问题固定的顺序是看target目录、看namespace、看mapperLocations三步走完基本能定位。5.3 Ajax总是进error回调返回的JSON没被正确识别现象点击指标列表刷新浏览器Network面板显示请求200但页面弹不出数据jQuery走了error分支。原因后端接口用了ResponseBody但没引入jackson依赖或者SpringMVC配置里没开mvc:annotation-driven/导致返回对象时Content-Type是text/html而不是application/jsonjQuery的dataType为json时直接判定解析失败。解决确认pom里有jackson-databind在spring-mvc.xml里保留mvc:annotation-driven/。接口返回时显式指定produces application/json; charsetutf-8这招最直接。前端调试时先打开浏览器Network看响应的Content-Type和Body内容比盲猜快得多。5.4 事务不生效Service内部方法自调用是重灾区现象Service里先插入一条健康记录再更新成员的最近测量时间第二个SQL抛异常但第一个插入没有回滚数据库多了脏数据。原因这类系统里最常见的两种事务失效原因——事务管理器没配好或者Service的a方法调b方法时走的是this.b()而不是代理对象导致b方法上的Transactional完全被忽略。解决先确认applicationContext里有tx:annotation-driven transaction-managertransactionManager/且transactionManager的dataSource引用正确。内部调用问题改成注入自己的代理对象或者把需要事务的代码单独拆到一个Service类里由Spring注入调用。排查事务问题我会在方法里故意抛一个RuntimeException看前一条SQL是否回滚这是最快探明事务到底生效没有的土办法。5.5 改了代码不生效Tomcat发布路径和浏览器缓存的夹击现象明明改了Controller返回的字段名前端页面刷新后还是旧数据F12看接口返回的确实是旧JSON。原因IDEA里用Exploded方式部署时有时候target目录里的class没重新编译Tomcat加载的还是旧class另外浏览器对静态资源有缓存尤其是引用的JS和CSS文件同一个URL直接走本地缓存。解决部署前执行Maven的clean命令手动清掉target目录再重新编译前端静态资源引用的URL后面加?v版本号强制刷新缓存。这个坑在多人协作时尤其坑——因为别人可能根本没拉最新代码。我现在每改完一个接口第一件事就是看响应里的数据是不是预期格式然后强制刷新页面两步验证下来很少被改了没生效的问题卡住。6. 进阶玩法给系统加一个自动异常预警模块健康指标管理的价值不只是记录而是通过数据发现风险。这套系统跑通后下一步我会加一个预警判断模块根据最新指标值判断是否超出医学参考范围超出就在列表里标红提示并把预警记录存下来。预警规则用简单的配置类管理不引入规则引擎避免复杂度失控public class HealthAlertEvaluator { private static final MapString, double[] ALERT_THRESHOLD new HashMap(); static { // 血压高压收缩压 140 为异常 ALERT_THRESHOLD.put(blood_pressure_high, new double[]{140.0, 0}); // 血压低压舒张压 90 为异常 ALERT_THRESHOLD.put(blood_pressure_low, new double[]{90.0, 0}); // 空腹血糖 7.0 为异常 ALERT_THRESHOLD.put(glucose_fasting, new double[]{7.0, 0}); // 心率上限100下限50 ALERT_THRESHOLD.put(heart_rate, new double[]{50.0, 100.0}); } public static AlertResult evaluate(String type, double value, String unit) { double[] range ALERT_THRESHOLD.get(type); if (range null) { return null; } boolean abnormal; String message; if (type.equals(heart_rate)) { abnormal value range[0] || value range[1]; message abnormal ? 心率超出正常范围(50~100) : null; } else { abnormal value range[0]; message abnormal ? 数值高于参考上限( range[0] unit ) : null; } return new AlertResult(abnormal, message); } }预警模块的关键是阈值的可配置性。现在硬编码在Map里后续可以改成从数据库读配置表这样不同家庭可以为糖尿病人设置更严格的控制目标。我给这个类设计成静态方法是为了在Service里无状态调用不涉及Spring容器扩展接进现有项目成本最低。验证方法也很直接造一组边界测试数据血压分别传139.9、140.0、140.1血糖传6.9、7.0、7.1心跳传49、50、100、101运行看结果是否符合预期。重点检查等于阈值那条——业务规则默认达到阈值即预警所以140.0要预警139.9不预警这个边界很容易写反。把预警结果和健康记录表关联可以在查询列表时动态计算也可以单独建预警记录表以便做已处理/未处理追踪。我建议先用动态计算方案跑一段时间确认规则稳定了再落地预警表避免一开始就过度设计。从那以后我每次给系统加规则判断都强制走一遍边界数据测试上限、下限、等于阈值三个点必须跑通这个习惯帮我挡掉了不少看起来正常但边界翻车的问题。希望这套SSM家庭健康管理系统的拆解能帮到你动手跑一遍踩过的坑都会变成经验。本文还有配套的精品资源点击获取