
简介本资源为基于Spring Boot的智能推荐卫生健康系统完整开发资料包面向计算机专业学生、Java后端初学者及需要课程设计或毕业设计参考的开发者帮助解决健康类Web系统从选题到落地的全流程问题。包内共867个文件涵盖154个Java源码、54个Vue组件、153个JavaScript脚本、52个HTML页面及44个CSS样式文件另有SQL建表脚本、docx论文与任务书、开题报告等文档压缩包约16.61MB结构清晰便于按模块查阅。系统围绕智能推荐、在线咨询、健康论坛管理、科室类型管理等核心功能展开管理员模块包含用户管理、医生信息管理与论坛管理用户模块则覆盖我的发布、我的收藏与在线咨询配套论文完整呈现需求分析、数据库设计、系统测试等章节。目前已有41人学习下载适合需要完整赛题方案、可运行源码与规范文档的读者参考借鉴。1. 卫生健康系统里塞进智能推荐这套 SpringBoot 方案到底解决谁的痛点做医疗健康类系统的同行大概率都遇到过同一个尴尬后台数据表越建越多居民健康档案、体检记录、慢病随访、饮食运动日志全都有但前端页面永远只有「查询」和「列表」。用户登录进来看到的是一堆和自己无关的指标用两次就再也不打开了。这套基于 SpringBoot 的智能推荐卫生健康系统要解决的就是这个「数据多、用得少」的问题——把推荐算法嵌进健康管理流程让系统主动告诉用户「你该关注什么」。它适合三类人一是做毕设或课程设计、需要一套完整可跑通项目的学生二是中小型健康管理平台的后端开发想给现有系统加一层推荐能力三是想搞清楚「推荐算法怎么和 SpringBoot 业务系统结合」的工程师。整套方案的技术底座是 SpringBoot 做服务端、Vue 做前端、MySQL 存业务数据推荐部分用协同过滤加规则引擎的混合策略不依赖重型大数据组件单机就能跑起来。2. 智能推荐卫生健康系统的技术选型为什么是 SpringBoot 加协同过滤2.1 业务场景决定了推荐策略不能照搬电商电商推荐的核心目标是转化率用户点得越多、买得越多越好。卫生健康系统的目标完全不同它要的是健康干预的依从性——用户愿不愿意按推荐去测血压、去运动、去复诊。这个差异直接决定了算法选型。电商场景下ItemCF基于物品的协同过滤几乎是默认选择因为商品数量大、用户行为密集。但健康系统里用户量可能只有几百到几千行为数据稀疏得可怜一个用户一个月可能就产生十几条记录。这种情况下 ItemCF 算出来的相似度矩阵噪声极大推荐结果经常是「隔壁老王看了这个你也看看」——毫无医学逻辑。我一般会采用「协同过滤 规则引擎」的混合方案。协同过滤负责发现行为层面的相似性规则引擎负责兜底医学常识。比如一个 60 岁高血压用户协同过滤可能推荐「减盐饮食」规则引擎则强制保证「血压监测提醒」永远排在推荐列表前三位。两者加权融合既有个性化又不会出现「给糖尿病人推荐高糖食谱」这种致命错误。2.2 SpringBoot 在健康系统里的角色定位SpringBoot 在这套系统里不只是个 Web 框架它承担了三个关键职责第一作为推荐服务的调度中枢。用户请求进来后Controller 层拿到用户 ID 和当前场景比如「首页推荐」「饮食推荐」「运动推荐」调用 RecommendationServiceService 内部再决定走协同过滤还是规则引擎或者两者都走然后合并结果。第二作为定时任务的载体。推荐系统不能每次请求都实时算一遍全量相似度那样响应时间会爆炸。常见做法是用 SpringBoot 的Scheduled定时任务每天凌晨低峰期预计算用户相似度矩阵和推荐候选集写入 Redis 或本地缓存白天请求直接读缓存。第三作为数据管道的入口。健康系统的数据来源杂——有用户手动录入的有设备上传的有体检机构导入的。SpringBoot 的 Controller 层统一接收后通过 Service 层做清洗和归一化再写入 MySQL。推荐算法读的是清洗后的数据不是原始脏数据。2.3 最小可跑通的推荐模块代码结构下面是一个可落地的推荐服务核心代码骨架基于 SpringBoot 标准分层// RecommendationService.java Service public class RecommendationService { Autowired private UserBehaviorMapper behaviorMapper; // 用户行为数据访问 Autowired private RedisTemplateString, Object redisTemplate; // 缓存推荐结果 private static final String RECOMMEND_CACHE_KEY recommend:user:%d:scene:%s; /** * 获取推荐列表 * param userId 用户ID * param scene 推荐场景home/diet/exercise * param topN 返回条数 */ public ListRecommendItem getRecommendations(Long userId, String scene, int topN) { // 1. 先查缓存 String cacheKey String.format(RECOMMEND_CACHE_KEY, userId, scene); ListRecommendItem cached (ListRecommendItem) redisTemplate.opsForValue().get(cacheKey); if (cached ! null !cached.isEmpty()) { return cached.subList(0, Math.min(topN, cached.size())); } // 2. 缓存未命中走混合推荐 ListRecommendItem cfResult collaborativeFiltering(userId, scene); ListRecommendItem ruleResult ruleEngine(userId, scene); // 3. 加权融合协同过滤权重0.6规则引擎权重0.4 ListRecommendItem merged mergeWithWeight(cfResult, ruleResult, 0.6, 0.4); // 4. 写回缓存过期时间2小时 redisTemplate.opsForValue().set(cacheKey, merged, 2, TimeUnit.HOURS); return merged.subList(0, Math.min(topN, merged.size())); } /** * 协同过滤基于用户的健康行为相似度 * 行为包括浏览健康文章、记录饮食、完成运动、测量血压等 */ private ListRecommendItem collaborativeFiltering(Long userId, String scene) { // 获取目标用户的行为向量 MapString, Double targetVector behaviorMapper.getUserBehaviorVector(userId, scene); if (targetVector.isEmpty()) { return Collections.emptyList(); } // 获取所有其他用户的行为向量实际项目应做分页或采样 ListUserBehavior allUsers behaviorMapper.getAllUserBehaviors(scene); // 计算余弦相似度取Top20相似用户 ListSimilarUser similarUsers new ArrayList(); for (UserBehavior other : allUsers) { if (other.getUserId().equals(userId)) continue; double similarity cosineSimilarity(targetVector, other.getVector()); if (similarity 0.3) { // 相似度阈值低于0.3认为不相关 similarUsers.add(new SimilarUser(other.getUserId(), similarity)); } } similarUsers.sort((a, b) - Double.compare(b.getSimilarity(), a.getSimilarity())); // 取前20个相似用户的行为加权汇总作为推荐候选 MapString, Double candidateScores new HashMap(); for (int i 0; i Math.min(20, similarUsers.size()); i) { SimilarUser su similarUsers.get(i); MapString, Double behaviors behaviorMapper.getUserBehaviorVector(su.getUserId(), scene); for (Map.EntryString, Double entry : behaviors.entrySet()) { candidateScores.merge(entry.getKey(), entry.getValue() * su.getSimilarity(), Double::sum); } } // 排除目标用户已经做过的行为 candidateScores.keySet().removeAll(targetVector.keySet()); // 按分数排序返回 return candidateScores.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(50) .map(e - new RecommendItem(e.getKey(), e.getValue())) .collect(Collectors.toList()); } /** * 规则引擎基于医学常识和用户画像的硬规则 * 这部分保证推荐结果不会违反健康常识 */ private ListRecommendItem ruleEngine(Long userId, String scene) { UserProfile profile behaviorMapper.getUserProfile(userId); ListRecommendItem rules new ArrayList(); // 规则1高血压用户血压监测提醒永远置顶 if (profile.getDiseases().contains(hypertension)) { rules.add(new RecommendItem(blood_pressure_monitor, 100.0)); rules.add(new RecommendItem(low_salt_diet, 80.0)); } // 规则2糖尿病用户血糖监测和低GI饮食 if (profile.getDiseases().contains(diabetes)) { rules.add(new RecommendItem(blood_sugar_monitor, 100.0)); rules.add(new RecommendItem(low_gi_diet, 85.0)); } // 规则3根据BMI推荐运动强度 double bmi profile.getWeight() / Math.pow(profile.getHeight() / 100.0, 2); if (bmi 28) { rules.add(new RecommendItem(low_impact_exercise, 70.0)); } else if (bmi 18.5) { rules.add(new RecommendItem(nutrition_supplement, 70.0)); } return rules; } /** * 余弦相似度计算 */ private double cosineSimilarity(MapString, Double v1, MapString, Double v2) { SetString commonKeys new HashSet(v1.keySet()); commonKeys.retainAll(v2.keySet()); if (commonKeys.isEmpty()) return 0.0; double dotProduct 0.0, norm1 0.0, norm2 0.0; for (String key : commonKeys) { dotProduct v1.get(key) * v2.get(key); } for (Double val : v1.values()) norm1 val * val; for (Double val : v2.values()) norm2 val * val; if (norm1 0 || norm2 0) return 0.0; return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); } /** * 加权融合两组推荐结果 */ private ListRecommendItem mergeWithWeight(ListRecommendItem list1, ListRecommendItem list2, double w1, double w2) { MapString, Double scoreMap new HashMap(); for (RecommendItem item : list1) { scoreMap.merge(item.getItemId(), item.getScore() * w1, Double::sum); } for (RecommendItem item : list2) { scoreMap.merge(item.getItemId(), item.getScore() * w2, Double::sum); } return scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .map(e - new RecommendItem(e.getKey(), e.getValue())) .collect(Collectors.toList()); } }这段代码的关键设计点有三个。第一缓存优先——推荐结果不是实时算的而是预计算后缓存RECOMMEND_CACHE_KEY的格式是recommend:user:{userId}:scene:{scene}方便按用户和场景维度清理。第二相似度阈值 0.3 是经验值低于这个值说明两个用户的行为模式基本不相关强行推荐只会引入噪声。第三规则引擎的分数是硬编码的高分80-100确保医学常识类推荐永远排在协同过滤结果前面。参数调整建议相似用户数量取 20 是平衡精度和性能的结果用户量小于 500 时可以取全部缓存过期时间 2 小时适合日活不高的健康系统如果用户行为频繁可以缩短到 30 分钟。2.4 数据库表设计里容易忽略的推荐字段推荐系统能不能跑好一半看算法一半看数据。很多健康系统的表设计只考虑了业务查询没考虑推荐计算后期改表非常痛苦。下面这几张核心表在设计时就要预留推荐相关字段-- 用户健康画像表推荐系统的输入源 CREATE TABLE user_health_profile ( user_id BIGINT PRIMARY KEY, age INT, gender TINYINT, height DECIMAL(5,2), weight DECIMAL(5,2), diseases VARCHAR(500), -- 逗号分隔的慢病标签如hypertension,diabetes allergies VARCHAR(500), update_time DATETIME ); -- 用户行为日志表协同过滤的原始数据 CREATE TABLE user_behavior_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT, item_id VARCHAR(100), -- 行为对象ID如文章ID、食谱ID、运动ID item_type VARCHAR(50), -- 行为类型article/diet/exercise/measure behavior_type VARCHAR(20), -- 行为动作view/collect/complete behavior_score DECIMAL(3,2), -- 行为权重view1, collect3, complete5 scene VARCHAR(50), -- 场景home/diet/exercise create_time DATETIME, INDEX idx_user_scene (user_id, scene), INDEX idx_item (item_id, item_type) ); -- 推荐结果缓存表Redis不可用时的降级方案 CREATE TABLE recommend_result ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT, scene VARCHAR(50), item_id VARCHAR(100), score DECIMAL(10,4), algorithm VARCHAR(50), -- 来源算法cf/rule/hybrid create_time DATETIME, INDEX idx_user_scene (user_id, scene) );behavior_score这个字段是很多项目会漏掉的。浏览、收藏、完成三种行为的权重完全不同如果统一按 1 分算协同过滤出来的结果会偏向「标题党」内容——用户点进去看一眼就退出的文章和认真看完并收藏的文章在算法眼里是一样的。我一般会设 view1、collect3、complete5这个梯度能有效区分行为深度。3. 从零跑通推荐流程环境搭建与核心接口联调3.1 SpringBoot 项目结构与依赖配置这套系统的后端采用标准 SpringBoot 分层结构推荐模块作为独立 package 存在方便后续替换算法实现src/main/java/com/health/ ├── HealthApplication.java // 启动类 ├── config/ │ ├── RedisConfig.java // Redis序列化配置 │ └── ScheduleConfig.java // 定时任务线程池配置 ├── controller/ │ ├── UserController.java │ ├── HealthRecordController.java │ └── RecommendController.java // 推荐接口入口 ├── service/ │ ├── UserService.java │ ├── HealthRecordService.java │ └── RecommendService.java // 推荐核心逻辑 ├── mapper/ │ ├── UserBehaviorMapper.java │ └── RecommendMapper.java ├── entity/ │ ├── UserHealthProfile.java │ ├── UserBehaviorLog.java │ └── RecommendItem.java └── task/ └── RecommendPreComputeTask.java // 定时预计算任务pom.xml里推荐模块需要额外引入的依赖!-- Redis缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 定时任务SpringBoot自带无需额外引入 -- !-- 如果需要分布式定时任务可引入xxl-job但单机项目不需要 -- !-- 数学计算库用于相似度计算 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-math3/artifactId version3.6.1/version /dependencyapplication.yml中推荐相关的配置项spring: redis: host: localhost port: 6379 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 recommend: cf: similarity-threshold: 0.3 # 相似度阈值 max-similar-users: 20 # 最大相似用户数 cache-hours: 2 # 缓存过期时间 rule: enabled: true # 规则引擎开关 schedule: cron: 0 0 3 * * ? # 每天凌晨3点预计算similarity-threshold这个参数需要根据实际数据调。用户行为数据多的时候可以调到 0.4 甚至 0.5数据稀疏时降到 0.2。判断标准是如果推荐结果里出现了明显不相关的条目说明阈值太低如果推荐列表经常为空说明阈值太高。3.2 推荐接口的 Controller 层实现与联调推荐接口的设计要遵循一个原则前端只传用户 ID 和场景不传任何算法参数。算法参数是后端配置不应该暴露给前端。// RecommendController.java RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private RecommendService recommendService; /** * 获取推荐列表 * GET /api/recommend/list?userId1scenehometopN10 */ GetMapping(/list) public ResultListRecommendItem getRecommendList( RequestParam Long userId, RequestParam(defaultValue home) String scene, RequestParam(defaultValue 10) Integer topN) { // 参数校验 if (userId null || userId 0) { return Result.error(用户ID不合法); } if (topN 50) { topN 50; // 限制最大返回条数防止前端恶意请求 } ListRecommendItem list recommendService.getRecommendations(userId, scene, topN); return Result.success(list); } /** * 用户行为上报接口 * POST /api/recommend/behavior * 前端在用户浏览、收藏、完成时调用 */ PostMapping(/behavior) public ResultVoid reportBehavior(RequestBody BehaviorReportDTO dto) { // 行为权重映射 double score switch (dto.getBehaviorType()) { case view - 1.0; case collect - 3.0; case complete - 5.0; default - 0.5; }; UserBehaviorLog log new UserBehaviorLog(); log.setUserId(dto.getUserId()); log.setItemId(dto.getItemId()); log.setItemType(dto.getItemType()); log.setBehaviorType(dto.getBehaviorType()); log.setBehaviorScore(score); log.setScene(dto.getScene()); log.setCreateTime(new Date()); recommendService.saveBehavior(log); return Result.success(); } }联调时最容易出问题的是行为上报接口。前端经常忘记在「用户离开页面」时上报 view 行为导致行为数据缺失。我的做法是在前端路由的beforeRouteLeave钩子里统一上报而不是依赖用户点击某个按钮。另外behavior_score的映射逻辑放在后端而不是前端防止前端篡改权重。3.3 定时预计算任务的实现与调度推荐结果不能每次请求都实时算必须预计算。SpringBoot 的Scheduled注解配合 cron 表达式就能实现// RecommendPreComputeTask.java Component public class RecommendPreComputeTask { private static final Logger log LoggerFactory.getLogger(RecommendPreComputeTask.class); Autowired private RecommendService recommendService; Autowired private UserMapper userMapper; /** * 每天凌晨3点执行全量用户推荐预计算 * cron表达式秒 分 时 日 月 周 */ Scheduled(cron ${recommend.schedule.cron}) public void preComputeAllUserRecommendations() { long start System.currentTimeMillis(); log.info(推荐预计算任务开始); // 分批处理每批100个用户防止内存溢出 int pageSize 100; int pageNum 1; int totalProcessed 0; while (true) { ListLong userIds userMapper.getUserIdsByPage((pageNum - 1) * pageSize, pageSize); if (userIds.isEmpty()) break; for (Long userId : userIds) { try { // 预计算三个场景的推荐结果 for (String scene : Arrays.asList(home, diet, exercise)) { recommendService.getRecommendations(userId, scene, 50); } totalProcessed; } catch (Exception e) { log.error(用户{}推荐预计算失败, userId, e); // 单个用户失败不影响整体任务 } } pageNum; } long cost System.currentTimeMillis() - start; log.info(推荐预计算任务完成处理用户数{}耗时{}ms, totalProcessed, cost); } }这个任务有几个关键设计。第一分批处理——一次性加载所有用户 ID 会导致内存溢出每批 100 个是安全值。第二单用户异常捕获——某个用户的数据有问题不应该中断整个任务。第三预计算时 topN 设 50实际接口返回时再截取前 10 或 20这样缓存里存的是更全的候选集前端请求不同 topN 时不用重新计算。启动类上需要加EnableScheduling注解否则定时任务不会生效SpringBootApplication EnableScheduling MapperScan(com.health.mapper) public class HealthApplication { public static void main(String[] args) { SpringApplication.run(HealthApplication.class, args); } }3.4 前端 Vue 对接推荐接口的注意事项前端对接推荐接口时最常见的翻车场景是「推荐列表闪烁」——页面加载时先显示空列表接口返回后突然填充用户体验很差。解决方案是在 Vuex 或 Pinia 里做推荐结果的本地缓存页面切换时先读缓存再异步刷新。// store/recommend.js (Pinia) import { defineStore } from pinia import axios from axios export const useRecommendStore defineStore(recommend, { state: () ({ cache: {}, // 格式{ userId_scene: { data: [], timestamp: 0 } } cacheTTL: 5 * 60 * 1000 // 前端缓存5分钟 }), actions: { async fetchRecommend(userId, scene, topN 10) { const key ${userId}_${scene} const cached this.cache[key] // 缓存有效则直接返回 if (cached Date.now() - cached.timestamp this.cacheTTL) { return cached.data } // 缓存过期或不存在请求接口 const res await axios.get(/api/recommend/list, { params: { userId, scene, topN } }) if (res.data.code 200) { this.cache[key] { data: res.data.data, timestamp: Date.now() } return res.data.data } return [] }, // 用户行为上报 async reportBehavior(userId, itemId, itemType, behaviorType, scene) { await axios.post(/api/recommend/behavior, { userId, itemId, itemType, behaviorType, scene }) // 行为上报后清除对应场景的缓存下次请求重新拉取 const key ${userId}_${scene} delete this.cache[key] } } })前端缓存 TTL 设 5 分钟比后端 Redis 的 2 小时短得多这是故意的——后端缓存保证计算不重复前端缓存保证页面切换流畅。行为上报后立即清除前端缓存确保用户完成一个健康任务后推荐列表能及时更新。4. 推荐效果调优与避坑那些文档里不会写的血泪经验4.1 冷启动问题的三种解法新用户没有行为数据协同过滤完全失效这是推荐系统最经典的冷启动问题。健康系统里这个问题更严重——用户注册后可能只填了基本信息一条行为记录都没有。解法一注册引导问卷。用户注册时强制填写健康画像年龄、性别、身高体重、慢病情况这些数据直接喂给规则引擎生成初始推荐。问卷不要超过 8 个问题否则用户会直接跳过。解法二热门推荐兜底。系统维护一个「全局热门健康任务」列表按完成率排序新用户直接推这个列表。热门列表每天更新一次数据来源是所有用户的完成行为统计。解法三人群标签匹配。根据用户注册时填的年龄和性别匹配到对应人群的推荐模板。比如「25-35 岁女性」模板里默认包含「叶酸补充提醒」「宫颈癌筛查提醒」等条目。这个模板由运营人员维护不需要算法参与。三种解法在实际项目里是组合使用的注册问卷拿到基础画像 → 规则引擎生成初始推荐 → 热门列表补充 → 等用户产生 5 条以上行为后协同过滤开始介入。4.2 推荐结果可解释性让用户知道「为什么推这个」健康推荐和电商推荐最大的区别是用户对健康建议的信任度要求极高。如果系统推荐「多吃西兰花」用户第一反应是「凭什么」。没有解释的推荐用户不会执行。我的做法是在推荐结果里附带reason字段public class RecommendItem { private String itemId; private String itemName; private Double score; private String reason; // 推荐理由 private String algorithm; // 来源算法 // 推荐理由的生成逻辑 public static String buildReason(String algorithm, String scene, UserProfile profile) { if (rule.equals(algorithm)) { if (profile.getDiseases().contains(hypertension)) { return 根据您的高血压病史建议定期监测血压; } return 根据您的健康画像推荐; } if (cf.equals(algorithm)) { return 与您健康状况相似的用户也在关注; } return 综合推荐; } }前端展示时推荐理由放在条目标题下方字号小一号、颜色浅一点。别小看这一行字加上之后推荐任务的点击率通常能提升 30% 以上。4.3 避坑清单五个真实踩过的坑坑一相似度计算时把「未发生行为」当成 0 分现象协同过滤推荐结果全是热门内容个性化程度极低。原因用户行为向量里没看过的文章被填了 0 分导致所有用户的向量都很稀疏余弦相似度算出来都差不多。解决只对「有行为的物品」计算相似度未发生行为的物品不参与向量构建。具体做法是用MapString, Double而不是固定长度的数组来存行为向量只存有值的 key。坑二定时任务和实时请求抢数据库连接现象凌晨 3 点预计算任务运行时白天正常的接口响应变慢偶尔超时。原因预计算任务全表扫描用户行为表和业务查询争抢数据库连接池。解决给预计算任务单独配置数据源或者限制任务的查询频率——每处理完一批用户Thread.sleep(100)给业务查询留出间隙。更彻底的做法是读写分离预计算走从库。坑三Redis 缓存击穿导致推荐接口雪崩现象缓存过期瞬间大量请求同时打到数据库接口响应时间从 50ms 飙升到 3s。原因缓存 key 在同一时刻过期所有请求都去查库。解决缓存过期时间加随机值比如2小时 Random(0, 30分钟)。另外对推荐接口做限流单用户每秒最多请求 2 次。坑四行为权重设置不合理导致推荐结果偏激现象推荐列表里全是「完成」类任务浏览类内容完全消失。原因complete5和view1的权重差距太大协同过滤时完成行为完全主导了相似度计算。解决权重梯度不要超过 5 倍。我一般用 view1、collect2、complete3这样既区分了行为深度又不会让某一类行为完全压制其他行为。坑五规则引擎硬编码疾病标签新增病种要改代码现象系统上线后要新增「高血脂」人群的推荐规则开发改代码、重新部署耗时半天。原因规则引擎的疾病判断写死在 Java 代码里。解决把规则配置化存到数据库或配置中心。规则表设计为rule_id, disease_tag, item_id, score, priority新增病种只需要插入一条记录不用改代码。规则引擎启动时加载全量规则到内存定时刷新。5. 推荐效果验证A/B 测试与离线指标怎么落地推荐系统做完不是终点能不能证明它有效才是。健康系统的推荐效果验证比电商更难——电商看转化率就行健康系统要看用户健康行为的改变周期长、干扰因素多。我一般用「离线指标 在线 A/B」两层验证。离线指标看算法本身的质量在线 A/B 看实际业务效果。离线指标主要看三个准确率推荐列表中用户实际发生行为的比例、覆盖率被推荐过的物品占总物品的比例、多样性推荐列表中不同类别物品的占比。准确率用留出法验证——把用户最后 20% 的行为作为测试集用前 80% 训练看推荐结果命中测试集的比例。覆盖率低于 30% 说明推荐过于集中多样性低于 0.5 说明推荐类别太单一。在线 A/B 测试的做法是把用户随机分成两组对照组用热门推荐实验组用混合推荐跑两周对比两组的「健康任务完成率」。这里有个坑——健康行为的改变有滞后性第一周的数据可能两组没差异第二周才显现。所以 A/B 测试至少跑两周别跑三天就下结论。// ABTestService.java 简易分流实现 Service public class ABTestService { /** * 根据用户ID哈希分流保证同一用户始终在同一组 * return control 或 experiment */ public String getGroup(Long userId) { // 用用户ID的哈希值对100取模小于50进对照组 int hash Math.abs(userId.hashCode()) % 100; return hash 50 ? control : experiment; } public ListRecommendItem getRecommendByGroup(Long userId, String scene, int topN) { String group getGroup(userId); if (control.equals(group)) { // 对照组热门推荐 return hotRecommendService.getHotItems(scene, topN); } else { // 实验组混合推荐 return recommendService.getRecommendations(userId, scene, topN); } } }分流逻辑用用户 ID 哈希而不是随机数保证同一用户每次请求都落在同一组否则用户体验会错乱——这次看到热门推荐下次看到个性化推荐用户会以为系统出 bug 了。最后说一个我自己的习惯每次调整推荐算法参数后先跑离线指标离线指标不达标就不上线做 A/B。离线指标达标了再小流量10% 用户跑三天观察接口响应时间和推荐点击率没问题再全量。这套流程看起来慢但比直接全量上线然后回滚要快得多。希望帮到你。本文还有配套的精品资源点击获取