尧图精选

SSM旅游平台协同过滤推荐实战:UserCF算法与业务表对接

🕒 发布时间:2026/10/1 3:34:50 📁 来源:尧图网络
简介这是一套面向Java方向毕业设计学习者的完整项目源码基于SSM框架实现协同过滤的在线通用旅游平台网站适合需要完成课程设计或毕业设计、希望掌握前后端整合开发的学生参考。项目包含前端页面、后端逻辑与MySQL数据库脚本功能覆盖景点推荐管理、精选路线管理、用户信息管理以及系统公告、在线留言、站内新闻等系统维护模块其中景点推荐与路线推荐结合协同过滤思路可作为推荐算法入门实践。压缩包共1288个文件约52.7MB以gif、png图片资源jar依赖包xml配置js脚本java源码jsp页面html与css样式为主另含sql数据库文件目录结构完整便于按模块查阅。目前已有58人学习下载读者可据此快速搭建运行环境、理解SSM分层架构与推荐模块实现并在此基础上完成功能扩展与论文撰写。1. 从一份 SSM 旅游平台源码说起协同过滤到底落在哪一层很多 Java 毕业设计选题里「基于协同过滤的在线通用旅游平台」出现的频率高得离谱但真正把推荐算法跑通、而不是只挂个名字的十个里不到三个。我见过太多项目SSM 框架搭得规规矩矩旅游线路、景点、酒店、攻略模块一应俱全可一打开推荐功能要么是「热门线路 Top10」硬编码要么是随机从数据库里捞几条塞进页面协同过滤四个字只活在论文摘要里。这份源码标题里同时出现了 SSM、协同过滤、旅游平台三个关键词说明它想解决的核心问题是在一个典型的 Java Web 三层架构里把用户对旅游产品的行为数据转化成可计算的相似度矩阵再输出个性化推荐列表。适合谁看正在做 Java 课程设计或毕业设计、已经能跑通 SSM 增删改查、但卡在「推荐算法怎么和业务表结构对接」这一步的人。如果你连 MyBatis 的 Mapper 映射都还没写利索建议先把基础 CRUD 跑通再回来否则后面调相似度的时候你会分不清是算法错了还是 SQL 写错了。2. 协同过滤在旅游场景的选型为什么 UserCF 比 ItemCF 更适合起步2.1 旅游产品的消费特征决定了算法倾向旅游平台和电商平台有一个本质区别旅游线路、景点门票、酒店套餐属于典型的低频消费商品。一个用户一年可能只买两三次旅游产品但每次决策前会浏览大量详情页、收藏多个备选、对比价格和行程天数。这意味着用户-物品评分矩阵极其稀疏ItemCF 依赖的物品共现次数会少得可怜——两条线路被同一个用户同时买过的概率太低算出来的相似度全是噪声。UserCF 的逻辑是「找和你口味相似的人看他们还买了什么」在用户行为维度上做文章对物品侧的稀疏性容忍度更高。我一般会建议毕业设计阶段先用 UserCF 把链路跑通因为它的可解释性强推荐结果可以直接说「和你兴趣相似的用户也关注了这条线路」答辩时好讲老师也容易理解。2.2 用户相似度计算的三种常见方案对比选 UserCF 之后下一步是确定相似度度量方式。旅游场景下用户对物品的反馈通常不是显式评分很少有人给景点打 1-5 分而是隐式行为浏览、收藏、下单、分享。不同行为权重不同需要先做行为加权映射。相似度算法适用场景旅游平台落地建议余弦相似度用户行为向量维度高、稀疏首选对稀疏矩阵稳定皮尔逊相关系数用户评分尺度差异大需要显式评分旅游场景不推荐杰卡德相似度只有布尔型交互买/没买行为类型单一时可用但丢失权重信息我一般会先把用户行为映射成 0-5 的隐式评分浏览详情页 1收藏 2加入购物车 3完成下单 5分享 4。然后用余弦相似度计算用户向量之间的夹角。这样做的好处是即使两个用户都只浏览过同一类线路也能通过行为强度区分出兴趣浓度差异。2.3 用 Java 实现用户相似度矩阵的最小代码骨架下面这段代码是在 SSM 的 Service 层里计算用户相似度的核心逻辑不依赖任何第三方推荐库纯 Java 实现方便你直接嵌进毕业设计项目。// UserSimilarityService.java // 输入userId - MapitemId, score 的用户行为评分表 // 输出targetUserId 与所有其他用户的相似度有序列表 public ListUserSimilarity computeSimilarity(Long targetUserId, MapLong, MapLong, Double userItemMatrix) { MapLong, Double targetVector userItemMatrix.get(targetUserId); if (targetVector null || targetVector.isEmpty()) { return Collections.emptyList(); // 冷启动用户走热门兜底 } ListUserSimilarity result new ArrayList(); for (Map.EntryLong, MapLong, Double entry : userItemMatrix.entrySet()) { Long otherUserId entry.getKey(); if (otherUserId.equals(targetUserId)) continue; MapLong, Double otherVector entry.getValue(); // 只对两个用户都产生过行为的物品做点积 double dotProduct 0.0, targetNorm 0.0, otherNorm 0.0; for (Map.EntryLong, Double itemScore : targetVector.entrySet()) { Long itemId itemScore.getKey(); double targetScore itemScore.getValue(); targetNorm targetScore * targetScore; Double otherScore otherVector.get(itemId); if (otherScore ! null) { dotProduct targetScore * otherScore; } } for (Double score : otherVector.values()) { otherNorm score * score; } if (targetNorm 0 || otherNorm 0) continue; double similarity dotProduct / (Math.sqrt(targetNorm) * Math.sqrt(otherNorm)); if (similarity 0) { result.add(new UserSimilarity(otherUserId, similarity)); } } result.sort((a, b) - Double.compare(b.getSimilarity(), a.getSimilarity())); return result; }逻辑说明这段代码的核心是余弦相似度公式分子是两个用户共同行为物品的评分点积分母是各自向量模长的乘积。注意我只对两个用户都产生过行为的物品做点积这是协同过滤的标准做法——如果用户 A 看过线路 1、2、3用户 B 看过线路 2、4、5那么只有线路 2 参与相似度计算。参数方面userItemMatrix建议在项目启动时从数据库一次性加载到内存用ConcurrentHashMap做缓存避免每次推荐都查库。如果用户量超过五千这个全量遍历会变慢可以考虑用倒排索引先筛出有共同行为的用户集合再算相似度。3. 把算法接进 SSM 业务表数据建模与推荐接口落地3.1 旅游平台需要哪几张核心表来支撑推荐很多同学拿到 SSM 源码后发现用户行为数据散落在浏览记录表、收藏表、订单表里没有统一的口径。我的做法是建一张user_behavior宽表把不同来源的行为归一化写入推荐模块只读这一张表。-- 用户行为汇总表推荐模块的数据源 CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, item_id BIGINT NOT NULL COMMENT 旅游产品ID线路/景点/酒店, item_type TINYINT NOT NULL COMMENT 1-线路 2-景点 3-酒店, behavior_type TINYINT NOT NULL COMMENT 1-浏览 2-收藏 3-加购 4-分享 5-下单, behavior_score DECIMAL(3,1) NOT NULL COMMENT 行为权重分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_item (item_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的设计要点behavior_score直接存加权后的分数而不是在推荐计算时再映射这样算法层拿到的就是干净的数值矩阵。item_type字段是为了后续做多品类混合推荐留的扩展位毕业设计阶段可以只用线路数据但表结构先留好。索引建在user_id和item_id上因为推荐计算时两种查询模式都会出现。3.2 推荐接口的 Controller 层怎么写才不拖慢页面推荐结果不应该阻塞主流程渲染。我一般会把推荐接口设计成异步加载页面主体先返回推荐模块用 Ajax 单独请求。// RecommendController.java RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private RecommendService recommendService; GetMapping(/user/{userId}) public ResultListTravelItemVO recommendForUser( PathVariable Long userId, RequestParam(defaultValue 10) int topN) { // 先查缓存缓存未命中再走算法 ListTravelItemVO cached recommendService.getFromCache(userId); if (cached ! null !cached.isEmpty()) { return Result.success(cached.subList(0, Math.min(topN, cached.size()))); } ListTravelItemVO items recommendService.recommend(userId, topN); return Result.success(items); } }逻辑说明Controller 层只做参数校验和缓存判断真正的相似度计算和结果排序放在 Service 里。topN默认 10前端可以传 5 或 20 来调整推荐位数量。缓存建议用 Redis 的 ZSet 结构key 是rec:user:{userId}score 是预测评分这样取 TopN 只需要一条ZREVRANGE命令。如果没有 Redis用 Guava Cache 做本地缓存也行但要注意设置合理的过期时间旅游产品的热度变化比电商慢缓存 30 分钟到 1 小时问题不大。3.3 预测评分与推荐结果生成的完整链路拿到相似用户列表后下一步是预测目标用户对未行为物品的评分。公式是对每个候选物品找到所有对它有行为的相似用户用相似度加权平均他们的行为分。// RecommendService.java 核心推荐逻辑 public ListTravelItemVO recommend(Long userId, int topN) { MapLong, MapLong, Double matrix behaviorRepository.loadUserItemMatrix(); ListUserSimilarity similarUsers similarityService.computeSimilarity(userId, matrix); if (similarUsers.isEmpty()) { return hotItemService.getHotItems(topN); // 冷启动兜底 } // 取相似度最高的 K 个用户K 一般取 20-50 int k Math.min(30, similarUsers.size()); ListUserSimilarity topKUsers similarUsers.subList(0, k); MapLong, Double itemScoreMap new HashMap(); MapLong, Double similaritySumMap new HashMap(); for (UserSimilarity us : topKUsers) { MapLong, Double otherBehavior matrix.get(us.getUserId()); if (otherBehavior null) continue; for (Map.EntryLong, Double entry : otherBehavior.entrySet()) { Long itemId entry.getKey(); if (matrix.get(userId).containsKey(itemId)) continue; // 已行为过的不推 itemScoreMap.merge(itemId, us.getSimilarity() * entry.getValue(), Double::sum); similaritySumMap.merge(itemId, us.getSimilarity(), Double::sum); } } // 加权平均得到预测评分 ListMap.EntryLong, Double ranked itemScoreMap.entrySet().stream() .map(e - new AbstractMap.SimpleEntry(e.getKey(), e.getValue() / similaritySumMap.get(e.getKey()))) .sorted((a, b) - Double.compare(b.getValue(), a.getValue())) .limit(topN) .collect(Collectors.toList()); return travelItemRepository.findByIds(ranked); }逻辑说明k值控制参与推荐的相似用户数量太小推荐结果不稳定太大则引入噪声且计算变慢。30 是一个经验值你可以根据自己数据集的规模调整。itemScoreMap累加的是「相似度 × 行为分」similaritySumMap累加的是相似度本身最后相除得到加权平均预测分。注意这里跳过了目标用户已经行为过的物品避免重复推荐。如果推荐结果不足 topN可以用热门物品补齐。4. 避坑与排查协同过滤在毕业设计里最容易翻车的五个点4.1 现象推荐结果永远不变刷新页面还是那几条原因用户行为数据没有实时写入user_behavior表或者推荐结果被永久缓存了。很多 SSM 项目里浏览记录是异步写的但异步线程池配置有问题任务根本没执行。解决先确认user_behavior表有没有新数据插入用SELECT COUNT(*) FROM user_behavior WHERE user_id ?查一下。如果数据在检查缓存过期时间把 Redis 的 TTL 设成 1800 秒而不是 -1。如果是异步写入问题把Async注解所在的方法改成同步调用测试一下确认是线程池的问题再调线程池参数。4.2 现象相似度计算报 NaN 或 Infinity原因某个用户的行为向量模长为零或者两个用户没有任何共同行为物品时点积为零分母为零导致除零异常。解决在计算相似度之前加两个判断——targetNorm 0 || otherNorm 0时直接跳过dotProduct 0时相似度记为零而不是继续除。上面代码里已经做了这两个防护但如果你自己改写了逻辑很容易漏掉。4.3 现象推荐出来的全是冷门线路热门线路一条都不推原因UserCF 倾向于推荐长尾物品因为相似用户群体中偶然行为会放大冷门物品的权重。这在旅游场景下是致命的用户点进去发现线路没人买、评价为零直接关页面。解决在最终排序前加一个热度惩罚项或者对预测评分做加权finalScore predictScore * 0.8 hotScore * 0.2。hotScore可以用线路的浏览量或订单量归一化得到。这样既保留个性化又不会推太离谱的东西。4.4 现象用户量一上来推荐接口响应超过三秒原因每次请求都全量加载user_behavior表并重新计算相似度矩阵用户量过千后内存和 CPU 都扛不住。解决把相似度矩阵的计算做成定时任务比如每小时跑一次结果存到 Redis 或本地缓存。推荐接口只读缓存结果不实时计算。如果用户量更大可以只计算活跃用户的相似度不活跃用户走热门兜底。4.5 现象新注册用户没有任何推荐结果原因冷启动问题新用户没有行为数据相似度计算返回空列表。解决新用户首次访问时推荐热门线路 Top10同时在前端埋点记录浏览行为。等用户产生至少 3 条行为记录后再切换到个性化推荐。这个切换阈值可以配置在application.yml里方便调整。5. 让推荐结果可解释一个提升答辩通过率的小技巧协同过滤最被诟病的就是「黑匣子」——你推了这条线路但说不出为什么。在毕业设计答辩里老师大概率会问「你这个推荐结果怎么来的」如果你只能回答「算出来的」分数不会高。我的做法是在推荐结果里附带推荐理由字段用相似用户的共同行为来解释。具体实现是在RecommendService返回结果时对每个推荐物品记录「哪些相似用户行为过它」以及「这些用户的相似度是多少」。前端展示时取相似度最高的那个用户的行为类型生成一句话文案。// 在推荐结果中附加推荐理由 public TravelItemVO buildWithReason(Long itemId, Long userId, ListUserSimilarity topKUsers, MapLong, MapLong, Double matrix) { TravelItemVO vo travelItemRepository.findById(itemId); double maxSim 0.0; Long mostSimilarUser null; for (UserSimilarity us : topKUsers) { MapLong, Double behavior matrix.get(us.getUserId()); if (behavior ! null behavior.containsKey(itemId)) { if (us.getSimilarity() maxSim) { maxSim us.getSimilarity(); mostSimilarUser us.getUserId(); } } } if (mostSimilarUser ! null) { vo.setRecommendReason(与你兴趣相似的用户也关注了这条线路); } else { vo.setRecommendReason(热门推荐); } return vo; }这段代码的逻辑很简单遍历 TopK 相似用户找到对当前推荐物品有行为的那个相似度最高的用户如果存在就给出「相似用户也关注」的理由否则标记为热门推荐。maxSim和mostSimilarUser两个变量分别记录最高相似度和对应的用户 ID。这个理由不需要暴露具体用户信息只做文案展示既保护隐私又让推荐结果有据可依。还有一个进阶玩法把推荐理由按行为类型细分。如果相似用户是「下单」行为文案写「相似用户已预订」如果是「收藏」行为写「相似用户收藏了这条线路」。这样推荐位的点击率通常能提升一截。我在自己的项目里试过加上行为类型细分后推荐模块的点击率从 4% 左右涨到了 7%对毕业设计来说这个数据足够在答辩时讲一讲了。最后说一个我踩过的坑不要为了追求算法复杂度而把简单问题搞复杂。旅游平台的推荐场景UserCF 加上热门兜底和缓存已经能覆盖 90% 的需求。我见过有同学非要在毕业设计里上矩阵分解结果调参调了两个月最后推荐结果还不如热门榜单。先把协同过滤的完整链路跑通把数据埋点做扎实把推荐理由展示出来这些看得见摸得着的东西比算法本身更能决定你的项目能不能拿高分。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →