Spring Boot旅游推荐系统毕设:协同过滤算法与实战指南
2026/9/8 4:53:31 网站建设 项目流程

每年到这个时间点,总有一批大四的同学开始对着毕设选题列表眉头紧锁。选个简单的怕过不了关,选个复杂的又担心自己搞不定。如果你正在这个状态里,又刚好对Java后端开发有点基础,那么"基于Spring Boot的旅游推荐系统"这个题目,其实是一个性价比非常高的选择——它既不是那种随便找个CRUD就糊弄过去的"管理系统"式毕设,也没有卷到需要动辄分布式微服务或者深度学习的程度。它在"工作量可见"和"技术含量可讲"之间,有一个非常适合本科生答辩的平衡点。

这篇文章我不会给你堆概念,而是以我实际指导过多个类似项目的经验,把这个系统从选题逻辑、技术选型、协同过滤算法落地、数据库设计,到答辩准备和实际部署中的坑,从头到尾拆一遍。不管你手里是已经有了源码在琢磨怎么消化透,还是想完全从0自己写一个,这篇文章的思路都可以直接拿来用。

1. 毕设选题背后的真实计算:为什么旅游推荐系统能"稳中带亮点"

先说实话,每年毕设管理系统泛滥成灾,一个班里可能有十几个"XX管理系统"。而旅游推荐系统的差异化,恰好在于它往传统的增删改查之上,加了一个"推荐算法"的层。这个点非常微妙——对老师来说,它证明你不是只会写接口调CRUD;对你自己来说,协同过滤算法的复杂度又完全在本科生的可控范围内。所以这个题目能在众多管理系统中一眼被看到,靠的就是这个"算法亮点"。

但需要清醒的一点是,毕业论文写作和实际开发不一样。论文需要的是"分模块阐述问题与分析策略",而系统开发需要的是"把功能真实跑通"。我见过不少同学在开题时把推荐算法吹得特别玄乎,结果系统做完了算法部分就一个简单的"按评分排序",最后答辩时被老师追问几个推荐效果的问题就卡住了。所以如果你选了这个题目,一开始就要把"推荐"这件事当成系统的灵魂来认真对待——宁可在其余模块上少花点精力,推荐模块也要做得有层次感。

这个题目的知识覆盖范围也值得展开说说:首先是Java Web的基础内容——Spring Boot整合、依赖注入、分层架构(Controller、Service、Mapper);然后是数据库设计——因为你涉及用户、景点、评分、评论、收藏等多个实体,表关系比普通的管理系统要复杂一些;再然后是算法层面的实践——相似度计算、矩阵构建、排序取TopN,这些都能在论文的"系统核心设计"章节里大书特书。一个毕设能覆盖这三层,无论放在哪个答辩组都不算虚。

从工作量拆分上看,一个标准的旅游推荐系统毕设大致由四块构成:用户与权限模块、旅游景点的信息管理模块(含后台管理)、用户行为采集模块(评分、收藏、评论、浏览记录)、推荐引擎模块。前两块占用的开发时间大约40%,行为采集大约20%,推荐引擎需要集中精力去啃和调试的也是约40%。这样拆完之后你会发现,真正让人发愁的只有推荐引擎那一块,剩下的都属于熟练工种。

2. 技术选型的权衡:不用盲目追新,稳定和熟悉才是王道的底层逻辑

确定题目之后,下一步就是选技术栈。很多同学容易犯一个毛病——一看网上别人用了什么就想跟着用,根本不管自己会不会。实际上毕设选型的第一原则是"你熟悉什么就用什么,不要为了图新而给自己埋雷"。在这个原则下,我结合目前市面上最常见的毕设技术组合,给你逐一拆一下利弊。

2.1 后端框架:Spring Boot 2.7.x 是当下的安全牌

现在Spring Boot已经出到3.x了,但有一个现实问题:3.x强制要求JDK 17+,而且有些第三方组件的兼容性还没有完全跟上。对于毕设来说,除非你是在校期间已经玩熟了JDK 17的新特性,否则我建议你还是用Spring Boot 2.7.x配合JDK 1.8。原因很实际:在座的各位如果是想在自己电脑上复现项目,JDK 8是绝大多数环境变量教程里的默认版本,装好即用;同时在答辩的现场演示环境里,老师用的机器未必装的是最新JDK,JDK 8的安全系数最高。另一个潜在的好处是,网上你能够搜到的Spring Boot教程、遇到的报错解决方案,十有八九都是基于2.x版本的,遇到问题你更容易找到参考。

版本确定了,配套的依赖也就能跟着确定下来。如果你用的是Spring Boot 2.7.x,对应的Spring Framework是5.3.x,MyBatis Plus要用3.5.x左右的版本,MySQL驱动用8.0.x,这些组合是我实测过很多次、几乎不会出现兼容性问题的稳定搭配。

2.2 ORM框架:MyBatis Plus 为什么比 JPA 更适合毕设

关于数据访问层,有两条路:Spring Data JPA和MyBatis / MyBatis Plus。我的建议很明确——优先选MyBatis Plus。原因不是JPA不好,而是JPA的"自动建表、实体关联、懒加载"这些特性,需要你对持久化机制有更深入的理解。一旦实体关联配置不当,很容易出现N+1查询、懒加载异常(比如在Controller层访问关联对象时抛出LazyInitializationException)等莫名其妙的问题。而MyBatis Plus的思路非常直白——表结构自己建,XML或注解里写SQL,你能清晰地知道每一条SQL在执行什么,出了问题也容易定位。

尤其是推荐模块里有一段"查询用户评分数据"和"相似度计算"的逻辑,用MyBatis Plus你可以直接写自定义SQL一次性查出用户的行为记录,非常直观。如果在JPA里绕来绕去,反而影响你做算法的精力。还有一点我觉得值得提:MyBatis Plus内置了分页插件,你在做景点列表和后台管理分页查询时,只需配置一个拦截器然后调用Page对象,几乎零成本。

2.3 数据库与缓存:MySQL 8.0 是基石,Redis 是加分项

数据存储上,MySQL 8.0已经是绝对主流。设计表时需要注意一点:8.0默认的字符集是utf8mb4,这在存储表情符号和生僻字时不会出现乱码。虽然景点名称一般用不到表情符号,但用户评论里可什么都会有,所以建库时一定要指定utf8mb4

至于Redis,它是典型的"加分项但非必须项"。引入Redis不是让你炫技,而是有几个场景确实合适:一是热门景点排行的数据在短时间内的变化不大,放缓存可以减少数据库压力;二是协同过滤计算出来的推荐结果,可以以userId为key缓存一段时间,不用每次都重新计算矩阵;三是登录态Token的存储,用Redis做自动过期比传统的Session更优雅。如果你决定引入Redis,哪怕毕业设计只需要用到其中一个场景,也要在论文里把这个设计写上——老师普遍认这个。

2.4 前端方案:Vue 前后端分离更出效果

前端是另一个纠结的地方。很早以前的毕设喜欢用Thymeleaf做服务端渲染,一套Spring Boot直接搞定。这种方式适合不会前端又想省事的同学。但如果你指望答辩现场效果稍微好一点,我建议用Vue 3 + Element Plus做一个独立的前端工程,后端只提供JSON接口。前后端分离的项目在查看时给人的"工作量感"和"专业感"是完全不同的,页面上也不容易透出那种"这是套模板"的气息。而且Vue本身的学习曲线不算陡,配合现成的后台管理模板,两个星期出页面并不难。

生产环境的落地方式一般是:前端npm run build打包生成静态文件后,可以扔到Nginx里做反向代理,也可以直接把dist目录扔到Spring Boot的resources/static下,由后端统一提供服务。后一种在毕设演示时更省事——一条命令启动Spring Boot,前后端就都跑起来了,不会出现“前端npm启动超时”这种让你在答辩现场冒冷汗的尴尬。

3. 推荐引擎实战:把协同过滤从数学公式变成可运行的Java代码

推荐模块是整个系统最核心的部分,也是你论文里最大的亮点。很多同学在这一步容易翻车,原因是他们从网上找来的协同过滤算法代码是Python写的,而自己的系统是Java后端,最后只能硬着头皮用Python单独跑一个微服务——这样一来,代码复杂度直线上升,部署时又要配Python环境,问题很多。我的建议是:别偷懒,直接用Java把协同过滤算法实现一遍。这个算法本身逻辑并不复杂,500行代码足够了。

3.1 两种协同过滤怎么选:基于用户还是基于物品

协同过滤分为两大类:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。它们各自的思路用大白话说——UserCF是"和你兴趣相似的人喜欢的景点,你也可能喜欢";ItemCF是"和你之前喜欢过的景点相似的景点,你也可能喜欢"。

那么在旅游场景里应该选哪种?这里有一个经验:UserCF在用户数较少、物品相对固定的场景下效果更明显,但一旦用户规模变得非常大,计算用户两两之间的相似度会非常耗时;ItemCF则更稳定,计算的是物品之间的相似度,而旅游景点的数量通常远小于用户数量,且物品特征相对稳定。所以在毕设的旅游推荐系统里,我推荐以ItemCF作为主力算法,同时保留UserCF作为对比(答辩时可以说你调研过两种方式,最终根据场景数据量选择了ItemCF)。

3.2 核心步骤拆解:从数据到推荐的完整计算链路

ItemCF的实现链路分为三步:构建物品共现矩阵、计算物品相似度、根据用户历史行为生成推荐列表。我分别展开说。

第一步,从数据库里把用户的评分、收藏、点击行为查出来,形成一个"用户-物品"评分矩阵。需要说明的是,旅游场景里用户可能很少主动打分,所以行为数据可以融合成隐含评分:收藏计2分、点击浏览计1分、评论计2分,按比例汇总到一个0到5的分数区间,作为矩阵的值。没有行为的记为0。

第二步是计算物品相似度。经典的实现是余弦相似度。打个比方,想象有两个景点A和B,它们分别被一系列用户产生过行为,每个用户可以看作一个维度,那么A和B就是这个高维空间里的两个向量。余弦相似度就是计算这两个向量夹角的余弦值,越接近1说明方向越一致,也就是说同时喜欢/排斥它们的用户群体越重叠,两个景点就越相似。公式长这样:

similarity = (A · B) / (|A| * |B|)

在Java里,可以用Map来表示稀疏向量,不用真的开一个二维数组。比如:

public Map<Integer, Double> buildUserRatingVector(List<UserAction> actions) { Map<Integer, Double> vector = new HashMap<>(); for (UserAction action : actions) { vector.merge(action.getUserId(), action.getScore(), Double::sum); } return vector; } public double cosineSimilarity(Map<Integer, Double> vectorA, Map<Integer, Double> vectorB) { Set<Integer> union = new HashSet<>(vectorA.keySet()); union.retainAll(vectorB.keySet()); double dotProduct = 0; for (Integer userId : union) { dotProduct += vectorA.get(userId) * vectorB.get(userId); } double normA = 0; for (Double value : vectorA.values()) { normA += value * value; } double normB = 0; for (Double value : vectorB.values()) { normB += value * value; } if (normA == 0 || normB == 0) return 0; return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }

第三步,针对当前用户,找出他有过正反馈行为的景点列表,再根据物品相似度矩阵计算他未见过的景点得分。

public List<Integer> recommendForUser(Integer userId, List<ScenicSpot> spots, Map<Integer, Map<Integer, Double>> similarityMatrix) { Map<Integer, Double> userInteractedScores = getUserActionScoreMap(userId); Map<Integer, Double> recommendScores = new HashMap<>(); for (Map.Entry<Integer, Double> interacted : userInteractedScores.entrySet()) { Integer spotId = interacted.getKey(); Double userScore = interacted.getValue(); Map<Integer, Double> simMap = similarityMatrix.get(spotId); if (simMap == null) continue; for (Map.Entry<Integer, Double> simEntry : simMap.entrySet()) { Integer candidateSpotId = simEntry.getKey(); if (userInteractedScores.containsKey(candidateSpotId)) continue; recommendScores.merge(candidateSpotId, userScore * simEntry.getValue(), Double::sum); } } return recommendScores.entrySet().stream() .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed()) .limit(10) .map(Map.Entry::getKey) .collect(Collectors.toList()); }

这段逻辑说白了就是:用户越喜欢的景点,它周围相似的景点越应该被推给用户。实际开发中,为了防止某个极端热门景点跟所有景点都"相似"从而霸榜,你可以给相似度乘一个Math.log(1 + itemCount)之类的降权因子,或者过滤掉相似度低于阈值的候选结果。

3.3 新用户冷启动:推荐系统里最容易答不上的问题

答辩时老师最爱问的一个问题就是:"如果这个用户是刚注册的新用户,没有任何行为数据,你怎么推荐?"很多同学没有想过这个问题,当场就愣住了。冷启动的应对方案一定要提前准备好——最简单的方案是:做一个热度兜底。比如统计所有景点在近30天的浏览次数、收藏次数、评分人数,按综合热度排序,推荐Top10。这个逻辑实现起来不超过50行,却能让你的推荐系统在"任何用户进来都有东西可推"这件事上闭环。

更进一步,你还可以做"基于规则的初筛",比如根据用户注册时填写的偏好分类(想看自然风景的、喜欢历史人文的、偏悠闲度假的),在热度推荐的基础之上叠加内容筛选。这部分内容写在论文里就是"混合推荐策略",含金量直接上一个台阶。

3.4 推荐结果的评测:论文里的数据从哪来

论文写到"系统测试"一章时,很多人只会贴接口测试结果,这其实不够亮。一个加分的做法是给出推荐算法的离线和在线评测数据——比如用Precision@10和Recall@10来评估推荐准确率。操作方式是把已有的用户行为数据按照8:2分为训练集和测试集,用训练集计算相似度矩阵,然后在测试集上预测用户的偏好,对比预测的Top10列表里有多少物品是用户在测试集中真实产生过行为的。精确率就是"推荐列表里用户真正感兴趣的占了推荐总量多少",召回率是"推荐列表里用户真正感兴趣的占用户全部真正感兴趣总量的多少"。这两个指标跑出来,哪怕只是50%~60%的精确率,放到论文里作分析,也远比你空口说"这个系统推荐效果不错"有说服力得多。

4. 系统模块与数据库设计:旅游推荐系统不是只有推荐

推荐算法再亮眼,如果基础模块一团糟,答辩一样会被老师挑剔。接下来我们过一遍系统的功能模块和数据结构。一个完整的旅游推荐系统至少需要这些模块:用户管理模块、景点管理模块(含后台CRUD)、用户行为模块、推荐模块、数据统计模块。

4.1 核心功能模块划分

  • 用户端:注册、登录、个人信息维护、浏览景点列表、搜索景点、查看景点详情、评分、收藏、评论、查看个人推荐列表。
  • 管理端:景点信息新增/编辑/删除/上下架、景点分类管理、用户管理、用户反馈/评论管理、数据统计(如访问量、推荐点击量)。

4.2 数据库表设计的几个关键决策

表的设计直接决定后续开发的顺畅程度。我建议至少设计以下6张表:用户表(sys_user)、景点分类表(spot_category)、景点信息表(scenic_spot)、用户行为表(user_behavior)、收藏表(user_favorite)、评论表(comment)。

  1. 用户表:user_id(主键,自增)、usernamepassword(BCrypt加密)、nicknameavatar_urlpreference(可选,存用户偏好的分类ID,用逗号分隔)、create_timestatus(是否禁用)。

  2. 景点分类表:category_idcategory_namedescription

  3. 景点信息表:spot_idspot_namecategory_id(关联分类)、description(景点详细介绍)、addresscover_image(封面图URL)、tags(用逗号分隔,如"亲子,爬山,5A")、avg_score(平均评分,可以实时计算)、view_count(浏览次数)、status(上架或下架)。

  4. 用户行为表:这张表是整个推荐系统的数据基础,建议做成一张宽表。behavior_iduser_idspot_idbehavior_type(1:浏览,2:收藏,3:评分)、score(评分时才有值)、create_time。为了避免行为表无限膨胀,你可以做一个定时任务定期清理过老的浏览记录。

  5. 收藏表:favorite_iduser_idspot_idcreate_time。理论上行为表里已经能查收藏,但单独一张表方便用户端的"我的收藏"功能直接分页查询。

  6. 评论表:comment_iduser_idspot_idcontentratingcreate_time

这里有个容易忽略的点是"联合唯一索引"。比如用户行为表里的(user_id, spot_id, behavior_type)和收藏表里的(user_id, spot_id),建议都加上唯一索引。这能防止用户反复点击收藏时产生重复数据,也能作为接口幂等性的一层保障。

4.3 核心接口设计规范

接口设计的风格会影响代码的可读性和后续联调效率。我建议统一RESTful风格,返回体封装为一个统一的Result<T>对象,包含code(200成功,其他失败)、messagedata三部分。以下是几个核心接口的示例:

功能请求方式路径备注
用户注册POST/api/user/register用户名唯一性校验
用户登录POST/api/user/login返回Token
景点分页列表GET/api/spot/page支持关键词、分类筛选
景点详情GET/api/spot/{spotId}含评论、相似景点推荐
用户评分POST/api/behavior/score同一用户同一景点只能评一次
添加收藏POST/api/favorite/add重复收藏需返回提示
获取推荐列表GET/api/recommend/{userId}优先走缓存,冷启动走热度
后台景点新增POST/api/admin/spot/add需要管理员权限校验
数据统计GET/api/admin/statistics返回总用户数、景点数、行为总数

接口里需要注意权限问题。管理员接口必须做权限判断,最简单的方式是登录时返回一个包含role字段的JWT Token,然后在后端的拦截器里校验role是否为admin。这虽然只是简单权限,但也体现了你的工程意识。

5. 实际开发与调试中的坑:这些细节直接影响你能不能跑起来

代码写好了是一回事,真正能跑起来又是另一回事。我把自己在多个项目里反复踩过的坑集中整理一下,这些内容你提前看过就可以少走很多弯路。

5.1 环境与版本类坑

我见过最多的报错,都出在"环境不一致"上。比如你本地是JDK 17,项目是Spring Boot 2.x编译的target字节码版本若不一致,启动时会报UnsupportedClassVersionError。所以务必确认IDE里的Project Structure、Maven的compiler插件、以及系统环境变量里的JAVA_HOME三个位置指向同一个JDK。

还有一类经典的坑是数据库连接失败。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,不是老旧的com.mysql.jdbc.Driver;同时URL里建议加上useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,否则会出现时区报错或者SSL警告。如果使用MyBatis Plus,spring.datasource.driver-class-name配置对不上也会启动失败,你需要逐一核对。

5.2 算法数据类坑

算法模块最容易出的问题是"所有用户拿到的推荐结果都一样"。排查思路通常是:先去数据库里确认行为数据是否真实存在——如果测试阶段用户行为太少,相似度矩阵稀疏,推荐结果退化成热门列表是很正常的。我给的建议是:自己在测试阶段写一个"造数据"的单元测试循环,模拟20个用户对100个景点产生随机行为,这样你才能看到算法对不同用户产生差异化推荐的实际效果。造完数据后再去调试相似度计算的阈值,效果明显不一样。

5.3 远程调试与答辩前的部署策略

很多同学的最终处境是"代码在IDE里跑得很欢,一打包部署就挂"。为了避免现场演示翻车,最稳妥的做法是——在答辩前一周就做一次"干净环境模拟"。具体操作是你拿一台没有安装开发工具的电脑(或者虚拟机),只装JDK和MySQL,然后把你的Spring Boot项目打成jar包,用java -jar启动,看看能不能顺利跑起来。这一步能暴露大量依赖、配置、绝对路径、端口占用等所谓"开发环境特有"的问题。等你本地模拟通过了,再把它部署到云服务器上,用IP加端口远程访问一遍所有核心页面,不然你真不知道你的前端打包后请求后端接口的地址写没写对。

远程调试这种需求往往出现在"别人写的代码自己要改"的毕设场景中。拿到源码之后想在本地启动,最容易出问题的往往是配置文件里的数据源信息。如果你遇到一个项目连数据库都连不上,先看application.yml里的MySQL账号密码和本地是否一致;如果工程里携带有SQL脚本文件,先执行脚本把表结构和测试数据导进去,再启动项目。提醒一句,不要一上来就改一堆配置,而是按"数据库→后端→前端"的顺序逐层验证。

6. 论文里的"系统实现"与"系统测试"怎么写才能扛住答辩追问

论文和代码一样重要。很多同学代码实现了十成,论文却写成流水账,最后答辩导师翻了翻PPT觉得"工作量不饱和",这就很亏。实际上,代码之外的呈现能力是毕业设计的高权重隐性评分项。

论文中的"系统设计"章节,不要只是贴接口表格。一个更好的写法是:先画一个架构图——上级是接入层(Vue页面)、中间是后端核心(Spring Boot的Controller/Service/Mapper分层)、底部是数据层(MySQL+Redis),然后在架构图下面逐层展开设计思想,尤其要细化"推荐服务"内部的处理流程:数据加载、矩阵构建、相似度计算、推荐生成、结果缓存。

测试章节也不能只贴快乐路径。你最好附上错误输入的测试结果,比如"未登录用户调用收藏接口,返回401""评分分数超出1-5范围,参数校验失败""数据库断连时,系统返回统一的错误提示而不直接崩溃"等。这些边界测试用例特别能打动答辩老师,因为它说明你想过异常场景,而且处理了。

最后是论文里非常重要的一部分——"系统评估"或"结果分析"。这里把你在第3节跑出来的精确率、召回率放进去,用一两句话分析一下为什么会有这样的结果:如果精确率不够高,可能是行为数据稀疏;如果个性化程度一般,可能是冷启动样本太多。这些分析哪怕浅一些,也足以证明你不是照着别人的论文拼凑出来的,而是真的动手验证过。

7. 一个过来人最后想提醒你的事

如果把旅游推荐系统比作一栋房子,Spring Boot是地基和框架,数据库是水电管线,前后端页面是装修,而协同过滤算法就是这栋房子最显眼的落地窗——如果你能把落地窗擦得又亮又透,整个房子的档次都不一样。我真心建议你,无论你是要自己从零开发,还是手里已经有一份源码需要改成"自己"的项目,都要把推荐算法这一块的原理彻底吃透,确保每一行代码都能解释得清楚。答辩的时候,你从"这个相似度是用余弦公式算的"讲起,比你从"我用了Spring Boot框架"讲起,老师对你的印象完全不同。毕竟去掉技术的包装,毕业设计真正考察的还是你有没有把一个系统从需求分析到落地实现的完整方法论,而算法部分正是这块方法论最锋利的展示窗口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询