书荒困境与 Spring Boot 智能阅读推荐系统的完整落地我一直觉得阅读类产品最难的不是内容供给而是把对的书在对的时候推到对的人面前。前阵子帮一个做阅读产品的小团队整理后台数据发现书库里明明有上千本质量不错的书但用户来回看的还是畅销榜上那几十本很多人打开 App 不到两分钟就退出。问题不是书不够多而是用户根本不知道自己下一条该读什么。这种场景下靠人工运营去排书单根本忙不过来必须要有一套能理解用户口味的推荐机制。我当时的方案是直接用 Spring Boot 从零搭一个智能阅读推荐系统。这个项目麻雀虽小但五脏俱全覆盖了用户注册、图书管理、行为采集、个性化推荐、冷启动兜底、离线相似度计算、缓存加速和效果评估这些完整链路。核心推荐算法用的是基于物品的协同过滤ItemCF不需要训练深度学习模型一个小团队完全养得起。前后花了大概两周业余时间从数据库设计到接口联调全部跑通。这篇文章我会把整个项目拆开讲包括技术选型背后的逻辑、数据库怎么建模、算法代码怎么写、冷启动怎么做、性能怎么优化、以及实测中踩过的坑。如果你正准备做阅读类产品、想提升用户阅读时长和留存又或者刚学完 Spring Boot 想找一个真正有深度的练手项目这篇笔记应该能给你一套可以直接落地的参考思路。1. 书荒背后的真实需求个性化推荐到底要解决什么1.1 三类典型用户场景拆解我开始动手前先没有急着写代码而是把用户场景梳理了一遍。这个系统不是给编辑部的运营人员用的是给真正在读书的用户用的。所以我列了三类典型用户第一类是刚注册的新用户打开 App 面对几千本书完全无从下手。他要的不是什么深度个性化而是我一进来就看到一本我愿意点开的书。这个需求听起来基础但很多产品都没做好——把畅销榜往首页一摆跟书店门口摆摊没区别。第二类是已经读过十几本书的老用户口味开始收敛。比如连续读了好几本悬疑推理之后再推同类容易腻推太跳的又接不住。他需要的是顺着已知口味慢慢拓宽边界的推荐方式。第三类是轻度用户一周上一次行为数据不多但也不是完全空白。推荐系统要能从很稀疏的行为里找出他的偏好信号同时兼顾新鲜感不能因为一次点击就把他钉死在某类书里。这三类场景落到技术上其实是同一个问题怎么用有限的行为数据预测用户下一本愿意打开的书。图书推荐的难点跟电商还不一样电商复购周期短用户行为频繁图书阅读周期动辄几周行为稀疏得多推荐策略不能生搬硬套电商那套热门逻辑。1.2 功能边界划定先保证核心链路跑通需求拆完我给自己划了一个很明确的边界第一版只做五个核心接口——用户注册登录、图书分类浏览、行为上报、热门榜单、个性化推荐。书单分享、社交评论、签到积分这些先统统不做。原因很朴素推荐系统项目最容易死在无限膨胀的业务需求里先把推荐这条主链路跑通才能真正验证算法有没有价值。系统模块划分则分成了四层数据层用 MySQL 存用户、图书、行为三类核心数据Redis 存相似度矩阵和热门榜单算法层负责离线计算相似度矩阵、在线生成推荐候选和排序业务层用 Spring Boot 的 Service 和 Controller 处理登录、图书查询、行为上报和推荐请求接入层统一走 RESTful API 给前端或客户端调用。这个分层看起来稀松平常但有一个关键点我想强调算法层和业务层一定要解耦。千万不要把相似度计算逻辑直接写在推荐 Service 里否则后面想换算法会非常痛苦。我的做法是把相似度计算和在线推荐拆成两个物理上独立的模块前者只负责定时把计算结果写进 Redis后者只负责从 Redis 读数据算排序两者之间没有任何直接的方法调用。2. 技术选型复盘Spring Boot 3 ItemCF 的组合逻辑2.1 为什么整个项目选了 Spring Boot 而不是 Python 或 Node很多人一听说做推荐系统就默认应该用 Python毕竟市面上的算法教程全是 Python 写的。但我的项目形态是一个完整的 Web 工程它不只有算法还要管用户会话、图书管理、权限控制、接口限流这些活。这些事恰恰是 Spring Boot 的强项。算法部分用 Java 实现协同过滤也不复杂几百行就能搞定加上 Spring Boot 自带的定时任务、事务管理、连接池和监控体系比用 Node 或 Python 从头组装一套 Web 基建要省事太多。我最终采用的组合是 Spring Boot 3.2.5 JDK 17 Maven。Spring Boot 3 基于 Spring Framework 6默认支持 JDK 17可以用 record 定义 DTO、用 var 简化局部变量写起来没有旧版本那种啰嗦感。如果还有人纠结要不要从 Spring Boot 2.x 迁到 3.x我的建议是新项目直接上 3.x 加 JDK 17旧版本虽然稳但新特性全都没法用踩坑还得自己补。持久层我选了 MyBatis-Plus没走 Spring Data JPA。原因是行为数据的查询条件非常灵活经常要按时间范围聚合、按行为类型过滤、按用户去重写 SQL 比靠 JPA 的方法名派生查询更可控。MyBatis-Plus 的分页插件也很成熟热门榜单这种分页查询直接就能接上。2.2 推荐算法选型ItemCF 在阅读场景的天然优势阅读场景是一个典型的长尾稀疏场景一个用户一年高质量阅读可能就几十本但书库里有几千本。如果选 UserCF基于用户的协同过滤每次推荐都要实时计算用户和用户之间的相似度用户规模一上来性能就很难看。更麻烦的是用户兴趣漂移快——上个月爱看悬疑推理这个月追言情UserCF 的相似用户群根本反应不过来。ItemCF基于物品的协同过滤在这两点上都更稳。书和书之间的相似关系变化很慢《三体》和《球状闪电》的相似度不会因为某个用户的短期口味变化而剧烈波动所以计算结果可以离线算好存起来在线推荐只做查询聚合性能压力小得多。ItemCF 还有个额外好处是解释性强推荐结果可以顺口说一句因为你读过《XX》所以推荐《XX》这个推荐理由在产品界面上非常有用用户信任度高。深度学习模型比如 DeepFM、双塔召回结构我也认真考虑过但最后还是否决了。原因很现实这类模型需要大量训练资源、特征工程和样本标注小体量产品根本喂不饱模型训练出来的东西还不如 ItemCF 稳定。我的判断标准很简单先低成本跑通闭环如果以后用户量真的大到一定程度再在 ItemCF 之上加一层深度学习排序器做精排两者并不冲突。2.3 MySQL 和 Redis 在推荐链路里各管什么整个系统的存储方案被我把职责切得很清晰。MySQL 只管三张核心表用户表、图书表、行为表。行为表是推荐系统的原材料也是数据量增长最快的表所以索引一定要设计好否则刷几百万条记录后查询就开始卡。Redis 干两件事第一存图书相似度矩阵用 Hash 结构主 key 是book:similarity:图书IDfield 是相似图书的 IDvalue 是相似度分数第二存热门榜、新品榜这些轻量榜单用 ZSet 结构按热度评分排序每小时刷新一次。为什么相似度矩阵一定要放 Redis 而不是 MySQL因为在线推荐时系统要高频随机读取某本书的相似邻居列表Redis Hash 一次 HGETALL 就能把整本书的相似列表全部拉回来耗时在毫秒级。如果放 MySQL每次都要走二级索引查几千行再组装延迟直接高一个数量级。缓存这东西用对场景就是降本增效用错场景就是给自己多造一个故障点。我还顺手给 Redis 开了一个内存估算假设有效书池 5000 本每本书保留 Top 50 个相似邻居每个 field 大约 16 字节最终相似矩阵占用的内存大概 25MB 左右完全在可控范围内。3. 数据库建模与埋点设计把推荐系统这块地基打牢3.1 用户、图书、行为三张核心表的字段设计推荐系统绕不开的就是那张行为表。我在设计user_behavior表时没有偷懒字段直接对标埋点需求设计。主键用自增id业务字段包括user_id、book_id、behavior_type、read_progress、read_duration、source和created_at。其中behavior_type存的是枚举值包含 VIEW、SEARCH、COLLECT、READ 四种read_progress存阅读进度百分比从 0 到 100source记的是本次行为来源比如首页推荐位、搜索页、分类页这个字段平时看着没用但分析推荐位点击率的时候全靠它。索引设计是这张表的重中之重。我建了三个索引联合索引(user_id, created_at)支撑按用户拉取行为时间线联合索引(book_id, created_at)支撑按图书统计谁读过它单独给behavior_type建普通索引支撑按行为类型过滤。在百万级行为数据量下这套索引方案实测查询都能控制在几十毫秒以内足够支撑第一版业务。图书表除了基本的书名、作者、分类、封面、简介、字数、出版时间之外我额外加了两个专为推荐服务的字段hot_score和avg_rating。hot_score不依赖用户手动打分而是通过最近时间段的点击、收藏、完整阅读等行为加权计算出的热度值冷启动阶段直接按这个字段取热门榜。avg_rating是评分均值作为推荐结果里的辅助信号让用户对推荐书目的质量有个预判。3.2 行为数据的采集、去重与权重规则行为采集的前端链路很简单客户端在用户完成指定动作时调一个统一上报接口POST /api/behavior/report上报内容是行为表对应的 JSON。但这里藏着一个非常容易被忽视的坑重复上报。客户端网络抖动、用户快速连点、前端事件重复绑定这些情况都会导致同一条行为在几秒内被上报多次。如果不做去重后面算相似度矩阵的时候虚假的重复行为会把两本书的相似度拉得虚高。我做了两层去重防线。第一层在接口层用 Redis 布隆过滤器记录用户ID图书ID行为类型时间窗口是否已经处理过处理过的直接丢弃。第二层放在离线计算任务里基于相同user_id、book_id、behavior_type、日期四个维度做聚合去重只保留一条有效记录。两层方向不同接口层拦截突发重复上报离线层清理历史脏数据配合使用效果最好。行为权重方面我按产品直觉定了一套初始规则以后完全可以基于 AB 实验再来调整。我的设定是VIEW 点击权重 0.5SEARCH 搜索权重 2.0COLLECT 收藏权重 4.0READ 完整阅读进度大于等于 95%权重 6.0未完结的阅读按进度线性衰减。这套权重决定了推荐模型如何理解什么行为代表真正的兴趣——一个反复点击又不读的人跟一个整本读完的人肯定不应该享受同样的推荐待遇。4. 推荐引擎核心实现从相似度矩阵到在线推荐4.1 离线相似度计算的完整实现算法层最核心的离线任务是每天算一次图书相似度矩阵。我直接用 Spring 自带的Scheduled注解每天凌晨 2 点触发。这个时间点业务流量最低不会跟在线接口抢资源。计算流程拆成四步每一步都对应代码里的一个逻辑块第一步从行为表拉取最近 30 天的有效正反馈行为。正反馈定义为 SEARCH、COLLECT 和完整阅读单纯的 VIEW 不纳入相似度计算因为点击太容易造假噪音太大。第二步构建用户到图书的倒排表也就是一个MapLong, SetLong表示每个用户正反馈过哪些书。第三步统计两本书之间的共现次数。所谓共现就是指有多少个用户同时跟这两本书发生过正反馈行为。第四步用余弦相似度公式计算最终分数sim(A,B) 共现次数(A,B) / sqrt(包含A的用户数 * 包含B的用户数)。这个公式是 ItemCF 的标准做法分子表示两本书被同一批人看过的次数分母做归一化避免热门书天然就跟所有书都相似。代码我贴了一段核心逻辑去掉了一部分工程包装你可以直接照着实现Component public class ItemSimilarityCalculator { Scheduled(cron 0 0 2 * * ?) public void runDailyCalculate() { // 1. 拉取最近30天的正反馈行为 ListUserBehavior behaviors behaviorRepository.findRecentPositiveBehaviors(30); if (behaviors.isEmpty()) { log.info(无有效行为数据跳过本次相似度计算); return; } // 2. 构建 用户 - 图书集合 的倒排表 MapLong, SetLong userBooks behaviors.stream() .collect(Collectors.groupingBy( UserBehavior::getUserId, Collectors.mapping(UserBehavior::getBookId, Collectors.toSet()) )); // 3. 统计每本书被多少个用户正反馈过 MapLong, Integer itemCounts new HashMap(); userBooks.values().forEach(bookIds - bookIds.forEach(bookId - itemCounts.merge(bookId, 1, Integer::sum))); // 4. 统计两两共现次数 MapLong, MapLong, Integer cooccurrence new HashMap(); userBooks.values().forEach(bookIds - { ListLong ids new ArrayList(bookIds); for (int i 0; i ids.size(); i) { for (int j i 1; j ids.size(); j) { cooccurrence.computeIfAbsent(ids.get(i), k - new HashMap()) .merge(ids.get(j), 1, Integer::sum); cooccurrence.computeIfAbsent(ids.get(j), k - new HashMap()) .merge(ids.get(i), 1, Integer::sum); } } }); // 5. 计算余弦相似度每本书只保留 Top 50 个相似邻居 cooccurrence.forEach((bookId, related) - { MapLong, Double similarity new HashMap(); related.forEach((otherId, count) - { double sim count / Math.sqrt( itemCounts.get(bookId) * itemCounts.get(otherId)); similarity.put(otherId, sim); }); ListMap.EntryLong, Double topEntries similarity.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(50) .collect(Collectors.toList()); redisTemplate.opsForHash().putAll( book:similarity: bookId, topEntries.stream().collect(Collectors.toMap( e - String.valueOf(e.getKey()), e - String.valueOf(e.getValue())) )); }); log.info(相似度矩阵计算完成共 {} 本图书参与计算, cooccurrence.size()); } }这里有一个很少有人强调的工程细节必须做 Top K 截断只保留每本书相似度最高的 50 个邻居。如果不截断一本热门书可能跟几千本书都有关联相似度矩阵会膨胀到无法控制在线推荐时也会把大量低质量候选拉进排序列表既浪费计算又稀释推荐质量。加上截断之后Redis 内存可控在线查询压力也小得多。4.2 在线推荐候选生成与加权排序离线算好相似度矩阵在线推荐的处理就变得很清爽。整个流程从接口收到userId开始分四步走第一步从行为表里查这个用户最近一段时间有正反馈的书最多取 20 本。为什么要限 20 本因为用户最近读的 20 本书已经能充分代表近期兴趣再拉更多数据量大了计算耗时也会上来收益却很小。第二步遍历这 20 本书从 Redis 里分别取出各自的相似书列表。第三步对候选书打分。候选书 j 的得分等于所有用户读过的书 i 的行为权重与相似度sim(i,j)的乘积之和。这个公式的含义是用户越喜欢一本书且这本书跟候选书越像候选书得分就越高。第四步过滤掉用户已经读过的书按得分降序取前 topN 返回。代码核心部分如下Service public class RecommendService { public ListBookVO recommendForUser(Long userId, int topN) { // 1. 获取用户最近的正反馈行为 ListUserBehavior positiveBehaviors behaviorRepository .findRecentPositiveByUserId(userId, 20); if (positiveBehaviors.isEmpty()) { return getColdStartBooks(userId, topN); } // 2. 聚合候选得分 MapLong, Double candidateScores new HashMap(); for (UserBehavior behavior : positiveBehaviors) { MapLong, Double similarBooks getSimilarityMap(behavior.getBookId()); double weight behaviorWeight(behavior); similarBooks.forEach((bookId, similarity) - { if (!hasPositiveFeedback(userId, bookId)) { candidateScores.merge(bookId, weight * similarity, Double::sum); } }); } // 3. 排序取 topN ListLong recommendIds candidateScores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); return bookRepository.findByBatchId(recommendIds); } private MapLong, Double getSimilarityMap(Long bookId) { MapObject, Object entries redisTemplate.opsForHash() .entries(book:similarity: bookId); MapLong, Double result new HashMap(); entries.forEach((key, value) - result.put(Long.valueOf(key.toString()), Double.parseDouble(value.toString()))); return result; } private double behaviorWeight(UserBehavior behavior) { switch (behavior.getBehaviorType()) { case COLLECT: return 4.0; case READ: return behavior.getReadProgress() 95 ? 6.0 : 1.0 behavior.getReadProgress() / 25.0; case SEARCH: return 2.0; default: return 0.5; } } }在线推荐这段逻辑看上去简单但它是整个系统吞吐量的核心。因为所有数据都从 Redis 读计算都在内存里跑单次推荐请求的耗时能稳稳压在 30 毫秒内。这也再次证明了一开始选型时的判断ItemCF 的结构决定了它天然适合做离线计算 在线查询这种高性能架构。4.3 定时任务的线程隔离与缓存原子切换定时任务这块有个坑必须单独拎出来说。Scheduled默认的调度线程池只有 1 个线程如果任务在计算过程中阻塞了其他所有定时任务都会排队等待。我第一次上线时下午高峰时段用户侧接口出现了明显卡顿排查到最后发现就是定时任务占着唯一一个调度线程做全量计算。后来我把计算逻辑抽出来用独立线程池去跑调度方法只负责提交任务彻底解决了线程抢占问题。另一个容易被忽略的细节是 Redis 写入时的原子性。相似度矩阵是整个批量覆盖的如果计算过程中把中间状态直接写进线上 key在线请求就可能读到半截数据导致某本书的相似邻居列表不完整。我的处理方式是先写入临时 key等整份矩阵全部算完再执行rename做原子切换或者在 key 里带上版本号线上读取时固定读最新版本。这种方式虽然多一步但能保证数据一致性值得养成习惯。5. 接口层设计与冷启动实战策略5.1 三个核心接口的定义与实现接口我刻意控制在三个避免第一版就接口满天飞。第一个是行为上报接口第二个是个性化推荐接口第三个是热门榜接口。所有接口统一返回一个Result包装类包含code、msg、data三个字段错误码收敛到一个枚举里前端联调时只要对一次文档就能记住。RestController RequestMapping(/api) public class RecommendController { PostMapping(/behavior/report) public ResultVoid report(RequestBody BehaviorReportRequest request) { behaviorService.report(request); return Result.ok(); } GetMapping(/recommend/{userId}) public ResultListBookVO recommend(PathVariable Long userId, RequestParam(defaultValue 10) int topN) { return Result.ok(recommendService.recommendForUser(userId, topN)); } GetMapping(/books/hot) public ResultPageResultBookVO hotBooks( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size) { return Result.ok(bookService.hotBooks(page, size)); } }行为上报接口要做两件事一是校验参数合法性比如用户 ID、图书 ID、行为类型必须存在阅读进度不能超过 100二是做前面提到的布隆过滤器去重这是防止脏数据进入推荐链路的关键关卡。推荐接口的topN参数默认给 10这个值可以根据产品需求调但最好不要超过 30否则推荐列表太长用户根本看不完转化率反而下降。5.2 新用户与新上架图书两条不同的冷启动路径冷启动是推荐系统绕不开的话题。我把冷启动分成两个方向分别处理。新用户的冷启动最直接有效的办法就是在注册流程里让用户勾选两三个感兴趣的图书分类。有了分类标签之后第一轮推荐就可以按这些分类做混排完全不需要等用户产生行为。同时我会在推荐结果里混入一定比例的热门榜和新书榜内容保持新鲜感让用户感觉系统推荐的不是清一色同类书。新上架图书的冷启动麻烦一点因为它没有行为数据ItemCF 算不出相似度。我做了一个内容层面的规则近似同作者、同分类、同标签的新书默认给它一个基础相似度分数 0.1。这个分数不高不会冲击正常推荐结果但能让新书有机会出现在关联推荐位。等这本书积累够行为数据之后离线任务会自动把它纳入真实的相似度矩阵规则近似会被覆盖掉。除此之外我还做了多样性限制最终 topN 推荐结果里同一分类的书最多占 50%。不做这个限制就会出现用户爱看悬疑推荐列表前八本全是悬疑的情况虽然精准但容易让用户产生阅读疲劳。推荐系统不能永远追着用户已知的兴趣跑适度探索是拉住用户的重要手段。6. 推荐效果验证与性能调优实测6.1 离线数据上的推荐质量评估推荐做得好不好不能光靠感觉。我采用了一套最朴素的离线评估方案把每个用户的行为序列按时间切成 80% 和 20% 两段前 80% 作为已知历史输入推荐系统后 20% 作为待预测的标准答案然后看推荐结果中有多少本书出现在后 20% 里。指标用三个精确率10、召回率10、覆盖率。我构造了一批模拟行为数据来做测试规模是 300 个用户、3 万条行为记录、覆盖 800 本书。测试结果如下表所示指标数值说明Precision100.16推荐 10 本里平均有 1.6 本是用户真实行为的书Recall100.24用户后 20% 真实行为中有 24% 被推荐系统命中Coverage37%推荐池覆盖了全部图书的 37%多样性尚可这个数据谈不上惊艳但要知道它是在没有深度学习模型、没有精心调参的情况下跑出来的。业务侧的体感是用户收到的推荐比单纯畅销榜更贴合个人口味。我特别提醒一句推荐系统的评估不能只看精确率和召回率覆盖率低说明系统永远在推同一批热门书长尾图书会彻底失去曝光这对内容型产品是慢性死亡。6.2 三个性能瓶颈与对应优化第一个瓶颈是离线计算量的爆炸。如果不做任何限制让任意两本书都算一遍相似度10 万本书就是 10 的 10 次方量级的组合单机根本跑不完。我的优化策略是只对有效书池做计算也就是近 30 天有正反馈行为的书这一步通常能砍掉 70% 以上的无效计算。再加上每本书只保留 Top 50 个相似邻居计算量被压到了一个单机完全能承受的范围。第二个瓶颈是 Redis 内存增长。相似度矩阵如果不做邻居截断热门书关联的 key 数量会到百万级内存很快爆掉。限制 Top 50 邻居后内存占用大约是完整矩阵的十分之一这个取舍非常划算。第三个瓶颈是在线查询延迟。第一版直接串行遍历用户读过的 20 本书逐个从 Redis 取相似列表并发一上来延迟就开始飘。我的优化方案是在本地加了一层 Caffeine 缓存热门书的相似列表直接命中本地Redis 查询压力大幅下降。实测优化前后推荐接口的 P99 延迟从 210 毫秒降到 30 毫秒左右。推荐系统不能只追求算法准确率性能决定了它能不能在大流量下活下去这两个指标都要抓。7. 踩坑记录与后续扩展方向7.1 三个值得记录的实战坑这个项目做下来最深刻的三个坑值得分享。第一个坑是行为数据重复导致相似度虚高。前面虽然设计了布隆过滤和离线去重但最初版本没有严格执行结果某本热门书几乎跟所有书都算出了高相似度推荐列表被这本书的关联书霸榜。真正排查下来才发现是刷点击的行为没有被过滤掉。后来接口层布隆过滤和离线聚合去重双管齐下才彻底解决。这类问题最隐蔽因为它不会让程序报错只会悄悄污染算法质量。第二个坑是定时任务阻塞核心线程。前面提到过Scheduled默认单线程的问题当时的表现是下午高峰用户侧接口卡顿日志里发现很多请求排队等待调度线程。排查过程花了大半天最终解决方案是独立线程池执行计算任务调度方法只负责提交。这个坑在小项目里非常典型因为本地开发时很难压出并发问题一上线就暴露。第三个坑是 Redis 序列化方式不统一。Hash 的 field 和 value 在写入时用了一套序列化器读取时又换了另一套导致部分 key 解析失败。后来全链路统一使用 String 序列化杜绝了JdkSerializationRedisSerializer和StringRedisSerializer混用的问题。这种问题在本地测试时很难触发因为同一个 JVM 环境序列化一致跨环境部署就现形了。7.2 推荐系统后续可以考虑的扩展方向整个项目跑通之后我觉得后续有四个方向值得按优先级推进。第一是引入图数据库。图书、作者、读者、标签之间存在非常丰富的网状关系用关系型数据库表达很别扭换成图数据库之后可以做多跳关联推荐比如读 A 的人也读 B读 B 的人还喜欢作者 C 的书这种深度链路。第二是接入大模型做内容标签提取。现在新书冷启动全靠人工规则给一个基础相似度如果引入大模型自动提取书籍摘要、主题、情感倾向新书在上架当天就能获得高质量内容画像冷启动时间会大幅缩短。第三是在 ItemCF 之上叠加排序模型。ItemCF 负责从几万本书里快速筛出几百个候选再用简单的逻辑回归或者 XGBoost 对候选做精排融合更多特征比如图书评分、出版时间、用户阅读时长推荐精度还能上一个台阶。第四是搭一个简易的 AB 实验平台。推荐策略改动不能拍脑袋上线先小流量验证指标再逐步放量这是推荐系统进入生产环境后必须补上的一环。这个项目从需求梳理到完全跑通前后大概花了两周代码我整理得比较干净依赖和部署说明都写在 README 里了。如果你在准备做阅读类产品或者想拿 Spring Boot 练手推荐系统欢迎私信找我拿完整源码参考。最后再分享一条我最大的心得体会推荐算法的核心不在于模型多么炫酷而在于你对用户行为数据的理解有多深。数据干净、权重合理、链路顺畅哪怕只是最朴素的协同过滤也能让用户明显感觉到这个 App 懂我。