这个题目我太熟了。每年毕设季总有一批同学来问“基于Spring Boot的xx系统”怎么做而“智能推荐卫生健康系统”是其中问得最多的一类。选这个题的人多半是看中了“Spring Boot 推荐算法”两个标签——既能体现工程能力又能带一点算法含量答辩还好讲。但真正动手你会发现推荐算法和业务系统怎么结合才是这道题最磨人的地方。这个系统本质上解决的是一个很具体的问题卫健委、社区医院或健康管理平台里资讯、食谱、运动方案越来越多用户根本翻不过来需要系统根据每个人的健康档案和行为习惯把最适合的内容推送到首页。做得好的话这套逻辑可以直接扩展成健康管理App的推荐服务。这篇文章我从选题、架构、算法、建库、接口到踩坑完整过一遍适合正在做同类毕设、或者想快速上手Spring Boot项目的同学直接参考。1. 系统整体设计与开发思路1.1 为什么会选这个题目先说选题逻辑。一个能被答辩老师认可的毕设题目通常要满足三个条件第一技术栈主流不说多新但必须是行业里正在用的第二业务场景清楚能讲明白“解决了什么问题”第三有一定发挥空间不能是纯增删改查。“智能推荐卫生健康系统”恰好三点全占。Spring Boot是目前Java后端的事实标准出去找工作面试问的也是这套卫生健康领域有明确的目标用户和痛点推荐算法可以浅做也可以深做实在写不出复杂模型一个协同过滤也能讲出完整的推荐链路。说白了这是一个“进可攻退可守”的题目普通的写法和进阶的写法都能落地。我见过的做法里有人只做健康档案管理和资讯浏览推荐只是一个摆设答辩时被老师问“推荐逻辑在哪”就卡住了。也有人认真做了物品协同过滤配合健康档案冷启动效果和论文深度都明显高一个档次。既然源码都拿出来了我建议你至少把推荐这条链路跑通。1.2 功能模块划分与业务流转这系统拆开来看主要分前台用户端和后台管理端两块。前台用户端面向普通用户功能可以这样规划用户注册登录以及个人中心的基础信息维护健康档案填写身高、体重、年龄、既往病史、过敏史、血压血糖等健康资讯浏览文章列表、分类筛选、关键词搜索、文章详情个性化推荐首页推荐内容、猜你喜欢、相似推荐健康指标记录手动录入血压、心率、体重、血糖等数据系统生成趋势饮食与运动方案展示按用户健康档案匹配后台管理端则是管理员的操作入口用户管理查看用户列表、禁用/启用账号健康资讯管理发布、编辑、下架文章健康档案审核与统计推荐参数配置比如推荐条数、相似度阈值基础数据看板用户数、文章数、行为数据量模块拆完业务流转也要理清楚。用户注册登录以后第一步是引导填写健康档案这一步非常关键因为后续的冷启动推荐全靠这份档案来匹配内容标签。用户开始浏览资讯每看一篇文章、收藏一条内容系统都会记录一条行为数据。行为数据攒到一定量推荐算法就开始工作用协同过滤找出相似物品再从物品池里挑出用户可能感兴趣的内容最终生成推荐列表返回到首页。整个链路就是用户注册 → 完善档案 → 产生行为 → 行为入库 → 离线计算偏好 → 生成个性化推荐。把这段话记住答辩的时候能顺下来比背代码有用得多。1.3 技术选型说明技术选型这块我直接给出一套经过验证的组合能少踩很多坑。后端就用Spring Boot版本选2.7.x就行了。别一上来追新上3.x3.x基于Jakarta命名空间很多老教程和依赖配置对不上毕设本来就赶时间犯不上和版本问题死磕。ORM框架我建议用MyBatis Plus而不是JPA原因是MyBatis Plus的代码生成器、分页插件、条件构造器写起来非常快国内公司用MyBatis的也更多答辩说出去也接地气。数据库选MySQL 8.0是标配。JDK用8或11这俩版本跑Spring Boot 2.7都没问题。开发工具就用IDEA社区版免费版完全够用。前端部分不必单独搞前后端分离的Vue项目除非你本身对前端很熟否则直接用Thymeleaf模板引擎加Bootstrap或Layui把页面渲染出来代码量少很多部署也更省事。如果你确实想用Vue做前后端分离那就要处理好跨域这部分后面我会讲到。辅助工具类库推荐三个Lombok省掉getter/setterHutool提供各种工具方法日期、加密、字符串处理MyBatis Plus Generator用来生成实体和Mapper层代码。这三个组合起来能省掉至少三分之一的无聊代码。2. 推荐算法的选型与核心实现2.1 三种主流推荐算法在毕设里的取舍推荐算法这个部分是整个系统的点睛之笔也是很多同学卡住的地方。其实在毕设场景里主流选择就三种基于内容的推荐、协同过滤、混合推荐。基于内容的推荐逻辑最简单看用户A浏览过的资讯都带什么标签然后推荐同样标签的其他资讯。优点是冷启动问题好解决用户只要有行为立刻能推荐缺点是推荐结果太“死板”翻来覆去都是同一个主题个性化程度不高。在健康领域里“内容相似”本身是很有意义的比如高血压用户看了一篇《高血压饮食指南》再推一篇《高血压运动注意事项》都是围绕“高血压”这个主题听起来就很合理。协同过滤又分基于用户的UserCF和基于物品的ItemCF。UserCF的思路是“找和你相似的人把他们喜欢的内容推荐给你”ItemCF则是“找和你看过的内容相似的内容推荐给你”。在毕设这种数据量级下我会优先推荐ItemCF原因是健康资讯的数量通常远小于用户数量资讯之间的相似度矩阵计算量小而且资讯是相对稳定的物品相似度矩阵可以离线算好存起来在线请求的时候直接查结果速度快技术上也更好解释。至于混合推荐就是把上面两种方法结合用基于内容的匹配解决冷启动用协同过滤解决个性化最后按比例加权合并或者用规则切换。论文里写混合推荐是加分项因为它解决了一个真实存在的问题——新用户没有行为数据时协同过滤是无法工作的。实操中用“规则”做混合就是非常简单有效的方式。2.2 相似度计算与协同过滤代码实现相似度计算是协同过滤的核心。最常见的做法是余弦相似度。拿资讯推荐举例我们把每个用户的浏览行为抽象成一个向量向量的维度是资讯数量某个用户浏览过第i篇文章第i个分量就是1否则为0。两篇资讯之间的相似度就用“同时浏览过这两篇资讯的用户数”除以各自的“用户向量模长”的乘积来算。数学公式是长这样的similarity(itemA, itemB) count(usersWhoViewedBoth) / (sqrt(usersWhoViewedA) * sqrt(usersWhoViewedB))这个公式其实就是余弦相似度在0/1向量上的等价形式。为什么不用皮尔逊系数或者杰卡德系数余弦相似度在稀疏行为数据下表现稳定计算直观代码只要十几行答辩时你能把它解释清楚已经超过一半人了。核心的推荐模块我用Java实现过一版思路是分三步public class RecommendService { private final ArticleMapper articleMapper; private final UserBehaviorMapper behaviorMapper; private final MapInteger, MapInteger, Double similarityCache new ConcurrentHashMap(); // 1. 构建“用户-资讯”行为矩阵 public MapInteger, SetInteger buildUserItemMatrix() { ListUserBehavior behaviors behaviorMapper.selectList(null); MapInteger, SetInteger userItemMatrix new HashMap(); for (UserBehavior behavior : behaviors) { userItemMatrix .computeIfAbsent(behavior.getUserId(), k - new HashSet()) .add(behavior.getArticleId()); } return userItemMatrix; } // 2. 根据行为矩阵计算资讯间余弦相似度 public void computeItemSimilarity() { MapInteger, SetInteger userItemMatrix buildUserItemMatrix(); ListInteger articleIds articleMapper.selectAllIds(); int n articleIds.size(); for (int i 0; i n; i) { for (int j i 1; j n; j) { int itemA articleIds.get(i); int itemB articleIds.get(j); double similarity calcCosineSimilarity(itemA, itemB, userItemMatrix); // 只保留相似度较高的降低内存占用 if (similarity 0.1) { similarityCache.computeIfAbsent(itemA, k - new HashMap()).put(itemB, similarity); similarityCache.computeIfAbsent(itemB, k - new HashMap()).put(itemA, similarity); } } } } // 3. 为目标用户生成推荐列表 public ListArticle recommendForUser(Integer userId) { MapInteger, SetInteger userItemMatrix buildUserItemMatrix(); SetInteger viewedItems userItemMatrix.getOrDefault(userId, Collections.emptySet()); MapInteger, Double scoreMap new HashMap(); // 遍历用户看过的东西 for (Integer viewedId : viewedItems) { MapInteger, Double simMap similarityCache.getOrDefault(viewedId, Collections.emptyMap()); for (Map.EntryInteger, Double entry : simMap.entrySet()) { Integer candidateId entry.getKey(); if (viewedItems.contains(candidateId)) { continue; // 看过的就不再推荐 } scoreMap.merge(candidateId, entry.getValue(), Double::sum); } } ListInteger candidateIds scoreMap.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(10) .map(Map.Entry::getKey) .collect(Collectors.toList()); if (candidateIds.isEmpty()) { return Collections.emptyList(); } return articleMapper.selectBatchIds(candidateIds); } }这一步有个关键细节在线计算时把用户看过的物品集合和候选物品集合算交集看过的直接过滤掉避免推荐冗余内容。排序时用分值累加的方式聚合最终取Top N。这套逻辑性能足够应对几千用户和几百篇文章的小型系统。如果你希望代码更严谨可以提前把相似度矩阵算完存到Redis或MySQL表里启动时加载到内存缓存避免每次请求都重新计算。对毕设来说用ConcurrentHashMap做进程内缓存已经够用Redis属于锦上添花有时间再加。2.3 解决冷启动健康档案与内容标签的匹配冷启动是推荐领域的老大难也是答辩老师最喜欢追问的点。好在卫生健康系统有个天然优势每个用户注册后都会填写健康档案档案里的字段就是最好的个性化特征。我给系统设计了这样一套规则每篇健康资讯在上架时打上标签标签从“慢性病类型”“饮食主题”“运动类型”“人群分类”这几个维度取比如“高血压”“低盐饮食”“有氧运动”“老年人”。用户填完健康档案后系统把档案字段映射成同样的标签集合然后计算用户标签和资讯标签的重合度。重合度数越高意味着这篇资讯对用户越相关。举个例子某用户档案里填了“高血压”和“超重”那系统会优先推荐文章库中带“高血压”和“减重”标签的内容。等用户产生浏览、点赞、收藏等行为之后协同过滤逐渐接管推荐权重冷启动期自动结束。public ListArticle coldStartRecommend(Integer userId) { HealthProfile profile healthProfileMapper.selectByUserId(userId); // 将用户档案关键词与资讯标签做交集匹配 ListString userTags analyzeProfileTags(profile); if (userTags.isEmpty()) { return articleMapper.selectHotArticles(10); // 兜底返回热门文章 } QueryWrapperArticle wrapper new QueryWrapper(); for (String tag : userTags) { wrapper.like(tags, tag).or(); } wrapper.last(limit 10); return articleMapper.selectList(wrapper); }注意看这个兜底逻辑如果用户档案为空或者一个标签都匹配不上就返回热度最高的文章。这个设计在真实系统里叫“热门推荐兜底”属于非常成熟的降级方案保证任何时候首页都有内容可展示不会一片空白。答辩的时候这一块话术我给你准备好先用健康档案做基于规则的冷启动积累到一定行为量级后再切换为协同过滤实现从冷启动到个性化推荐的平滑过渡。这句话既展示了算法理解又展示了你考虑了工程落地中的真实问题。3. 数据库设计与核心接口实现3.1 核心表结构设计数据库设计是整个系统的地基设计得好后面写代码会非常顺畅。表结构我建议按这个思路来用户域、档案域、内容域、行为域、推荐域。五个域各自独立又通过外键关联清晰、可扩展。给你们整理了一份核心表清单表名所属域关键字段说明sys_user用户域id, username, password, gender, age, phone用户基本信息health_profile档案域id, user_id, height, weight, blood_pressure, blood_sugar, disease_history与sys_user一对一health_article内容域id, title, summary, content, tags, category, view_count, status健康资讯内容user_behavior行为域id, user_id, article_id, behavior_type, create_time记录浏览/收藏/点赞health_indicator档案域id, user_id, indicator_type, indicator_value, record_time血压、心率等指标记录recommend_result推荐域id, user_id, article_id, score, create_time存储推荐结果可后续分析这里重点说一下user_behavior表它的behavior_type字段我推荐用字符串而不是数字取值包括view、collect、like、share这样代码可读性更好排查问题时一眼能看懂。create_time和user_id一定要建联合索引因为推荐算法和用户行为分析都要按这个维度去查数据。数据量不大时建表可以不那么讲究但索引这个动作建议养成习惯。health_profile表里disease_history这种字段建议用逗号分隔的字符串存储多个值比如“高血压,糖尿病”查询时用FIND_IN_SET或者LIKE都能匹配。如果你用MyBatis Plus实体类里就直接定义一个String字段存取都非常方便比单独建一张疾病关联表更适合毕设的体量。3.2 后端核心接口实现后端接口这块我挑了几个最核心的来讲登录、健康档案保存、行为上报和推荐查询。登录注册用JWT方案比较多Spring Boot里引入jjwt依赖登录成功后签发一个token前端后续请求头带上token后端通过拦截器解析。如果项目时间紧张用简单的Session方案也完全可以毕竟毕设的重点在推荐系统本身不要上来就碰Spring Security那么重的东西很容易陷入配置地狱。健康档案的保存接口用一个简单的Controller实现RestController RequestMapping(/api/profile) public class HealthProfileController { Autowired private HealthProfileService profileService; PostMapping(/save) public Result saveProfile(RequestBody HealthProfile profile) { // 简单的参数校验 if (profile.getUserId() null) { return Result.error(用户ID不能为空); } boolean saved profileService.saveOrUpdate(profile); return saved ? Result.success() : Result.error(保存失败); } }行为上报接口是整个推荐系统的数据来源逻辑很简单但非常关键PostMapping(/behavior) public Result recordBehavior(RequestBody UserBehavior behavior) { behavior.setCreateTime(new Date()); behaviorMapper.insert(behavior); // 异步触发相似度矩阵的增量更新简化处理 recommendService.rebuildCacheIfNecessary(); return Result.success(); }真正的项目里会把这部分做成异步消息但对毕设而言插入行为数据后触发一次缓存刷新已经足够。这里有个思路要讲清楚推荐系统的质量高度依赖行为数据的完整性所以前端点击文章详情时一定要上报view行为收藏按钮上报collect行为这些数据越全推荐越准。推荐查询接口是最后一步把推荐结果返回前端GetMapping(/recommend) public Result getRecommendList(RequestParam Integer userId) { ListArticle articles recommendService.recommendForUser(userId); if (articles.isEmpty()) { articles recommendService.coldStartRecommend(userId); } return Result.success(articles); }注意这个接口里的降级顺序先尝试协同过滤拿不到结果就切到冷启动规则还是拿不到就返回热门兜底。三层逻辑层层递进保证接口永远有数据返回。这段代码你在答辩时可以大大方方讲真实系统里也是这样设计的。3.3 前端页面与接口联调前端我推荐直接用Thymeleaf加Bootstrap原因前面提过就是省事。页面结构大概这样首页展示推荐资讯列表健康档案页是一个表单页资讯详情页带上报行为的JS代码个人中心展示历史记录。如果用ThymeleafController里返回视图GetMapping(/home) public String home(Model model, RequestParam Integer userId) { ListArticle recommendList recommendService.recommendForUser(userId); model.addAttribute(recommendList, recommendList); return home; }页面里用Thymeleaf语法遍历列表加上Bootstrap的卡片样式展示效果就不会太差。如果你想在前端独立调后端接口也可以用Vue加Axios但那样要处理跨域。Spring Boot里解决跨域非常简单写一个配置类Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE); } }; } }这部分我特别提醒一下前后端分离的项目十个里有五个是挂在跨域上。你本地前端跑5173端口后端跑8080端口端口不同就必然跨域要么配CorsConfig要么在Controller上加CrossOrigin注解。别等到浏览器控制台报错才想起这回事。4. 实操过程中最容易踩的坑4.1 项目初始化和环境配置的坑先讲一个我当年自己踩过的坑。Spring Boot版本不对会导致一堆依赖死活引不进来。有同学用了Spring Boot 3.2然后照着一个旧教程引入MyBatis Plus结果启动直接报错翻来覆去查了一天才发现是版本兼容问题。MyBatis Plus对Spring Boot 3的适配和Spring Boot 2完全不同所以还是那句话毕设老老实实选Spring Boot 2.7.x教程多依赖兼容性好网上的资料随便搜都能用。再说配置文件的坑。application.yml里最核心的三处配置要写对spring: datasource: url: jdbc:mysql://localhost:3306/health_recommend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: trueurl里的serverTimezoneAsia/Shanghai非常重要不写的话MySQL 8.0经常报时区错误。编码一定用utf8不然存中文信息进去全变问号。这些都是一行配置的问题但每一个都能让你调试半小时起步。Lombok也是一个经典坑源。IDEA里必须安装Lombok插件同时pom.xml里引入依赖否则编译直接报“找不到getter/setter”。如果你用的JDK 11以上的版本Lombok版本太老也会出问题。稳妥的组合是JDK 8加Lombok 1.18.20以上。4.2 推荐模块的Bug现场推荐模块最容易出的问题有三个我都排一下方便你直接对照自己的项目。第一个是用户没有行为数据时协同过滤返回空列表。这个就是我前面讲的冷启动问题解决办法就是调用推荐接口前先判断列表是否为空空就走规则推荐。这个逻辑一定要放在Service层处理别让Controller层去判断保持各层职责清晰。第二个是行为数据重复上报导致计算偏差。用户重复刷新一个页面前端可能连续上报三四条view行为如果不做去重这个物品的权重就会被异常抬高。解决方案是前端在每次会话里只上报一次同一文章的view或者后端在一个时间窗口内只接受一次重复行为。做毕设的话后端判断一下同一用户同一文章短时间内是否重复上报简单可靠。第三个是缓存导致推荐结果不更新。我刚写完推荐模块时明明新增了行为数据推荐结果却跟之前一模一样。排查半天发现是相似度矩阵只在项目启动时计算了一次。解决方式也很简单每次写入行为数据后把相似度矩阵重新计算一遍或者设定一个定时任务每小时重算一次。对毕设体量来说直接在行为上报接口里触发重新计算完全够用代码就一行调用副作用可以忽略。4.3 答辩高频问题整理答辩环节老师们对推荐系统往往格外“照顾”因为推荐在他们眼里属于有故事可讲的部分。我整理了三个高频问题和参考回答思路。第一个问题为什么选协同过滤而不是其他推荐模型回答思路是毕设系统面向健康资讯推荐场景数据规模有限协同过滤实现简单、可解释性强无需大量标注数据。深度学习模型更适合海量数据和复杂特征场景但在小规模系统里收益不明显。这番话既回答了选择原因又展示了你了解更广阔的推荐技术方向。第二个问题冷启动问题怎么解决回答思路系统先让用户填写健康档案将档案字段映射为内容标签运用基于内容的规则推荐完成冷启动。用户产生足够多行为后系统自动切换为协同过滤实现个性化推荐的平滑过渡。这段内容在2.3已经讲过这里再背一遍就是。第三个问题推荐效果怎么评估如果你系统里已经预留了行为数据就答以用户点击率作为效果指标系统上线后持续记录推荐位文章的点击行为对比推荐位点击率和全局点击率评估个性化推荐的提升效果。如果时间允许还可以做一个简单的离线实验把行为数据按时间分成训练集和测试集计算精确率或者召回率。哪怕没有真实用户你也要把这个指标体系说清楚证明你的设计不是拍脑袋。4.4 源码使用建议与后续扩展方向拿到这套源码之后我不建议照搬提交。既然学校要求“基于Spring Boot的智能推荐卫生健康系统”而且明确写了“智能推荐”那推荐就得做出真实存在感。至少要做到首页确实能根据档案和行为给出不同的推荐结果管理后台能对资讯进行管理并配置标签。后面想扩展的话方向也很多增加每日饮食推荐模块结合用户档案中的忌口和慢性病情况按天生成食谱计划增加运动方案推荐根据健康档案中的身高体重和运动习惯匹配合适的运动计划用ECharts展示用户健康指标的趋势图血压、血糖、心率的变化一目了然把推荐结果存入recommend_result表积累数据后分析推荐位的点击转化率形成完整的评估闭环有条件的话部署到云服务器用Nginx做反向代理域名访问答辩现场直接展示线上效果这些方向都属于在原项目基础上的合理延展工作量可控但呈现出来的完整度会高很多。我个人在实际操作中最强烈的感受是推荐系统的精髓不在算法多高深而在“数据—算法—业务”这条链路是否闭环。论文里写完算法公式代码里必须让推荐结果真实可见、可验证答辩现场演示三分钟胜过你背十页稿子。最后再分享一个很实用的建议做任何毕设系统先把基础CRUD跑通再把推荐链路接上最后才考虑界面和细节打磨。顺序不要搞反先有核心功能再谈锦上添花。