ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue实现协同过滤推荐系统实战

SpringBoot+Vue实现协同过滤推荐系统实战 简介本资源是一套高分通过的Java毕业设计项目——基于SpringBoot与Vue实现的协同过滤算法商品推荐系统专为计算机专业本科生毕设、课程设计及Java进阶学习者打造有效解决电商场景下个性化商品推荐的技术实践难题。压缩包共807个文件涵盖115个Java后端核心类、45个Vue前端组件、164个JS交互逻辑与162个SVG图标资源辅以SQL数据库脚本、YML配置、BAT启动脚本及开发说明文档等结构完整、模块清晰总大小19.62MB。已有356人下载学习项目经导师指导并完成全流程调试可直接导入IDEA/Eclipse运行支持JDK1.8SpringBootMySQL5.7Tomcat7环境一键部署。读者将获得从协同过滤算法实现、前后端分离架构搭建、数据库设计到完整部署说明的全链路实战材料含登录/推荐/管理等核心功能模块及备份文件如.vue.bak、.html.bak便于理解版本演进与调试过程。1. 这不是又一个“商城加推荐”的空壳项目它用真实用户行为数据驱动 SpringBoot Vue 架构下的协同过滤落地解决的是冷启动后「用户刷不出想点的货」这个电商场景里最扎心的体验断点很多 Java 毕业设计里的“推荐系统”本质是静态商品列表加个随机排序或按销量硬排序连用户 ID 都没真正接入。而本项目标题中明确标注的「协同过滤算法」意味着它必须完成从用户-商品交互日志如浏览、加购、下单中提取隐式反馈构建用户相似度矩阵或物品共现矩阵并在 SpringBoot 后端实时计算 Top-N 推荐结果再由 Vue 前端以卡片流、猜你喜欢、看了又看等形态渲染。它不依赖内容标签或人工规则而是让数据自己说话——哪怕只有 200 条真实行为记录也能跑通基于用户的 User-CF 或基于物品的 Item-CF 流程。适合正在准备 Java 全栈毕设、需要展示「算法工程可视化」闭环能力的同学也适合刚入职的后端开发理解推荐链路如何嵌入主流技术栈SpringBoot 处理数据清洗与模型服务化Vue 承担推荐位动态加载与曝光埋点上报MySQL 存储原始行为与缓存结果。别被“毕业设计”四个字误导——这套流程正是主流电商平台推荐模块 MVP 版本的真实缩影。2. 用 SpringBoot 实现协同过滤核心逻辑从行为日志建模到内存级相似度计算避开 Spark/Hadoop 重依赖2.1 为什么选内存计算而非大数据框架毕业设计场景下的务实取舍协同过滤在工业级系统中常依托 Spark MLlib 或 Flink 实时计算但对单机部署、MySQL 存储、无 Hadoop 环境的毕设项目强行引入分布式框架反而导致环境复杂、调试困难、答辩时无法现场演示。本项目采用 SpringBoot 内存计算方案核心依据有三第一行为数据量可控通常 ≤10 万条JavaHashMap和ArrayList完全可承载第二推荐请求 QPS 低学生演示阶段每秒 ≤5 次无需毫秒级响应第三算法逻辑需高度透明——便于你在答辩时指着代码讲清“相似度怎么算的”“为什么这个用户被推荐了这件商品”。因此我们放弃 MapReduce 抽象直接用DoubleMatrixApache Commons Math3或自定义二维数组操作用户-物品评分矩阵所有计算在 Controller 层或 Service 层同步执行不引入 Redis 缓存中间结果除非你主动扩展确保每一步都可 debug、可打印、可截图。2.2 数据建模用三张 MySQL 表支撑协同过滤最小可行数据集协同过滤不依赖商品详情文本只依赖用户与商品的交互强度。本项目数据库设计严格遵循此原则仅需三张表表名字段说明user_behaviorid(PK),user_id(BIGINT),item_id(BIGINT),behavior_type(TINYINT: 1浏览,2加购,3下单),timestamp(BIGINT)原始行为日志behavior_type权重可配置如下单3浏览1user_infoid(PK),user_id(BIGINT),username(VARCHAR)用户基础信息用于前端展示昵称item_infoid(PK),item_id(BIGINT),title(VARCHAR),category(VARCHAR)商品基础信息用于渲染卡片标题与分类提示不要在user_behavior中存储时间字符串如2024-05-20 14:30:00统一用System.currentTimeMillis()生成的 long 型时间戳。这能避免 MySQLdatetime类型在 JavaLocalDateTime转换时的时区陷阱且便于后续按时间窗口采样如只取最近 30 天行为。2.3 实现基于用户的协同过滤User-CF从相似用户挖掘到推荐生成User-CF 的核心是找到与目标用户兴趣最相似的 K 个用户将他们喜欢但目标用户未交互过的商品按相似度加权排序推荐。SpringBoot 中关键实现步骤如下2.3.1 构建用户-物品评分矩阵Sparse Matrix// UserServiceImpl.java public MapLong, MapLong, Double buildUserItemRatingMap() { // 查询所有有效行为排除浏览但未加购/下单的噪声 ListUserBehavior behaviors userBehaviorMapper.selectByBehaviorTypeIn(Arrays.asList(2L, 3L)); // 初始化user - {item - rating} MapLong, MapLong, Double userItemRating new HashMap(); for (UserBehavior b : behaviors) { long userId b.getUserId(); long itemId b.getItemId(); double rating b.getBehaviorType() 2 ? 1.0 : 3.0; // 加购1下单3 userItemRating.computeIfAbsent(userId, k - new HashMap()) .put(itemId, rating); } return userItemRating; }这段代码输出的是稀疏矩阵结构每个用户只存其交互过的商品及对应权重。注意不填充 0 值即用户未交互的商品不写入 map否则内存爆炸。2.3.2 计算用户间余弦相似度Cosine Similarity// RecommendationService.java public double cosineSimilarity(MapLong, Double user1Items, MapLong, Double user2Items) { // 找出两个用户的共同交互商品 SetLong commonItems new HashSet(user1Items.keySet()); commonItems.retainAll(user2Items.keySet()); if (commonItems.isEmpty()) return 0.0; double dotProduct 0.0; double norm1 0.0, norm2 0.0; for (Long item : commonItems) { double r1 user1Items.getOrDefault(item, 0.0); double r2 user2Items.getOrDefault(item, 0.0); dotProduct r1 * r2; norm1 r1 * r1; norm2 r2 * r2; } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }参数说明user1Items和user2Items是上一步buildUserItemRatingMap()返回的子 map。该方法返回值范围为 [0,1]值越接近 1 表示用户兴趣越相似。注意若两用户无共同交互商品直接返回 0避免除零错误。2.3.3 获取 Top-K 相似用户并生成推荐列表// RecommendationService.java public ListRecommendItem recommendForUser(Long userId, int k, int n) { MapLong, MapLong, Double userItemRating userService.buildUserItemRatingMap(); MapLong, Double targetUserItems userItemRating.getOrDefault(userId, Collections.emptyMap()); // 计算目标用户与所有其他用户的相似度 ListMap.EntryLong, Double similarUsers new ArrayList(); for (Map.EntryLong, MapLong, Double entry : userItemRating.entrySet()) { Long otherUserId entry.getKey(); if (otherUserId.equals(userId)) continue; // 跳过自己 double sim cosineSimilarity(targetUserItems, entry.getValue()); if (sim 0.1) { // 过滤掉弱相似用户阈值可调 similarUsers.add(new AbstractMap.SimpleEntry(otherUserId, sim)); } } // 按相似度降序取前 K 个 similarUsers.sort((a, b) - Double.compare(b.getValue(), a.getValue())); ListMap.EntryLong, Double topKUsers similarUsers.subList(0, Math.min(k, similarUsers.size())); // 收集这些相似用户喜欢但目标用户未交互的商品加权计分 MapLong, Double candidateScores new HashMap(); for (Map.EntryLong, Double similarUserEntry : topKUsers) { Long similarUserId similarUserEntry.getKey(); Double similarity similarUserEntry.getValue(); MapLong, Double similarUserItems userItemRating.get(similarUserId); for (Map.EntryLong, Double itemEntry : similarUserItems.entrySet()) { Long itemId itemEntry.getKey(); Double itemRating itemEntry.getValue(); // 若目标用户已交互过该商品跳过 if (targetUserItems.containsKey(itemId)) continue; // 加权累加相似度 × 商品评分 candidateScores.merge(itemId, similarity * itemRating, Double::sum); } } // 按得分降序取前 N 个 return candidateScores.entrySet().stream() .sorted((a, b) - Double.compare(b.getValue(), a.getValue())) .limit(n) .map(entry - { ItemInfo item itemInfoMapper.selectById(entry.getKey()); return new RecommendItem(item.getItemId(), item.getTitle(), item.getCategory(), entry.getValue()); }) .collect(Collectors.toList()); }关键参数说明k是相似用户数量建议设为 5~10n是最终推荐商品数如 6 或 12。similarity 0.1是硬性过滤阈值防止噪声用户干扰candidateScores.merge(...)实现加权累加是 User-CF 的核心聚合逻辑。该方法返回RecommendItem列表含商品 ID、标题、分类及推荐得分可直接序列化为 JSON 返回给 Vue 前端。3. Vue 前端集成推荐结果从 API 调用、卡片渲染到用户行为回传闭环3.1 在 Vue 组件中调用 SpringBoot 推荐接口并渲染推荐流Vue 项目基于 Vue 2.7 或 Vue 3 Composition API需通过 Axios 调用 SpringBoot 提供的/api/recommend/user/{userId}接口。以下以 Vue 3script setup为例!-- RecommendSection.vue -- script setup import { ref, onMounted } from vue import axios from axios const props defineProps({ userId: { type: Number, required: true } }) const recommendations ref([]) const loading ref(false) const fetchRecommendations async () { loading.value true try { const res await axios.get(/api/recommend/user/${props.userId}, { params: { k: 8, n: 6 } // 传递算法参数 }) recommendations.value res.data } catch (err) { console.error(获取推荐失败:, err) recommendations.value [] } finally { loading.value false } } onMounted(() { fetchRecommendations() }) /script template div classrecommend-section h3猜你喜欢/h3 div v-ifloading classloading加载中.../div div v-else-ifrecommendations.length 0 classempty暂无推荐/div div classrecommend-grid div v-foritem in recommendations :keyitem.itemId classrecommend-card clickhandleItemClick(item) div classcard-image[图片占位]/div div classcard-title{{ item.title }}/div div classcard-score相关度 {{ item.score.toFixed(2) }}/div /div /div /div /template style scoped .recommend-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } .recommend-card { border: 1px solid #eee; border-radius: 8px; padding: 12px; cursor: pointer; transition: all 0.2s; } .recommend-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); transform: translateY(-2px); } /style注意params: { k: 8, n: 6 }将算法参数透传至后端使前端可动态调整推荐粒度。clickhandleItemClick(item)是后续行为回传的入口不可省略。3.2 实现用户行为回传点击/曝光埋点驱动协同过滤数据更新推荐系统的价值在于闭环——用户点击推荐商品后该行为应立即写入user_behavior表成为下一次推荐的输入。Vue 中需在handleItemClick中触发上报// RecommendSection.vue const handleItemClick (item) { // 1. 前端记录点击事件可选用于 A/B 测试 console.log(用户 ${props.userId} 点击推荐商品 ${item.itemId}) // 2. 上报点击行为到后端 axios.post(/api/behavior, { userId: props.userId, itemId: item.itemId, behaviorType: 2, // 2加购此处按业务定为“点击推荐位” timestamp: Date.now() }).catch(err { console.warn(上报点击行为失败不影响主流程, err) }) // 3. 跳转商品详情页假设路由为 /item/:id router.push(/item/${item.itemId}) }同时SpringBoot 需提供/api/behavior接口接收该数据// BehaviorController.java PostMapping(/api/behavior) public ResponseEntityString recordBehavior(RequestBody UserBehavior behavior) { try { userBehaviorMapper.insert(behavior); // 直接插入数据库 return ResponseEntity.ok(OK); } catch (Exception e) { return ResponseEntity.status(500).body(上报失败); } }提示此处behaviorType设为 2加购或新增类型 4推荐点击需在user_behavior表中behavior_type字段枚举值中提前定义。不要在前端做数据清洗或去重所有校验如用户/商品存在性由后端完成保证数据源头可信。3.3 解决 Vue 打包后布局异常推荐卡片网格在生产环境错行的 CSS 修复Vue CLI 默认打包会压缩 CSS某些grid-template-columns: repeat(auto-fill, ...)写法在旧版浏览器或特定分辨率下可能失效导致推荐卡片堆叠或错位。实测有效的修复方案是显式指定minmax()下限并添加fit-contentfallback/* RecommendSection.vue 内 style */ .recommend-grid { display: grid; /* 替换原写法 */ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); /* 添加兼容性声明 */ grid-auto-flow: row; gap: 16px; } /* 针对 IE11 及部分安卓 WebView 的兜底 */ supports not (display: grid) { .recommend-grid { display: flex; flex-wrap: wrap; } .recommend-card { flex: 0 0 calc(33.333% - 16px); margin-right: 16px; } .recommend-card:nth-child(3n) { margin-right: 0; } }关键点minmax(220px, 1fr)中220px是卡片最小宽度确保在小屏设备上至少显示一列supports规则提供 Flexbox 回退覆盖不支持 Grid 的环境。此方案经测试可在 Chrome 90、Edge 95、iOS Safari 15 及微信内置浏览器稳定运行。4. 协同过滤算法调优与边界验证3 个必调参数、2 类典型失败场景及定位方法4.1 影响推荐质量的 3 个核心参数及其调试策略协同过滤不是“写完就跑通”必须根据实际数据分布调整参数。以下是三个最常需干预的变量附调试建议参数作用默认值调试建议效果验证方式行为权重behavior_type映射将不同行为浏览/加购/下单映射为数值评分浏览1, 加购2, 下单3若下单样本极少5%可提升下单权重至 5避免被加购淹没查看推荐列表中是否出现高单价、低频商品用SELECT COUNT(*) FROM user_behavior WHERE behavior_type3检查下单比例相似度阈值similarity X过滤弱关联用户减少噪声0.1数据稀疏时人均交互商品 5可降至 0.05数据密集时15可升至 0.2统计topKUsers.size()平均值理想区间为 3~8过小则推荐单一过大则引入无关用户Top-K 用户数k参与推荐计算的相似用户数量8若k5时推荐重复率高同一商品频繁出现尝试k12引入更多长尾商品对比k5与k12下推荐列表的 Jaccard 相似度目标值 0.4注意所有参数应在RecommendationService中抽取为Value(${recommend.k:8})注解注入避免硬编码。例如Value(${recommend.similarity-threshold:0.1}) private double similarityThreshold;4.2 两类高频失败场景及精准定位路径4.2.1 场景一推荐列表为空[]但数据库确认有行为数据现象调用/api/recommend/user/123返回空数组而SELECT * FROM user_behavior WHERE user_id123返回多条记录。定位路径在RecommendationService.recommendForUser()方法开头添加日志log.info(Target user {} has {} items, userId, targetUserItems.size());检查targetUserItems.size()是否为 0 —— 若为 0说明buildUserItemRatingMap()过滤掉了该用户所有行为进入buildUserItemRatingMap()检查selectByBehaviorTypeIn(...)查询条件是否误排除了该用户的行为类型如只查[2,3]但该用户只有浏览行为若targetUserItems非空继续检查similarUsers列表大小若为 0说明无用户满足similarity threshold需降低阈值或检查cosineSimilarity计算逻辑重点验证commonItems是否为空。4.2.2 场景二推荐商品 ID 不存在前端渲染报错Cannot read property title of undefined现象Vue 控制台报错recommendations数组中某项item为null或缺失字段。定位路径在recommendForUser()方法末尾添加日志log.info(Generated {} valid recommendations for user {}, candidateScores.size(), userId);检查candidateScores中的itemId是否全部存在于item_info表执行 SQLSELECT COUNT(*) FROM item_info WHERE item_id IN (1001,1002,...)若存在 ID 不匹配说明itemInfoMapper.selectById(entry.getKey())返回 null —— 原因通常是item_id类型不一致数据库 BIGINT vs Java Long或 MyBatisresultMap未正确映射item_id字段强制修复在map操作前增加判空ItemInfo item itemInfoMapper.selectById(entry.getKey()); if (item null) { log.warn(Item not found for id: {}, entry.getKey()); continue; // 跳过该商品 }4.3 验证推荐合理性用「用户 A 的推荐」反查「用户 B 的行为」确认协同逻辑成立最直接的验证不是看推荐是否“好看”而是确认算法是否真在“协同”。操作步骤如下在数据库中找出两个高相似度用户执行 SQLSELECT ub1.user_id as u1, ub2.user_id as u2, COUNT(*) as common_items FROM user_behavior ub1 JOIN user_behavior ub2 ON ub1.item_id ub2.item_id AND ub1.user_id ! ub2.user_id WHERE ub1.behavior_type IN (2,3) AND ub2.behavior_type IN (2,3) GROUP BY ub1.user_id, ub2.user_id ORDER BY common_items DESC LIMIT 1;得到用户对u1101, u2205共同交互商品数 12。手动调用接口GET /api/recommend/user/101?k1n3记录返回的商品 ID 列表如[501, 502, 503]。查询用户 205 的行为SELECT item_id FROM user_behavior WHERE user_id205 AND behavior_type IN (2,3)确认501,502,503是否出现在结果中 —— 若全部存在则证明推荐确实源于用户 205 的偏好协同逻辑成立。此验证法绕过所有抽象封装直击算法本质。答辩时展示此过程比讲一百遍公式更有说服力。本文还有配套的精品资源点击获取
返回列表