ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot商品推荐系统实战:ItemCF协同过滤算法与冷启动策略详解

SpringBoot商品推荐系统实战:ItemCF协同过滤算法与冷启动策略详解 每年的毕业季都会有一大批学生来找我问同一个题目基于SpringBoot的商品推荐系统。这个题目看着不起眼但它是典型的“小切口、大纵深”的毕设——表面上是个CRUD加一个推荐列表深入进去却要人物品相似度、用户行为建模、冷启动、甚至性能优化。如果你正在为这个题目发愁或者你只是想快速搭一个推荐系统Demo应付工作里的POC这篇实操总结应该能让你少走很多弯路。我写过不下几十个SpringBoot项目也带过几届毕设这套方案是我反复用过、能在Demo阶段稳定跑通的。1. 项目整体设计与技术选型1.1 毕设题目背后的真实需求很多同学把“基于SpringBoot的商品推荐系统”单纯理解成“一个电商网站加一个猜你喜欢”这是最大的误区。毕设评审看的是三件事技术栈是否覆盖了主流开发环节、业务闭环是否完整、你能不能讲清楚背后的原理。推荐系统的完整闭环是“用户产生行为 → 行为数据落库 → 离线计算物品关联 → 在线生成推荐列表”这四个环节缺一个答辩时都会有短板。所以我给你的第一个建议是不要在首页轮播图和界面美观上花太多心思把精力放在“推荐引擎”这条主线上。商品模块、用户模块、订单模块都是为推荐服务的辅助设施只要够用就行。这个题目可以拆成三个层面表现层一个能登录、能浏览商品、能看到“为你推荐”的Web界面最好有管理后台。业务层用户、商品、订单、行为记录这些常规CRUD逻辑以及推荐结果的拼装。算法层用户行为数据的清洗、物品相似度计算、TopN推荐生成、冷启动策略。我见过太多人把算法层写成一个死循环或者直接随机推荐然后论文里大谈机器学习。这种方案运气好能过查重但答辩必挂因为评委随便问一个“你的相似度怎么算的”你就露馅。下面我会把每一层都讲透。1.2 为什么要用 SpringBoot 而不是其他框架SpringBoot 能成为毕设和中小型项目的默认选择原因非常实际自动装配让你不用再写一堆XML配置内置Tomcat让项目打成一个jar包就能跑再加上它在社区里的生态积累已经深到“你踩过的坑别人早就踩过且写了教程”。对比一下早期的SSHStruts2 Spring Hibernate或者SSMSpring SpringMVC MyBatisSpringBoot省掉的不仅仅是配置更重要的是它统一了项目的启动和部署方式。对毕设来说这一点直接影响演示环节——评委不会看你配了一个多漂亮的Tomcat他只看你双击Demo能不能跑起来。关于版本的坑我多说一句如果现在刚开始做选 SpringBoot 2.7.x 系列不要一上来就追 3.x。2.7 是2.x的最后一个大版本稳定、兼容性好网上资料最全MyBatis、PageHelper这些老牌组件对它的支持也是最成熟的。SpringBoot 3.x 虽然新但javax命名空间换成了jakarta很多老教程的代码直接搬过来会报包找不到的错。毕设图的是稳妥不是追新。1.3 技术选型和模块划分下面是我最推荐的毕设技术组合每一层都考虑到了“上手难度低、演示效果好、答辩能说清”三个维度。层次选型理由后端框架SpringBoot 2.7.x自动装配、生态成熟、部署简单持久层MyBatis PageHelperSQL可控、分页插件好用、面试常问数据库MySQL 8.x主流、好装、支持JSON字段缓存Redis缓存热门榜、用户最近浏览体现系统设计前端Vue 2 Element UI前后端分离界面速度成型快算法实现纯Java F一类Step不依赖外部算法库容易讲清原理模块划分我建议做成五个用户模块、商品模块、行为模块、推荐模块、管理模块。前三个是常规操作真正的核心是行为模块和推荐模块的衔接——你怎么从一张订单表里提取出“用户对商品的评分”这决定了算法的输入质量。这个我会在第二章详细拆解。2. 推荐算法原理与代码落地2.1 毕设推荐系统最合适的算法基于物品的协同过滤推荐算法何其多但毕设场景下最合适的几乎永远是Item-Based Collaborative Filtering简称ItemCF。它的核心思想一句话就能说完如果用户A买了《Spring实战》那找他跟其他买过《Spring实战》的人共同买过的书来推荐本质上是在做“物与物”的关联。为什么不用UserCF基于用户的协同过滤UserCF的思路是找相似用户把相似用户喜欢的物品推给你。它在新闻类产品里效果好因为用户群体大、兴趣变化快。但电商场景下用户数通常远大于商品数计算用户的相似矩阵开销大而且解释性弱——你很难跟评委解释“为什么我跟这个陌生用户像”。ItemCF的解释逻辑非常直接“因为你买过A商品所以推荐跟A商品最像的B商品”这一句话就能让评委点头。ItemCF的计算分两步根据用户行为计算物品之间的相似度。根据用户历史喜欢的物品找出相似物品生成推荐列表。相似度衡量方式很多最常用的是余弦相似度。把每个用户对物品的评分看成一个向量两个物品的相似度就是两个向量的夹角余弦值。夹角越小说明同时喜欢这两个物品的用户越多。2.2 从用户行为到评分矩阵算法需要输入但数据库里通常不会有一张现成的“用户-物品评分表”。电商系统里存的是订单表、浏览记录表、收藏表所以第一步是把这些行为转成隐式评分。我实践下来最稳的权重方案是浏览记1分、收藏记3分、加入购物车记5分、下单购买记10分。为什么不直接用购买金额因为金额波动太大会导致评分矩阵极不稳定而且容易把低价但高频率的商品排除掉。用行为权重的好处是即使没有真实评分也能构造出一个相对合理的用户-物品矩阵。对应的表结构大致这样CREATE TABLE user_action ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, action_type TINYINT NOT NULL COMMENT 1浏览,2收藏,3加购,4购买, score DECIMAL(5,2) NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_product (user_id, product_id) );在代码里我习惯用一个枚举类把行为映射为分数这样后续调整权重只改一处。public enum ActionType { VIEW(1, 1.0), COLLECT(2, 3.0), ADD_TO_CART(3, 5.0), PURCHASE(4, 10.0); private final int code; private final double score; ActionType(int code, double score) { this.code code; this.score score; } // 不推荐在枚举里直接加复杂逻辑但取分这种操作写在这里非常方便 }2.3 相似度计算的完整实现这里直接给出一个我用过多次的Java实现。它的思路是遍历所有用户的行为记录构建“同时被同一个用户消费过的商品对”的共现矩阵然后套余弦相似度公式。为了便于阅读我简化了部分代码但核心逻辑是完整的。public class ItemSimilarityCalculator { // key: productId_i_productId_j, value: 共同被用户消费的次数 private MapString, Integer cooccurrenceMap new HashMap(); // key: productId, value: 商品被消费的总次数 private MapLong, Integer productFreqMap new HashMap(); // 入参是每一行用户行为记录里解析出来的 (userId, productId) public void buildMatrix(ListUserAction actions) { MapLong, ListLong userItems new HashMap(); for (UserAction action : actions) { Long userId action.getUserId(); Long productId action.getProductId(); userItems.computeIfAbsent(userId, k - new ArrayList()).add(productId); productFreqMap.merge(productId, 1, Integer::sum); } for (ListLong items : userItems.values()) { // 去掉同一用户对同一商品的多条记录防止重复统计 SetLong uniqueItems new HashSet(items); ListLong itemList new ArrayList(uniqueItems); for (int i 0; i itemList.size(); i) { for (int j i 1; j itemList.size(); j) { Long p1 itemList.get(i); Long p2 itemList.get(j); String key p1 p2 ? p1 _ p2 : p2 _ p1; cooccurrenceMap.merge(key, 1, Integer::sum); } } } } public double cosineSimilarity(long pid1, long pid2) { String key pid1 pid2 ? pid1 _ pid2 : pid2 _ pid1; Integer co cooccurrenceMap.getOrDefault(key, 0); if (co 0) { return 0.0; } Integer freq1 productFreqMap.getOrDefault(pid1, 0); Integer freq2 productFreqMap.getOrDefault(pid2, 0); if (freq1 0 || freq2 0) { return 0.0; } // 余弦公式A·B / (|A| * |B|)这里忽略评分值只算0/1共现 return co / Math.sqrt(freq1 * freq2); } }这段代码里有个容易踩的坑key的拼接顺序。如果你不强制让p1 p2那用户行为顺序一变同一个商品对的key就会不同导致共现次数永远统计不上。我刚开始写这段逻辑时就是因为没注意排序结果相似度矩阵全是0排查了整整一晚上。2.4 冷启动问题的破解方法所有推荐系统都逃不过冷启动毕设答辩时评委几乎必问一个新用户没有任何行为记录你怎么推荐最简单的方案是热门榜兜底。新用户登录后推荐模块判断该用户的行为记录为空直接拉全局销量最高的TopN商品返回。这在工程上叫“Popularity-Based”策略代码实现只需一条SQL。SELECT product_id, SUM(score) AS total_score FROM user_action GROUP BY product_id ORDER BY total_score DESC LIMIT 20;另一个方案是基于商品内容属性的推荐本质上是在商品分类、品牌、标签维度做匹配。新用户注册时如果选了偏好分类就按偏好分类拉商品列表没选的话按默认热门分类混合推。这两个方案叠加冷启动问题在答辩场上基本就能站稳了。我实测下来的经验是热门榜兜底要加在推荐接口的最前面而且要给热门榜设置一个过期时间比如每小时的缓存刷新。否则一个用户刷十次页面推荐结果完全不动观感很差。用Redis做这个缓存是天然的搭配后面第三章会讲。3. 系统架构、数据库设计与核心模块实现3.1 项目结构到底怎么摆很多毕设代码的根目录混乱到让评委皱眉但推荐系统的项目结构其实有标准解。我的习惯是遵循SpringBoot推荐的包结构再单独拆一个recommend包放算法相关代码。src/main/java/com/example/mall ├── controller/ │ ├── ProductController.java │ ├── UserController.java │ └── RecommendController.java ├── service/ │ ├── ProductService.java │ ├── UserService.java │ └── RecommendService.java ├── mapper/ │ ├── ProductMapper.java │ └── UserActionMapper.java ├── entity/ │ ├── User.java │ ├── Product.java │ └── UserAction.java ├── common/ │ └── Result.java // 统一返回体 └── recommend/ ├── ItemSimilarityCalculator.java ├── RecommendationEngine.java └── PopularProductStrategy.java这样分的核心好处是算法逻辑recommend包与业务逻辑service包互相独立推荐引擎只依赖mapper提供的原始数据不掺入HTTP、参数校验等杂七杂八的东西。答辩时你介绍项目结构说一句“算法层与业务层分离”就是加分项。3.2 数据库表设计要点推荐系统毕设的数据库一般控制在6张表左右太少了撑不起架构图太多了浪费你写CRUD的时间。表名核心字段作用userid, username, password, nick_name用户登录与信息展示productid, title, price, category_id, tags商品信息tags用逗号分隔或JSONcategoryid, name, parent_id商品分类辅助内容推荐user_actionid, user_id, product_id, action_type, score行为数据算法的直接输入order_itemid, order_id, product_id, user_id, quantity下单明细与行为表互相印证recommendation_logid, user_id, product_ids, create_time记录推荐结果便于测试和审计其中recommendation_log往往被忽略但强烈建议加上。一是答辩时可以拿它展示“推荐是动态变化的”二是你可以用它做简单的推荐效果验证——比如查一下被推荐商品里有多少最终被用户购买。3.3 推荐引擎与业务代码怎么接这是整套系统的关键一步。推荐引擎算完之后必须转成业务接口能用的结构。我的做法是RecommendationEngine只负责算出一个“商品ID候选集和对应相似度值”具体的信息查询价格、图片、标题交给service层去关联。Service public class RecommendService { Resource private UserActionMapper userActionMapper; Resource private ProductMapper productMapper; Resource private RecommendationEngine recommendationEngine; public ListProductVO recommend(Long userId, int topN) { ListUserAction actions userActionMapper.selectByUserId(userId); if (actions null || actions.isEmpty()) { // 冷启动走热门榜策略 return productMapper.selectPopular(topN); } ListRecommendResult results recommendationEngine.recommend(userId, actions, topN); // 把推荐结果里的商品ID批量转成商品详情 ListLong productIds results.stream() .map(RecommendResult::getProductId) .collect(Collectors.toList()); return productMapper.selectBatchByIds(productIds); } }这里有一个细节不要用for循环在循环里逐条select商品信息那会造成严重的N1查询问题。批量IN查询是标准解法配合MyBatis的foreach标签就能一次搞定。数据量小的时候你可能感觉不到差异但评委如果问“这个接口并发高怎么优化”你能说出N1和批量的区别就是实打实的加分。3.4 登录、缓存与分页的配套实现推荐系统毕设里配套功能不需要做得很重但技术点要能拿得出手。三个最常见的配套点是Redis缓存、统一异常处理和分页查询。Redis在推荐系统里有三处妙用缓存热门榜、缓存推荐结果、存用户最近浏览记录。缓存热门榜我在2.4提过直接用opsForValue().set(hot:products, json, 30, TimeUnit.MINUTES)每次推荐前先查缓存缓存没命中再查库并回填。推荐结果同理一个userId对应的推荐列表5分钟内基本不变可以缓存起来减少计算压力。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 用Jackson做序列化器避免JDK默认序列化乱码 Jackson2JsonRedisSerializerObject serializer new Jackson2JsonRedisSerializer(Object.class); template.setDefaultSerializer(serializer); return template; } }分页推荐用PageHelper配合MyBatis非常顺手。在service方法上直接写PageHelper.startPage(pageNum, pageSize, score desc); ListUserAction actions userActionMapper.selectPage(userId); PageInfoUserAction pageInfo new PageInfo(actions);唯一要注意的是PageHelper的线程问题PageHelper.startPage必须紧跟你的第一个Mapper查询语句中间不能穿插其他数据库查询。有一次我把两行代码间加了一个debug日志日志里碰了另一个Mapper分页就失效了排查时特别隐蔽这也是比较典型的坑。4. 常见问题、性能排查与答辩准备4.1 我踩过的最典型的几个坑第一数据稀疏导致相似度全是零。这个几乎每个人都遇到过。商品几十个、用户十几个每个人只买过两三件商品共现矩阵里大部分商品对都没有同时出现过算出来自然全是0。我建议如果毕设没有真实数据可以用公开的电商数据集或者干脆自己写一个模拟用户行为生成器按正态分布随机给用户分配一系列“购买行为”。模拟数据的好处是可控你可以先把数据调稠一点保证算法效果能展示出来。第二SpringBoot版本升级带来的兼容性问题。我之前带过一个学生用了SpringBoot 3.0.0结果MyBatis的starter版本跟不上启动直接报ClassNotFoundException: javax.sql.DataSource。这种问题改起来非常头疼因为网上多数教程还是针对2.x写的。所以我的建议非常明确毕设项目不要追新版本用2.7.18测试过的组合。真遇到版本反复报错直接检查pom.xml里的依赖版本看是不是starter版本和Boot版本不匹配。第三用数据量去压算法性能。虽然金融级推荐系统动辄上亿数据但毕设数据量撑死几万条JavaHashMap级别的共现矩阵完全跑得动。我见过有人非要在毕设里引入Spark做分布式计算最后答辩现场启动Spark集群就要5分钟反而弄巧成拙。如果一个算法在一万条数据下都需要分布式评委只会认为你对计算复杂度没有概念。4.2 常见问题排查速查表问题可能原因解决方案推荐列表为空行为数据太少共现矩阵稀疏补充模拟行为数据增加热门榜兜底推荐的商品用户已经买过过滤逻辑遗漏推荐生成后按用户历史购买列表做排除PageHelper分页失效startPage后紧跟了非目标Mapper查询保证startPage下一条就是对应的Mapper方法Redis中文乱码序列化器用了JdkSerializationRedisSerializer改用Jackson2JsonRedisSerializer并指定UTF-8接口响应慢循环查库/推荐结果未缓存改成批量查询推荐结果加Redis缓存权重不区分行为类型action_type没映射到score使用枚举统一管理行为分数这张表我在答辩辅导时发给每个学生照着排查基本能解决百分之八十的代码问题。4.3 答辩时怎么讲推荐效果不讲效果的推荐系统答辩就是自曝短板。但“效果”不是随口说“推荐结果挺准的”你要给出可量化的指标。毕设场景下推荐三个简单指标准确率Precision、召回率Recall、覆盖率Coverage。解释起来也不难把测试集里用户真实购买的商品当作正例看看推荐列表命中了几件。一个最简单的评测方案把用户行为数据按时间对半分前一半作为训练数据来计算相似度后一半作为测试集。对用户u算推荐列表统计命中测试集中真实购买商品的个数然后除以推荐列表总长度。这个步骤用Java写个循环就能完成不需要额外工具。举个例子100个测试用户每个人推荐10件商品如果其中有120件商品命中了他们在测试期真实购买的商品那准确率就是120 / (100 × 10) 12%。这个数字看着不高但推荐系统评测里正常你可以跟评委解释真实购买受价格、库存、个人偏好等很多因素影响推荐命中本身就是高难度任务12%在一个离线小数据集上已经代表推荐模型学到了信息。我在实际带教过程中的体会是这个项目最大的价值不在于算法多前沿而在于它把SpringBoot后端、数据库设计、Redis缓存、算法实现、前端对接全部串成了闭环。你完全可以在一个项目里把Java后端的主要知识面都覆盖到这在工程实践里是性价比极高的学习路径。最后再分享两个小建议一是代码里所有魔法值比如行为权重1、3、5、10写成枚举或常量类方便你后期调参二是把项目运行录制成短视频保存在本地答辩当天即使环境出了问题你也能先放一段视频证明系统是真实可运行、可演示的。这些细节看着小关键时候都很顶用。
返回列表