ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Java个人健康管理系统:追踪与干预闭环设计

SpringBoot+Java个人健康管理系统:追踪与干预闭环设计

去年帮学弟把关毕业设计,他问得很直接:"老师给的题目是'基于SpringBoot与Java的个人健康管理系统',后面还挂了个副标题叫'智能个体健康追踪与干预平台'。我搜了一圈参考,感觉全是增删改查,这怎么写得出亮点?"这个疑问我太熟悉了。健康管理系统的难点从来不在CRUD,而在标题里"追踪"和"干预"这两个词——数据进来了能不能形成连续的追踪链条?分析出问题之后能不能给出让人愿意执行的干预动作?至于"个人全生命周期健康守护",说白了就是不同年龄段用不同的分析标准。今天就把这个经典的毕设题目完整拆一遍,从需求拆解、技术选型、数据库设计、核心代码到答辩准备,正在开题或已经写了一半的同学可以直接对着抄。

后来我陆续看过不少同题的成品,得分差距真的很大。有的做完就是三个页面:录入、列表、个人中心,评委问两句就露馅。有的功能不多,但把"记录→分析→干预→反馈"这条闭环做通了,答辩时现场演示一套完整的健康管理流程,分数明显高一档。这篇文章要解决的问题,就是怎么把后者这种"闭环感"落地成代码,而不是停在PPT上。

1. 先拆题:追踪、干预、全生命周期各自意味着什么

1.1 "追踪"不是记录流水账,而是数据连续性

很多同学把健康追踪理解成"用户每天往系统里录体重和血压",这没错,但只对了一半。追踪的价值在于"连续"两个字——单独的体重数据没有任何意义,一组按时间排列的体重数据才能看出趋势:是稳定、上升还是下降。所以系统从设计第一天起就要为"连续性"服务,而不是做一个高级记事本。

落到功能上,至少要有三件事。第一,每个指标要有独立的时间轴,录入时把记录时间存清楚,查询时能按天、按周、按月做聚合。第二,要有缺失数据的感知,用户连续几天没录体重,系统应该能发现并提醒,这种细节非常体现工程意识。第三,趋势可视化的数据结构要提前设计好,ECharts折线图需要的是"日期+数值"的序列,这个结构最好在数据库查询阶段就组织好,不要在内存里二次加工。

我的建议是:指标类型不要写死。用一张health_record表,通过record_type字段区分weight、blood_pressure、heart_rate、blood_sugar、sleep_duration等。这样做的好处是新增指标不用改表结构、不用加接口,前端一个下拉框把类型传进来,服务端统一处理。缺点是没法给特定指标建专用字段索引,但对毕设的数据量来说完全够用。

1.2 "干预平台"才是这个题目的分值点

如果只看"追踪",你的系统和一张Excel表没有本质区别。真正让评委眼前一亮的地方在于"干预":系统分析了用户的数据之后,能不能生成有针对性的建议和计划,并且推动用户去执行?

一个完整的干预闭环是这样的:分析出BMI偏高 → 系统生成"减重干预计划" → 计划里包含每日运动目标和饮食建议 → 用户逐项打卡 → 打卡结果再反馈回系统,作为下一轮分析的输入。这条链路跑通了,你的项目就有了"平台"的概念,而不是一个单机记录工具。

实现上需要两张表:intervention_plan(干预计划)和intervention_task(计划里的具体任务)。计划是总纲,任务是每天要执行的单元。一个计划可以包含多条任务,任务可以按天生成,也可以一次性生成一周的任务让用户自由选做。我在代码里采用的是"懒生成"策略:只生成当天的任务,第二天由定时任务补上,这样避免一次性生成一大堆过期任务没人管,也方便任务完成状态的统计。

1.3 "全生命周期"要控制范围,别给自己挖坑

"全生命周期"这个词听起来很大——婴幼儿、青少年、中老年,不同阶段的关注指标和参考标准都不一样,真要面面俱到,一个毕设是做不完的。合理的做法是:把用户按年龄段分组,不同年龄段对应不同的指标参考区间和建议模板。

比如18到39岁这个组,重心是BMI、作息、运动量;40到59岁这个组,增加血压等指标;60岁以上,重点关注血压、心率和慢性病提醒。实现上就是在user表加一个age_group字段,分析规则里做分层判断。这样既回应了标题里的"全生命周期",又控制住了开发量,答辩时还能理直气壮地说"系统覆盖了用户从成年到老年的各个健康阶段"。

模块范围也建议卡在这几块:用户管理、健康档案、指标记录、趋势分析、风险预警、干预计划、任务提醒、数据看板。看上去多,其实每个模块的核心逻辑都很直白,比做一个半吊子的"智能推荐系统"靠谱得多。

2. 技术选型:SpringBoot + Java 为什么是毕业设计的"标准答案"

2.1 技术栈组合与各自的理由

先给出一套我验证过比较稳的组合,适合大多数拿到这个题目的同学:

层次选型理由
后端框架SpringBoot 2.7.x + Java 8生态成熟,资料多,Java 8在各种环境的兼容性最好
ORMMyBatis-Plus 3.5.x单表CRUD零SQL,分页插件好用,比JPA更容易跟答辩评委解释
数据库MySQL 8.0免费、稳定,毕设数据量绰绰有余
缓存Redis(可选)做验证码、短期token或缓存分析结果,加分项不是必需
权限认证Sa-Token 或自研JWTSa-Token上手极快,比Spring Security少写很多配置
前端Vue 3 + Element Plus + ECharts组件成熟,图表漂亮,和管理后台的气质很搭
接口风格RESTful JSON前后端分离,答辩演示时也更直观

为什么推荐SpringBoot而不是更轻量的框架?两个原因。第一,毕业设计的评分逻辑里,"工程完备性"是很重要的一环,SpringBoot自带的配置管理、异常处理、打包部署机制让你天然拥有一个规范的项目骨架。第二,面试和后置学习都用得上,Spring全家桶是Java岗位的基本盘,做一个SpringBoot项目写在简历上是加分项,比用某个小众框架更有说服力。

2.2 目录结构设计要能"讲出道理"

项目包结构直接反映设计水平,评委大概率会让你讲一下每个包是干什么的。我的分层如下:

com.example.health ├── controller // 接口层:只做参数接收和结果返回 ├── service // 业务层:核心逻辑,必须有接口+实现类 ├── mapper // 数据访问层:MyBatis-Plus的Mapper接口 ├── entity // 实体类:和数据库表一一对应 ├── dto // 数据传输对象:接收前端参数 ├── vo // 视图对象:返回给前端的结构 ├── config // 配置类:跨域、拦截器、定时任务等 ├── common // 通用类:统一返回结果、异常、常量 └── utils // 工具类:日期、校验、计算

controller只负责两件事:接收参数、调用service。service里放业务规则。mapper里尽量只有MyBatis-Plus生成的基础方法,复杂统计SQL可以手写,但也要放在mapper层。这样分层最大的好处是答辩时好讲:"接口层、业务层、数据层分离,各层职责单一",这句话说出来,评委对你的印象分就上来了。

2.3 基础依赖和配置文件

pom.xml里最核心的依赖是这样:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

application.yml里几个关键配置:

spring: datasource: url: jdbc:mysql://localhost:3306/health_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto mapper-locations: classpath*:mapper/**/*.xml

这里有两个细节要认真对待:一是MySQL连接串里必须带serverTimezone,否则Java 8的LocalDateTime和数据库的datetime对接时会出现时区偏差,常见报错是通信链路失败或时间差8小时;二是MyBatis-Plus开启map-underscore-to-camel-case后,user_name这样的数据库字段才能自动映射到userName实体属性,忘记配置会导致查出来的对象属性全是null,而且是那种很难定位的"部分为null"。

3. 数据库设计:健康数据表结构怎么建才经得起追问

3.1 用户域:基础信息与健康档案分开

user表存登录信息和基本人口学特征,health_profile表存健康档案,两者分开的原因是档案数据更新频率低,不适合跟频繁登录操作混在同一张表。user表结构大概是这样:

CREATE TABLE `user` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(255) NOT NULL, `nickname` VARCHAR(50), `gender` TINYINT COMMENT '1男 2女', `birthday` DATE, `age_group` VARCHAR(20) COMMENT 'young/middle/elderly', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

health_profile则存身高、初始体重、既往病史、是否有慢性病标志等。身高是后续BMI计算必需的字段,所以初始化用户档案时这个字段不能为空。年龄分组不建议每次查询时现算,因为生日变了年龄组也会变,维护起来麻烦,不如在用户注册或完善档案时一次性算好存进age_group字段,后续分析服务直接读取。

3.2 健康指标表:一张表打天下的取舍

前面提过用一张health_record表承载所有指标,这是毕设性价比最高的方案。实际建表时我会比常规方案多一个secondary_value字段,专门用来处理血压这种"两个值"的指标:

CREATE TABLE `health_record` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT NOT NULL, `record_type` VARCHAR(32) NOT NULL COMMENT 'weight/blood_pressure/heart_rate/blood_sugar/sleep_duration', `record_value` DECIMAL(8,2) NOT NULL COMMENT '主值,如体重kg、收缩压mmHg', `secondary_value` DECIMAL(8,2) COMMENT '辅助值,如舒张压mmHg', `unit` VARCHAR(16), `record_time` DATETIME NOT NULL COMMENT '测量时间', `note` VARCHAR(255), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_user_type_time` (`user_id`, `record_type`, `record_time`) );

为什么要加secondary_value而不直接把血压存成"120/80"字符串?因为字符串没法参与数学运算和比较,后续要判断"舒张压是否超过90"就得在Java里做字符串拆分,写起来很丑,也容易出错。拆成两个数字字段,所有判断都在SQL或简单比较里完成,代码干净得多。

联合索引idx_user_type_time是本表最重要的索引,因为绝大多数查询都是"某个用户、某个指标类型、按时间排序",这个索引能让这类查询走索引而不是全表扫描。别小看这一行,答辩时能解释出设计索引的理由,是很加分的。

3.3 干预域:计划、任务、预警三张表联动

CREATE TABLE `intervention_plan` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT NOT NULL, `plan_name` VARCHAR(100), `plan_type` VARCHAR(32) COMMENT 'weight_control/exercise/sleep', `goal_value` DECIMAL(8,2) COMMENT '目标值', `start_date` DATE, `end_date` DATE, `status` TINYINT COMMENT '0待执行 1进行中 2已完成 3已中止', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `intervention_task` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `plan_id` BIGINT NOT NULL, `task_name` VARCHAR(100), `task_type` VARCHAR(32) COMMENT 'exercise/diet/sleep', `task_date` DATE, `completed` TINYINT DEFAULT 0, `complete_time` DATETIME, `reward_points` INT DEFAULT 10 ); CREATE TABLE `health_alert` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT NOT NULL, `alert_type` VARCHAR(32) COMMENT 'high_bmi/high_blood_pressure', `alert_level` VARCHAR(16) COMMENT 'warning/danger', `alert_content` VARCHAR(255), `status` TINYINT DEFAULT 0 COMMENT '0未读 1已读', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP );

这三张表的分工很清晰:plan定义"要干什么、目标是什么、什么时间范围",task定义"每天具体做什么、做了没有",alert记录"系统发现了什么风险、用户读没读"。我强烈建议把task设计成按天一条记录,而不是一条task记录带一个日期范围,因为打卡动作天然是"当天一次"的,按天建模会让"今天有没有完成"这个查询变得非常简单,一个等值条件就出来了。

4. 核心功能模块的实现路线:从入库到出图再到出建议

4.1 指标录入接口:先校验再入库

用户每录入一条健康数据,系统要做三层检查:格式校验、业务校验、落库。格式校验用Controller层的@Validated注解和DTO上的校验注解就能搞定;业务校验要放到Service层,比如体重不可能为负数、心率不可能是0;落库时要注意记录时间和记录类型不能被前端随意传。

@RestController @RequestMapping("/api/health/record") public class HealthRecordController { @PostMapping public Result<Void> addRecord(@RequestBody @Valid HealthRecordDTO dto) { healthRecordService.addRecord(dto); return Result.success(); } }
@Service public class HealthRecordServiceImpl implements HealthRecordService { @Override public void addRecord(HealthRecordDTO dto) { // 1. 指标类型白名单校验 if (!RecordType.isValid(dto.getRecordType())) { throw new BizException("不支持的指标类型"); } // 2. 数值范围校验 RecordType type = RecordType.valueOf(dto.getRecordType()); if (dto.getRecordValue().compareTo(type.getMinValue()) < 0 || dto.getRecordValue().compareTo(type.getMaxValue()) > 0) { throw new BizException("数值超出合理范围"); } // 3. 记录时间不能晚于当前时间 if (dto.getRecordTime().isAfter(LocalDateTime.now())) { throw new BizException("记录时间不能晚于现在"); } // 4. 入库 HealthRecord record = new HealthRecord(); BeanUtils.copyProperties(dto, record); record.setUserId(UserContext.getUserId()); healthRecordMapper.insert(record); } }

这里用枚举集中管理每种指标的单位、合理范围、展示名称,后续分析规则、前端下拉列表都能复用,避免在多个地方散落魔法数字。比如RecordType枚举里定义weight的最小值是1、最大值是500,blood_pressure的最小值是30、最大值是300,写清楚之后,前端校验和后端校验可以共用一套规则,不会出现两边标准不一致的情况。

4.2 趋势查询接口:聚合逻辑要在SQL里做

给前端折线图的数据,核心查询是"某用户、某指标、某时间范围内的全部记录"。这个接口本身不复杂,但有一个性能习惯要养成:按日期聚合时,不要查出所有明细后在Java里做groupBy,而是在SQL里用DATE_FORMAT做分组。

@Override public List<TrendPointVO> getTrend(Long userId, String recordType, LocalDate start, LocalDate end) { return healthRecordMapper.selectTrend(userId, recordType, start, end); }

Mapper对应的XML大致如下:

<select id="selectTrend" resultType="com.example.health.vo.TrendPointVO"> SELECT DATE_FORMAT(record_time, '%Y-%m-%d') AS datePoint, AVG(record_value) AS avgValue, MAX(record_value) AS maxValue, MIN(record_value) AS minValue FROM health_record WHERE user_id = #{userId} AND record_type = #{recordType} AND record_time BETWEEN #{start} AND #{end} GROUP BY DATE_FORMAT(record_time, '%Y-%m-%d') ORDER BY datePoint </select>

同一天录了多条数据时,取平均值是一个稳妥的选择。如果你想让系统看起来更"懂行",可以在前端把"当日平均"和"当日波动范围"一起展示,ECharts用一个折线加面积图就出来了,视觉上立刻比单条折线图高一个档次。这个接口的数据结构设计好之后,后面做"健康周报"、"月度总结"都可以直接复用,只是把分组粒度从day改成week或month。

4.3 风险预警:录入后立即触发一轮分析

预警触发的时机有两种,一种是用户每次录完数据立刻触发,另一种是定时任务每天晚上统一扫描。我建议两者都做:录入时实时触发,让用户当场看到"系统提示您血压偏高"的反馈,体验感很强;定时任务做兜底,防止用户某天漏录入导致判断断层。

预警判断的逻辑用一个独立服务承载:

@Service public class RiskAlertService { public void evaluateAfterRecord(Long userId, String recordType, BigDecimal value) { RiskLevel level = judge(recordType, value); if (level != RiskLevel.NORMAL) { healthAlertMapper.insert(buildAlert(userId, recordType, level)); } } private RiskLevel judge(String recordType, BigDecimal value) { // 按类型查阈值表,返回正常/警告/危险 } }

这里有个小的设计选择:预警记录写入health_alert表之后,用户在前端的"消息中心"里能看到未读消息,看到后标记已读。这个"已读/未读"机制虽然简单,但让系统有了"主动通知用户"的能力,比干巴巴的列表生动很多。

4.4 干预计划的生成闭环

当用户的某个指标持续异常(比如最近7天BMI一直处于超重区间),系统应该主动推荐一份干预计划。生成逻辑分四步:先读取用户最近一段时间的指标记录,再做汇总判断(计算均值、变化斜率),然后匹配预置的干预模板,最后按模板生成计划及每天的task。

计划生成后,用户可以在前端确认或放弃。确认后,每天凌晨的定时任务根据计划生成当天task,用户在页面里打卡;打卡数据回写后,第二天生成任务时会同步读取昨天的完成率,并据此微调建议文案。这个"记录→分析→干预→反馈→再分析"的闭环,是整个系统"智能感"的来源。强烈建议答辩演示时完整走一遍这个流程,比讲十页PPT都管用。

@Component public class PlanGenerateService { public InterventionPlan generatePlan(Long userId, String reasonType) { // 1. 读取最近7天指标记录 List<HealthRecord> records = healthRecordMapper.selectRecent(userId, 7); // 2. 计算均值与趋势 AnalysisSummary summary = analysisService.summarize(records); // 3. 匹配干预模板 PlanTemplate template = templateService.match(reasonType, summary); // 4. 生成计划主记录和首日task InterventionPlan plan = new InterventionPlan(); plan.setUserId(userId); plan.setPlanName(template.getName()); plan.setPlanType(template.getType()); plan.setStartDate(LocalDate.now()); plan.setEndDate(LocalDate.now().plusDays(template.getDays())); plan.setStatus(0); interventionPlanMapper.insert(plan); generateDailyTask(plan, LocalDate.now()); return plan; } }

5. "智能"到底怎么实现:计算、分级与建议生成规则

5.1 基础指标计算:BMI与基础代谢率

BMI的计算很简单:体重(kg)除以身高(m)的平方。代码里要注意BigDecimal的精度处理,除法的精度位数要显式指定,否则会抛ArithmeticException:

public BigDecimal calcBmi(BigDecimal weightKg, BigDecimal heightCm) { BigDecimal heightM = heightCm.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); return weightKg.divide(heightM.multiply(heightM), 1, RoundingMode.HALF_UP); }

基础代谢率可以选用营养学中常用的Mifflin-St Jeor公式,男性和女性系数不同,这也是"个体化"的一个体现:

public BigDecimal calcBmr(BigDecimal weightKg, BigDecimal heightCm, Integer age, Integer gender) { BigDecimal base = BigDecimal.valueOf(10).multiply(weightKg) .add(BigDecimal.valueOf(6.25).multiply(heightCm)) .subtract(BigDecimal.valueOf(5).multiply(BigDecimal.valueOf(age))); return gender == 1 ? base.add(BigDecimal.valueOf(5)) : base.subtract(BigDecimal.valueOf(161)); }

BMR算出来之后可以结合用户设置的活动系数估算每日消耗热量,干预计划里的每日摄入目标就从这个值推导出来。这样生成的建议就不是拍脑袋的"建议少吃",而是"根据您的代谢水平,每日摄入建议控制在1800千卡左右",说服力完全不一样。

5.2 风险判断规则:阈值表的设计比算法重要

毕设阶段的"智能"不靠机器学习,靠的是临床指南里成熟的阈值分组。以血压为例,目前临床常用的分级大致如下:

分类收缩压舒张压
正常小于 120小于 80
正常高值120-13980-89
高血压大于等于 140大于等于 90

BMI的分组、空腹血糖的正常区间、静息心率的参考范围也是类似的思路。把这些阈值集中放到一个枚举或配置类里,而不是散落在if语句中。这样做的一个额外好处是,将来要支持不同年龄段的差异化标准,只需扩展阈值表的结构,业务代码不用大面积改。

具体判断时用声明的区间做比较,避免出现"边界值被两边同时命中"的问题。写区间判断代码时,一定要明确规定每个区间的开闭。我的习惯是用枚举实现,并且采用"下含上不含"的原则:

public enum BmiLevel { UNDERWEIGHT("偏瘦", BigDecimal.ZERO, BigDecimal.valueOf(18.5)), NORMAL("正常", BigDecimal.valueOf(18.5), BigDecimal.valueOf(24)), OVERWEIGHT("超重", BigDecimal.valueOf(24), BigDecimal.valueOf(28)), OBESE("肥胖", BigDecimal.valueOf(28), null); private final String label; private final BigDecimal low; private final BigDecimal high; public static BmiLevel of(BigDecimal bmi) { for (BmiLevel level : values()) { boolean geLow = level.low == null || bmi.compareTo(level.low) >= 0; boolean ltHigh = level.high == null || bmi.compareTo(level.high) < 0; if (geLow && ltHigh) { return level; } } return NORMAL; } }

这里用的是"下含上不含"(左闭右开)原则:BMI 18.5归为正常起始,24.0归为超重起始,28.0归为肥胖起始,不会出现18.5既算偏瘦又算正常的bug。这个边界问题的处理细节,面试官和评委都很爱问,提前想清楚能答得很漂亮。

5.3 趋势判断:用最小二乘法做简单预测

"最近一周体重在上升"这种结论,最好由程序算出来,而不是靠肉眼看折线图猜。常见的做法是拿最近N天的数据做线性回归,用斜率判断趋势方向。最小二乘法拟合直线的斜率公式:

k = (n·Σxy - Σx·Σy) / (n·Σx² - (Σx)²)

代码实现也不复杂:

public BigDecimal calcTrendSlope(List<TrendPoint> points) { int n = points.size(); if (n < 3) { return BigDecimal.ZERO; // 数据太少不判断趋势 } BigDecimal sumX = BigDecimal.ZERO, sumY = BigDecimal.ZERO; BigDecimal sumXY = BigDecimal.ZERO, sumXX = BigDecimal.ZERO; for (int i = 0; i < n; i++) { BigDecimal x = BigDecimal.valueOf(i); BigDecimal y = points.get(i).getValue(); sumX = sumX.add(x); sumY = sumY.add(y); sumXY = sumXY.add(x.multiply(y)); sumXX = sumXX.add(x.multiply(x)); } BigDecimal numerator = BigDecimal.valueOf(n).multiply(sumXY).subtract(sumX.multiply(sumY)); BigDecimal denominator = BigDecimal.valueOf(n).multiply(sumXX).subtract(sumX.multiply(sumX)); if (denominator.compareTo(BigDecimal.ZERO) == 0) { return BigDecimal.ZERO; } return numerator.divide(denominator, 4, RoundingMode.HALF_UP); }

斜率大于0说明总体上升,小于0说明总体下降,绝对值大小反映变化剧烈程度。数据点少于3个时不判断趋势,这是一个很合理的业务约定。基于斜率定性地给出"体重呈上升趋势"的提示,比单纯看"今天比昨天重了0.5kg"科学得多,因为短期波动会被平滑掉。

5.4 建议文案的生成:模板加数据填充

干预建议不需要接入AI大模型,用模板加数据填充就够用了。实现上可以做一张建议规则表:每条规则包含"触发条件"和"建议文案模板"两个字段,触发条件满足时把用户数据填入模板。

比如BMI为"超重"时,建议文案可能是:"根据您最近的BMI(24.5)评估,体重处于超重区间。建议您在接下来4周内,每周安排3次30分钟的中等强度有氧运动,同时将每日摄入控制在1800千卡左右。系统已为您生成减重计划,您可以在'我的计划'中查看。"看起来像是"算出来的建议",实际就是模板加几个数字,但用户体验和答辩演示效果都很好。

关键是要把思路讲清楚:规则引擎在这个阶段不需要引入重量级框架,用一个策略模式或者Map<触发条件, 建议模板>就能实现。这个取舍在答辩时是加分项——说明你知道什么是合适的复杂度,没有过度设计。

6. 避坑指南与答辩准备:经历过才知道的细节

6.1 我见过翻车最多的五个地方

第一,时区问题。LocalDateTime和MySQL的datetime对接不上、JSON序列化后前端时间差8小时,根源都在serverTimezone和Jackson配置。排查思路是先确认数据库时间,再在接口返回里打印时间字段,逐段定位。这个问题在体检项目里尤其隐蔽,因为健康数据全是时间序列,错了就是全错。

第二,BigDecimal精度。体重、身高、BMI这些字段如果数据库里用double或float,会出现0.1加0.2不等于0.3的问题。所有涉及测量值的字段统一用DECIMAL,Java侧用BigDecimal,比较用compareTo而不是equals。不要嫌麻烦,这一步能省下后面大量的排查时间。

第三,JSON循环引用。如果实体类里配了关联关系(如User关联List,而HealthRecord又关联User),Jackson序列化时会出现无限递归,直接栈溢出。解决方法是给其中一边加忽略标注,或者干脆在VO层做字段裁剪,不要直接把实体返回给前端。我个人更推荐后者,接口返回什么字段由VO控制,安全又灵活。

第四,MyBatis-Plus分页不生效。很多人装了分页插件但忘了配置PaginationInnerInterceptor,导致page查询返回全量数据。最直接的检查方法:看SQL日志里有没有LIMIT。没有LIMIT就说明插件没生效,回去检查MybatisPlusConfig里的拦截器注册。

第五,跨域问题。前端跑在5173端口,后端跑在8080端口,典型的CORS问题。SpringBoot里加一个全局CorsConfig,放行指定来源和请求头,别在Controller上逐个加注解。至于前端联调时的请求地址,建议写在一个配置文件里,方便打包部署时切换。

6.2 答辩时怎么把"智能"讲清楚

答辩时最怕被问"你这个智能体现在哪里",这个问题答得好不好直接决定分数。我的建议是准备一个一条龙演示脚本:先用一个演示账号录入几天不同趋势的数据(比如BMI从正常到偏高的变化),然后打开趋势分析页展示折线图,接着让系统生成干预计划,再去"我的计划"里把今天的任务打卡,最后展示打卡后系统给出的新一轮反馈。全程3到5分钟,零口误,评委看完自然就理解这个项目的闭环结构了。

如果评委追问算法层面,可以坦率回答:本项目的智能分析采用基于临床分级标准的规则判断和基于最小二乘法的趋势预测,不依赖大规模训练数据,在个人健康管理场景下,规则方案的可解释性和可维护性更强。这句话既承认了技术边界,又给出了合理的选型理由,很加分。千万不要硬吹自己做了深度学习,评委追问几个细节就露馅。

6.3 提升完成度的小技巧

最后分享几个能显著提升完成度的细节。一是预置演示数据,项目启动时通过初始化脚本生成一个演示账号和两三周的模拟健康记录,免得答辩现场临时手输数据,不仅慢而且显示不出趋势效果。二是把提醒做成定时任务,用Spring的@Scheduled注解每天定时检查是否有未完成任务,有则生成站内提示。这个功能看起来小,但非常能体现工程意识,因为定时任务、任务调度是评委眼中"企业级"的标配。三是增加一个"健康周报"页面,每周日自动汇总本周指标均值、趋势和完成率,形式上很像真实产品,也是很好的收尾功能。可以再加一个简单的数据导出,把趋势数据导出成CSV,虽然只有十几行代码,但给评委的"完成度"印象是实打实的。

这些功能都不难,但组合在一起,你的系统就从"实现了基本功能"变成了"完成了完整产品闭环"。我在实际带项目时见过最可惜的情况,就是同学代码写得挺好,但演示顺序混乱、数据一塌糊涂,最后被评委误判成半成品。所以别忘了:毕业设计的分数一半在实现,一半在呈现。把闭环跑通、把故事讲顺、把数据备好,这个题目拿到高分的把握就非常大了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进