news 2026/10/6 8:30:57

SpringBoot3+Vue3旅游景点推荐系统:手写协同过滤全栈实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot3+Vue3旅游景点推荐系统:手写协同过滤全栈实战

最近总有读者和学弟学妹来找我要一套能做毕业设计、又能真正跑起来学东西的项目,我手头刚好整理完一个非常适合零基础入门、又能覆盖全栈核心知识点的作品:基于 SpringBoot3 + Vue3 的在线旅游景点推荐系统。后端用 SpringBoot3 搭 REST 接口,前端用 Vue3 全家桶做单页应用,推荐引擎没有现成框架,是手写的协同过滤算法,整个系统从数据库到推荐结果都有完整闭环。

这个项目的定位很明确:既可以当毕业设计直接答辩,也可以作为实训、课程作业,或者纯粹拿来学习 SpringBoot3 和 Vue3 怎么配合着写一个真实的全栈项目。我不打算只告诉你“代码在哪、怎么跑”,而是把系统的设计思路、算法细节、后端接口、前端页面,以及我调试时踩过的坑全部摊开讲清楚。你照着这篇文章敲一遍,比直接下载一个陌生源码包然后对着报错发呆,收获要大得多。

1. 项目整体设计与技术选型拆解

1.1 旅游推荐到底在做一件什么事

旅游景点推荐和电商推荐不一样。电商商品多、用户行为密集,一个用户可能一天产生几十次浏览;而旅游场景下,用户访问频次低,一个普通用户一年也就规划几次行程,能留下的评分数据少得可怜。如果直接把电商那套“猜你喜欢”搬过来,模型基本学不到有效特征,推荐质量会很差。

所以这个系统的业务模型我做了简化但保留了核心逻辑:用户登录后,可以给去过的景点打分、收藏喜欢的景点、浏览景点详情。这些行为会沉淀成用户对景点的“兴趣信号”,推荐模块从这些信号里计算用户之间的相似度或者景点之间的相似度,再预测用户对某个没去过的景点的偏好程度,生成一个个性化的推荐列表。

一句话概括:这个系统要解决的,是“数据库里有几百个景点,我到底该把哪些推荐给当前用户”的问题。协同过滤的价值就在于不需要给景点人工打标签、也不需要做复杂的画像分析,只靠用户行为数据就能产生推荐,逻辑直观,答辩的时候也特别容易讲清楚。

1.2 为什么选 SpringBoot3 + Vue3 这套组合

技术选型是这个项目最应该被认真对待的决策,因为选错了,后面全是坑。我先说后端。

SpringBoot3 是当前 Java 全栈开发的绝对主流方向,它强制要求 JDK17 起步,并且把原来 javax 命名空间全部迁移到了 jakarta。很多做毕设的同学还在用网上五年前的老教程,代码里写着import javax.servlet.*,在 SpringBoot3 里直接编译报错。用 SpringBoot3 做这个项目,一方面逼着你接触最新的 Java 生态,另一方面也是向面试官或者答辩老师传递一个信号:你用的是经过升级的新技术,而不是十八手的 SSM 老框架。

前端选 Vue3 就更好解释了:Vue3 的 Composition API 结构清晰,配合 Vite 开发服务器启动快到离谱,Pinia 管理状态比 Vuex 轻量得多。现在企业里的 Vue 项目九成以上是 Vue3 + Vite + TypeScript 或者 Vue3 + Vite + JavaScript 的组合,你做完这个项目,简历上写“熟悉 Vue3 全家桶”是有底气说出来的。

整套技术栈如下:

层级选型说明
后端框架SpringBoot 3.x基于 JDK17,迁移到 jakarta 命名空间
持久层MyBatis Plus单表 CRUD 不用写 SQL,适合快速开发
认证方案JWT + 拦截器无状态认证,前后端分离标配
推荐算法手写协同过滤基于物品的 ItemCF,不会被黑盒框架卡住
前端框架Vue3 + ViteComposition API + script setup 写法
状态管理Pinia替代 Vuex,类型友好、体积小
UI 组件Element PlusVue3 官方推荐的组件库
数据库MySQL 8.x建六张核心表,数据量几万条即可

1.3 系统架构与数据流转设计

这个系统是标准的前后端分离架构。浏览器里跑的是 Vue3 单页应用,通过 Axios 调用后端的 RESTful 接口;后端 SpringBoot3 负责接收请求、做认证鉴权、查询数据库、执行协同过滤计算,最后返回 JSON 数据;MySQL 负责持久化用户、景点、评分、收藏、浏览记录等数据。

数据流转我按一个完整场景描述:用户在首页看到景点列表,点进详情页产生一条浏览记录,觉得不错评分并收藏。前端把score和userId、scenicId提交到后端评分接口,后端把评分写入数据库。当用户打开“猜你喜欢”页面时,后端先从数据库捞全部用户的评分数据,构建“用户-景点评分矩阵”,计算景点之间的相似度,再找到当前用户有过行为的景点集合,加权预测出候选景点的评分,排序后取 TopN,通过推荐接口返回给前端渲染。

这条链路没有引入 Redis 缓存、没有消息队列、没有分布式组件,架构非常简单,但对一个毕设级项目来说完整度刚刚好。做一个系统的关键不是堆技术,而是让每一条数据流有来源、有去向、有解释。哪怕答辩老师追问“推荐结果是怎么算出来的”,你也能从数据库到相似度公式一路讲到底。

2. 数据库设计:推荐系统的基础设施

2.1 六张核心表,一个都不能少

推荐系统能不能算出有效结果,一半取决于数据库表结构设计。这个项目我总共设计了六张表,严格来说每一张都有它存在的用途。

用户表sys_user没什么好多说的,存用户名、密码密文、昵称、头像,主键用自增 id。密码必须加密存储,我用的是 BCrypt,千万不能明文进库,这是所有教程都不会反复强调但现实里极其重要的一点。

景点表scenic存储旅游景点的静态信息,字段包括景点名称、所在城市、详细描述、图片 URL、门票价格、开放时间、评分等。这张表是推荐系统的“候选池”,里面的数据越真实,推荐效果被感知得越明显。我从公开数据集里导入了三百多条真实景点信息,覆盖十几个热门旅游城市,展示效果和推荐效果都够用。

评分表user_scenic_score是整个推荐算法的数据基础,核心字段是用户id、景点id、评分值(1到5)。这张表越多数据,协同过滤算出来的结果越稳定。我另外做了数据初始化脚本,预置了几百个用户的模拟评分记录,这样项目第一次启动就能直接看到推荐效果,不用自己辛辛苦苦去点。

浏览记录表user_scenic_history和收藏表user_scenic_favorite服务于两个目的,一是丰富用户行为数据,二是给“猜你喜欢”和“看过/收藏过的景点”页面提供数据源。现实中用户不太会主动打分,但浏览和收藏行为更频繁,这三类行为可以组合成多维度兴趣信号。

2.2 评分数据为什么比浏览数据更值钱

这是个容易被忽略但很关键的点。浏览行为只能说明“用户看到了”,不能说明“用户喜欢”;评分和收藏则直接表达了用户的偏好强度。在协同过滤算法里,我们要计算的是用户对物品的偏好相似度,如果你把“浏览量 + 收藏 + 评分”混在一起算相似度,需要给不同类型的行为赋权重,逻辑复杂度会上去不少。

所以我第一版实现里,相似度计算的输入只用了评分表的 1 到 5 分显式数据,浏览和收藏单独用来做“你可能也喜欢”的补充推荐和场景展示。等你把最简单的版本跑通之后,再去扩展“浏览计 1 分、收藏计 4 分、评分用实际分值”的混合加权模型,梯度学习会非常舒服。

提示:建表时记得给user_scenic_score加联合唯一索引(user_id, scenic_id),否则同一用户对同一景点重复评分会出现多条记录,算法算出来的相似度矩阵直接失真。

3. 协同过滤算法的 Java 实现细节

3.1 基于物品的协同过滤(ItemCF)为什么更适合旅游场景

协同过滤分为两大类:基于用户的 UserCF 和基于物品的 ItemCF。UserCF 的思路是“找到和我口味相似的用户,把他喜欢的推荐给我”;ItemCF 的思路是“找到和我历史上喜欢物品相似的物品,把它推荐给我”。

电商领域 UserCF 曾经很流行,但旅游场景我更推荐 ItemCF,原因有三点。第一,用户相似度计算代价高。旅游用户数量大、活跃度低,用户间共同评分覆盖度极低,算出来的相似度往往是 0,推荐效果还不如热门榜单。第二,物品相似度相对稳定。景点是相对静态的物品,泰山和华山永远相似,但用户兴趣是变化的,ItemCF 可以在用户产生新行为后立刻调整推荐结果,解释性也更强。第三,推理逻辑更好讲。当你向答辩老师解释“用户喜欢杭州西湖,西湖和灵隐寺相似度高,所以推荐灵隐寺”时,这套因果关系几乎不需要额外铺垫。

核心公式是余弦相似度:

similarity = (用户A对景点的评分向量 · 用户B对景点的评分向量) / (向量A的模 × 向量B的模)

放在 ItemCF 语境下,“向量A”就是所有用户对景点A的评分集合,“向量B”是所有用户对景点B的评分集合。如果两个景点经常被同一批用户打高分,它们之间的余弦相似度就会趋近于 1;如果几乎没有共同评分的用户,相似度就是 0。

3.2 余弦相似度计算:原理与 Java 代码

我见过很多同学做协同过滤直接调 Python 里的 sklearn 或者 surprise 库,模型是能出来,但一问原理就支支吾吾。这个项目我用 Java 手写了整个算法,核心代码加起来不到一百行,逻辑完全透明。

先建一个“景点-用户评分表”的倒排索引:

// 从评分表构建倒排索引:景点 id -> Map<用户id, 评分> public Map<Long, Map<Long, Double>> buildItemUserMap(List<ScoreRecord> records) { Map<Long, Map<Long, Double>> itemUserMap = new HashMap<>(); for (ScoreRecord record : records) { itemUserMap .computeIfAbsent(record.getScenicId(), k -> new HashMap<>()) .put(record.getUserId(), record.getScore()); } return itemUserMap; }

然后写核心的相似度计算:

public double cosineSimilarity(Map<Long, Double> userScoresA, Map<Long, Double> userScoresB) { if (userScoresA == null || userScoresB == null || userScoresA.isEmpty() || userScoresB.isEmpty()) { return 0.0; } double dotProduct = 0.0, normA = 0.0, normB = 0.0; for (Double score : userScoresA.values()) { normA += score * score; } for (Double score : userScoresB.values()) { normB += score * score; } for (Map.Entry<Long, Double> entry : userScoresA.entrySet()) { Double scoreB = userScoresB.get(entry.getKey()); if (scoreB != null) { dotProduct += entry.getValue() * scoreB; } } if (normA == 0.0 || normB == 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }

这段代码的核心逻辑就是套公式:遍历景点A的评分向量算出模长,再遍历景点B的评分向量算出模长,最后把两个向量中共现用户的分值乘积累加,除以两个模长的乘积。注意一旦某个评分向量的模长为 0,直接返回 0,防止除零异常。

3.3 评分预测与 TopK 推荐列表生成

景点之间的相似度矩阵算出来之后,还只是完成了第一步。要给用户推荐,还需要结合“用户历史上喜欢的景点”进行加权预测。

预测用户 u 对景点 p 的评分公式如下:

pred(u, p) = sum( similarity(p, i) * score(u, i) ) / sum(abs(similarity(p, i)))

其中i是用户 u 评过分的景点集合中的景点,similarity(p, i)是景点 p 和景点 i 的相似度,score(u, i)是用户 u 对景点 i 的历史评分。这个公式的逻辑很直观:我一个历史评分很高的景点,它的相似景点应该得到更高的预测分;而相似度越低的景点,对预测分的贡献越小,且做了归一化处理。

生成推荐列表的代码流程:

public List<RecommendResult> recommend(Long userId, int topN) { // 1. 获取当前用户评分过的景点列表 Map<Long, Double> userScoredItemMap = scoreMapper.selectByUserId(userId) .stream().collect(Collectors.toMap(ScoreRecord::getScenicId, ScoreRecord::getScore)); // 2. 遍历所有景点,跳过用户已经评过分/已收藏的 Map<Long, Double> predictScoreMap = new HashMap<>(); for (Scenic candidate : scenicMapper.selectList(null)) { if (userScoredItemMap.containsKey(candidate.getId())) { continue; } double totalWeight = 0.0, weightedScore = 0.0; for (Map.Entry<Long, Double> entry : userScoredItemMap.entrySet()) { double sim = similarityMatrix.get(candidate.getId(), entry.getKey()); weightedScore += sim * entry.getValue(); totalWeight += Math.abs(sim); } if (totalWeight > 0) { predictScoreMap.put(candidate.getId(), weightedScore / totalWeight); } } // 3. 按预测分倒序,取 TopN return predictScoreMap.entrySet().stream() .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .limit(topN) .map(e -> new RecommendResult(e.getKey(), e.getValue())) .collect(Collectors.toList()); }

这里面有一个非常容易被忽视的细节:必须跳过用户已经评过分或者收藏过的景点。否则你就会看到一个用户刚给西湖打了 5 分,推荐列表里西湖又出现了。这在答辩演示时非常尴尬。

3.4 冷启动问题的处理

冷启动是推荐系统永远绕不开的话题,也是答辩老师最爱问的问题:新用户没有评分数据,协同过滤怎么推荐?

我在这套系统里处理了两类冷启动。第一类是新用户冷启动:没有历史评分时,直接返回全局热门景点 TOP10,按景点浏览量和平均评分加权排序。第二类是新景点冷启动:一个新景点没有任何评分记录,无法参与相似度计算,我在推荐候选池里默认加了一个“最新景点头像”的补充列表,保证页面不空白。

实操心得:新用户冷启动逻辑务必写进推荐接口的判断分支里。如果你不注意,用户第一次登录就去请求推荐接口,后端返回的必然是一个空列表,前端弹不出任何数据,体验直接归零。

4. SpringBoot3 后端实战与避坑指南

4.1 SpringBoot3 的命名空间变化,老代码迁移的噩梦

如果你是从零开始写这个项目,SpringBoot3 的坑你可能感受不深;但如果你像我一样习惯搜网上的参考代码,你会非常痛苦。

SpringBoot3 最大的变化是把 Java EE 规范从javax.*迁移到了jakarta.*。这意味着网上所有基于 SpringBoot2 的代码里,import javax.servlet.*这类引入全部失效,必须改成import jakarta.servlet.*。我第一次写的时候没注意,照着一段老代码敲,编译器直接给我标红了十几个javax开头的红色波浪线,查了半天才发现是包名变了。

第二坑是 Spring Security 6 的配置方式变了。如果你只看过 SecurityConfig 配置失效方案的老教程,里面重写configure(AuthenticationManagerBuilder auth)方法的方式已经废了,现在要用@Bean方式声明UserDetailsService和PasswordEncoder。推荐系统的管理员登录模块如果做不好授权配置,拦截器会把静态资源也拦截掉,页面加载 CSS 和图片全部 404,这种情况我遇到好几次。

第三坑是 Maven 依赖坐标变了。SpringBoot3 的 starter 版本要明确指定3.x.x,比如spring-boot-starter-web如果还引 2.x 的父工程,项目跑起来会提示循环依赖或者自动配置不生效。

4.2 推荐接口的完整实现流程

推荐接口的代码并不复杂,但请求链路很长,我按执行顺序拆解一遍。

用户的请求进来先被 JWT 拦截器拦截,从请求头Authorization里取出 Token,解析出 userId,放入当前请求上下文。然后进入RecommendController的/api/recommend/top接口,接口内部先判断 userId 是否有评分记录,没有就直接查热门景点 TOP10;有评分记录则走 ItemCF 完整流程:加载原始评分表 → 构建倒排索引 → 计算相似度矩阵 → 过滤已评分的景点 → 预测评分 → 排序取 TopN。

这里要特别说明相似度矩阵的计算时机的设计。假设景点有 500 个,每次推荐都现算一个 500x500 的相似度矩阵,用户一多,接口必卡。我的做法是首次启动时预计算全量相似度矩阵存到内存里,之后每次推荐直接查表。因为景点数据相对静态化,这种缓存策略是完全合理的。如果你有更高追求,可以把这个矩阵写入 Redis,重启也不用重新算,但毕设级项目放内存已经够用。

返回给前端的推荐数据我用 DTO 封装,别直接把实体类抛出去。实体类里包含数据库字段、逻辑删除标志位、还有 MyBatis Plus 自动填充的时间字段,直接序列化不仅数据冗余,还可能把敏感字段泄露出去。

4.3 JWT 认证与登录身份上下文

JWT 认证是前后端分离项目的基本功。我用 JJWT 库生成 Token,登录成功后把 userId 和用户名塞进 claims,设置两小时过期时间。后端写了一个JwtInterceptor,在preHandle里校验 Token 的签名和过期时间,解析出来的 userId 存放在ThreadLocal的UserContext对象里,推荐接口直接从UserContext.getUserId()拿当前用户,不用每个接口手写解析逻辑。

前后端联调时最容易出现的问题有两个。

第一个是前端请求没有带 Token。用户好好登录了,但是 Axios 拦截器里忘了从 localStorage 取 Token 放到请求头,后端拦截器每次都是 401,前端还没提示,控制台看着像跨域。第二个是跨域配置问题。SpringBoot3 的跨域配置类要继承WebMvcConfigurer,重写addCorsMappings,允许http://localhost:5173这个来源。Vite 默认端口是 5173,不是 8080,如果后端代码还写 8080 的允许跨域配置,前端就是死活调不通。

提示:生产环境不应该全放开allowedOrigins("*"),配合allowedMethods("*")一起放开是典型的安全隐患。但是这个项目是教学用途,阶段目标是把功能跑通,我只能提醒你:上线前一定要收紧跨域策略。

5. Vue3 前端实现与前后端联调

5.1 Vue3 项目初始化与关键依赖配置

前端我用 Vite 初始化一个 Vue3 项目,操作非常简单:执行npm create vue@latest,选择 JavaScript 而不是 TypeScript(零基础读者最好先不要上 TS,先把逻辑跑通),勾选 Vue Router 和 Pinia 选项,一个干净的骨架就出来了。

项目运行起来之后,我先装 Element Plus 和 Axios。Element Plus 的引入方式我选全量引入,毕设项目没必要做按需加载优化,全量引入最省心:

import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' app.use(ElementPlus)

然后是封装 Axios。这个环节非常重要,我在src/utils/request.js里创建 axios 实例,baseURL设置为http://localhost:8080/api,请求拦截器里从 localStorage 取 Token 放到 Authorization 头,响应拦截器里统一处理 401。

路由守卫的逻辑在 Vue Router 里配置。遍历路由表,对需要登录才能访问的页面检查 localStorage 里有没有 Token,没有就router.push('/login')。这个判断做不好,用户直接手动输 URL 就能跳进后台管理页,答辩现场演示的时候会被抓包。

5.2 推荐页面的展示逻辑与交互设计

推荐结果页是整个项目最有“算法感”的地方。我在页面上分三个模块展示:第一部分是“猜你喜欢”,展示 ItemCF 算出来的 TopN 推荐,每张卡片放景点图片、名称、城市、预测分,预测分用进度条或者星级显示,直观感受推荐强度;第二部分是“看过的人还看了”,逻辑是基于当前用户浏览历史里的景点,查相似度矩阵里每个景点的 Top3 相似景点;第三部分是热门景点 TOP10,作为冷启动兜底。

这里我建议对推荐卡片做一个额外的细节:展示推荐理由。比如“因为你去过杭州西湖,所以为你推荐灵隐寺”,红色推荐理由标签打在卡片右上角。这个小改动在我看来是整个系统体验升级最大的地方——协同过滤结果一眼看过去用户不知道为什么推荐,但加上解释后,信任感和点击率提升非常明显。答辩的时候这也是一个可以预埋的加分项。

前端页面我建议只展示后端返回的 DTO 字段,不要在前端做任何算法重算。有些同学图省事,把景点列表和相似度矩阵全丢给前端,让 JS 去算推荐结果,这不是架构设计上的偷懒,而是技术路线选错了——协同过滤数据量大之后浏览器扛不住,而且核心算法理应放在服务端,这是分层的基本素养。

5.3 联调中必须注意的三个问题

前后端联调是绝大多数人毕设卡壳的重灾区。我把高频问题按顺序列出来,你按这个顺序自查,能省一整晚调试时间。

先看前端网络面板。F12 打开 Network,如果请求状态是CORS error,说明后端跨域没配好,去后端CorsConfig里核对来源端口是否包含http://localhost:5173。如果请求状态是401,说明 Token 没带上或者过期了,去 Axios 请求拦截器里加个config.headers.Authorization = 'Bearer ' + token的日志输出。

再看后端控制台。如果报No serializer found,说明你试图返回一个包含循环引用关系的实体类,解决方案是改用 DTO 或者在关联字段上加@JsonIgnore。如果报IllegalStateException: current thread is not in a transaction,多半是 MyBatis Plus 分页插件没有配置分页拦截器。

最后看数据库日志。MyBatis Plus 默认打印 SQL 日志,翻一下最后一屏 SQL,直接能看到是查询条件写错了还是表名映射错了。我发现每年都有同学卡在数据库表名上:MySQL 里user是关键字,如果你建表叫user,查询全表必报语法错误,建议表名改成sys_user,字段order改成sort_order。

5.4 Vue3 组合式 API 写法与 Vue2 的差异

不管你之前有没有学过 Vue2,直接上手 Vue3 之后很快会感受到差异。Composition API 最大的变化是把零散在各处的 data、methods、computed 全部收敛到setup函数里,配合ref和reactive管理响应式数据。

这个项目里的推荐页面,我建议你优先熟悉ref和onMounted这对黄金组合:

<script setup> import { ref, onMounted } from 'vue' import { getRecommendList } from '@/api/recommend' const recommendList = ref([]) const loading = ref(false) const loadRecommends = async () => { loading.value = true try { const res = await getRecommendList() recommendList.value = res.data } finally { loading.value = false } } onMounted(() => { loadRecommends() }) </script>

这里有两个容易踩的坑。第一,ref包裹后的数据在读的时候要.value,在模板里会自动解包,但是你在 script 里操作recommendList.value = res.data千万别漏掉.value,漏了你就会拿到一个 Proxy 对象或一个包装对象,列表永远渲染不出来。第二,loading状态务必放在finally里关闭,如果放在try里面,一旦接口报错,loading 永远不会置回 false,页面转圈转到天荒地老。

reactive和ref的选择也有规律:基本类型用ref,对象、数组也可以用ref(内部帮你套reactive),但如果你要整体替换一个对象,ref的.value赋值方式比reactive直观得多。我项目里除了表单对象用了reactive,其余推荐列表、景点列表全部统一用ref,这个习惯帮我减少了很多心态崩溃的瞬间。

6. 常见问题排查与性能优化经验

6.1 问题排查速查表

我这一年多实际调试跑这个项目,整理了故障排查速查表。你现在把这个表格保存下来,遇到问题不要慌,按表定位:

症状最大概率原因排查手段
前端控制台报CORS error后端addCorsMappings没配置或端口不对检查后端跨域类的allowedOrigins
接口返回 401Token 未携带或已过期请求拦截器打印 Authorization 头
推荐接口返回空列表用户没有评分记录,冷启动分支未触发检查推荐服务里是否写了热门榜单兜底
分页不生效MyBatis Plus 分页插件未用@Configuration注册检查是否配置了PaginationInnerInterceptor
页面加载后服务器报 404静态资源被 Spring Security 拦截SecurityConfig 放行/assets/**
评分多次提交产生重复记录联合唯一索引未加建表语句UNIQUE KEY uk_user_scenic(user_id, scenic_id)
推荐接口响应超过 5 秒相似度矩阵每次请求现算改为启动时预热,缓存在内存
前端点击收藏报错 500收藏表里联合唯一索引冲突前端做重复收藏判断,后端捕获DuplicateKeyException

其中高频率出现的,还是跨域和 Token 问题。别看这些问题小,它们能把一个水平不错的学生卡在准备验收的前一晚。遇到问题先打开 Network 面板看状态码,再打开后端控制台看日志,这套“前后端双查”的套路永远管用。

6.2 推荐接口的性能优化思路

这个项目的推荐算法是纯 Java 手写的,在没有缓存、没有中间件的情况下,到了答辩演示环节也基本够用,但有几个性能杀手你不注意就会踩中。

第一个杀手是相似度矩阵的重复计算。我见过很多初版代码把相似度计算写在推荐接口内部,一个用户请求一次,接口就要 O(N²) 复杂度重算整个矩阵,几百个景点可能还好,几千个就卡出明显延迟。解决思路很明确:把相似度矩阵从“请求时计算”变成“启动时计算”。用@PostConstruct在 SpringBoot 启动后加载评分表并预热矩阵,保存在内存 Map 里,代码简单粗暴但确实能扛住。

第二个杀手是候选集的遍历范围。我在推荐实现里遍历了整个景点表,理论上这不是最优解,但景点数量在千级规模时基本无感知。如果你后续把景点表扩展到百万级,就需要引入分桶策略或者换用向量检索方案,这已经超出毕设范畴,不用提前焦虑。

第三个杀手是数据库查询次数。原始实现每算一个相似度就查一次数据库,几千个景点就是几千条 SQL,MyBatis Plus 的性能再好也扛不住。优化方案是一次性把所有评分记录查出来,在内存里完成全部计算逻辑,这一点我在推荐接口实现部分已经展示过了。这个思维是我认为做后端项目最核心的进阶能力:永远不要在循环里打 SQL,而是在代码里把数据准备好。

6.3 答辩时如何讲清楚你的系统

顺便提一嘴,如果你拿这个项目去答辩,一定不要在 PPT 上贴大段代码。老师在意的是你对“为什么要这么做”的解释能力。建议准备三个问题的答案:为什么用协同过滤而不是基于内容的推荐?为什么选 ItemCF 而不是 UserCF?冷启动怎么解决?这三个问题我在正文里都给了完整推理逻辑,你用自己的话复述一遍,比你背代码流畅得多。

还可以主动演示一下:把一个新用户注册后先展示热门列表,然后让他给几个景点打分,刷新推荐页,观察推荐结果变化。这个演示过程既展示了完整业务闭环,又体现了算法参与,是最自然的答辩加分解法。

我个人在实际推进这个项目的过程中最直观的体会是:一个毕业设计级全栈项目,真正难的不是某个技术难点,而是把业务流程、算法逻辑、前后端交互和异常处理串成一个闭环。SpringBoot3 和 Vue3 的 API 都不复杂,复杂的是你在调接口时怎么快速定位是前端的问题、后端的问题还是数据库的问题。把这个项目完整做一遍,你收获的不仅仅是一份能提交的毕设,更是一套“从零搭一个可用系统”的能力,这个能力比任何知识清单都值钱。

最后再分享一个小技巧:如果你打算照着这篇文章复现,我建议你先把推荐算法单独写成一个普通 Java 类写好测试,用 main 方法塞几行样例数据,验证推荐结果合理后再接入 SpringBoot 的 Service。这样调试周期至少缩短一半,因为你把最大的不确定性——算法正确性——提前隔离解决了。全程写完你手里就有一套完整的、逻辑闭环的、能讲清原理的旅游推荐系统,该下载下载、该参考参考,真正动手去敲一遍,它才会真正变成你的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 8:30:39

校园网三层架构设计与ENSP配置实战:从VLAN到VRRP/OSPF

说实话&#xff0c;校园网这个题目我做了不止一遍。第一版是为了练手&#xff0c;直接在ENSP里拖了几台设备&#xff0c;想当然把网关全都堆在核心交换机上&#xff0c;结果核心压力大、VLAN间访问要走一大圈&#xff0c;后面一加无线和访客隔离就乱套了。后来按真正园区网的交…

作者头像 李华
网站建设 2026/10/6 8:30:24

IP地址与TCP协议:计算机网络入门核心解析

有一次我给部门新来的同事讲网络基础&#xff0c;他问了句特别经典的话&#xff1a;我打开浏览器输入一个网址&#xff0c;敲下回车&#xff0c;中间到底发生了什么&#xff1f;我说这个问题要是展开讲&#xff0c;基本就是整本《计算机网络》教材。但如果你只是想解决实际工作…

作者头像 李华
网站建设 2026/10/6 8:28:33

计算机网络学习与实战:从分层模型到故障排查

1. 为什么计算机网络是“人人必修”的底层课 说实话&#xff0c;我见过太多人把计算机网络学成了“背完就忘”的科目。期末能默写TCP三次握手&#xff0c;但Wireshark抓个包看不懂&#xff1b;能说出OSI七层模型&#xff0c;但公司网络一卡就只会重启路由器&#xff1b;简历上写…

作者头像 李华
网站建设 2026/10/6 8:27:51

H3C DHCP中继配置详解:从原理到排障的跨网段IP分配实战

做企业网络维护这些年&#xff0c;我几乎每年都会遇到一次这样的需求&#xff1a;公司办公楼划分了十几个VLAN&#xff0c;终端分散在不同网段&#xff0c;DHCP服务器却只有一台&#xff0c;集中在核心机房。最开始有人图省事&#xff0c;提议在DHCP服务器上把每个网段的地址池…

作者头像 李华
网站建设 2026/10/6 8:25:53

2026 AI编程工具横评:免费与付费差距缩小,该如何选?

说实话&#xff0c;2026年聊AI编程工具&#xff0c;和两年前最大的区别就是&#xff1a;免费和付费的差距肉眼可见地缩小了。前几年大家提到AI编程&#xff0c;下意识会想到付费标杆Cursor、老牌GitHub Copilot&#xff1b;而最近这一轮&#xff0c;以Trae为代表的免费AI编程工…

作者头像 李华