news 2026/10/6 8:30:57

SpringBoot3+Vue3协同过滤旅游景点推荐系统实战与毕设指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot3+Vue3协同过滤旅游景点推荐系统实战与毕设指南

最近后台总有读者问我,有没有既能把毕业设计应付过去、又能真正学到东西的项目。我手里这套 SpringBoot3 + Vue3 协同过滤在线旅游景点推荐系统,就是为这种需求准备的。它不是那种只能截图交差的玩具,核心推荐功能由协同过滤算法实时计算,景点展示、用户评分、推荐列表整条业务链路都能跑通。如果你是零基础、时间紧、又想把全栈开发和推荐系统落地一次性摸清楚,这篇内容可以直接照着做。

我会按实际开发顺序来讲:先从项目整体设计和技术选型说起,再拆协同过滤算法的计算过程,然后分别落到后端 SpringBoot3 和前端 Vue3 的代码实现,最后把毕设答辩时最容易遇到的问题和加分技巧一起给你。

1. 项目画像与技术选型思路

1.1 这个项目到底做了什么

一句话描述:一个用户可以注册登录、浏览旅游景点、给景点打分,系统根据所有用户的打分行为,给当前用户推荐他可能感兴趣的景点。

功能拆开看,无非四块:

  • 用户模块:注册、登录、个人信息维护。这是几乎所有系统的入口,没有它就无法区分“谁在评分”“给谁推荐”。
  • 景点模块:景点的列表展示、详情页面、图片和基础介绍。景点数据可以提前录入,或者用爬虫、公开数据集填充。
  • 评分模块:用户对景点的显式评分,例如 1 到 5 分。这是协同过滤算法最需要的行为数据。
  • 推荐模块:核心亮点。算法读取所有用户的评分矩阵,计算景点之间的相似度,再给当前用户生成 TopN 推荐列表。

做过毕设和实训项目的人应该能感觉到,前三块属于标准 CRUD,大部分学生做到这里就停了。这个项目的价值恰恰在第四块,推荐模块把项目从“管理系统”提升到了“带算法的应用系统”,这也是答辩时老师最愿意听的部分。

1.2 为什么选 SpringBoot3 + Vue3,而不是其他组合

技术选型这件事,很多零基础的同学容易犯一个错误:什么技术热门就堆什么,完全不考虑项目规模和团队能力。

这个项目选 SpringBoot3 + Vue3,不是因为它们最潮,而是因为它们在三个维度上都恰到好处。

后端方面,SpringBoot3 是基于 JDK17 的,相比 SpringBoot2 的 JDK8 时代,整体更现代。Spring Security、Spring Data、Starter 生态依然齐全,社区资料足够多,遇到问题随便一搜就有答案。对毕设而言,SpringBoot 本身就是“约定大于配置”的代表,你不需要懂复杂的环境搭建,只要会加依赖、写 Controller、启动 Application 就行。

前端方面,Vue3 搭配 Vite,启动速度快到让人舒适。Composition API 在组织业务逻辑时比 Vue2 的 Options API 更清晰,一个景点推荐页面里的“加载数据”“处理评分”“切换 Tab”逻辑放到 setup 函数里,写起来相当顺手。

也不建议在这个阶段上微服务、K8s 这类重型方案。一个推荐系统课程设计没必要把项目拆成七八个服务,单体应用加清晰分层反而是最容易维护和答辩的形态。

1.3 推荐算法为什么选协同过滤

推荐算法家族里,常见的有基于内容的推荐、协同过滤推荐、混合推荐。

基于内容的推荐,要求每个景点有结构化的内容标签。比如景点是“自然风光”还是“人文古迹”,需要给每个景点做标签建模。如果数据里只有评分,没有标签,这条路就难走。

协同过滤正好不需要这些。它的核心假设是:过去有相似评分行为的用户,未来的偏好也相似;或者反过来,被同一批用户喜欢过的物品,本身也是相似的。你做在线旅游景点推荐,用户留下的最高频、最可靠的数据就是评分行为,这数据天然适合协同过滤。

再细分,协同过滤有 UserCF 和 ItemCF。UserCF 是“找与我相似的用户,推荐他们喜欢的景点”,更适合用户规模小、兴趣变化快的场景。ItemCF 是“计算景点之间的相似度,推荐与我历史喜欢的景点相似的景点”,更适合物品数量稳定、用户个性化需求明显的场景。

旅游景点正好符合 ItemCF 的条件:景点数量通常几百上千,不算海量;一个用户可能这辈子就去过一次某景点,但景点之间的相似性(比如同样在海边、同样适合爬山)是稳定的。所以我最终选择 ItemCF 作为主算法。

补充一句:如果你答辩时老师问“为什么不用 UserCF”,你可以回答:景点推荐场景下物品数量和数据稀疏度相对可控,ItemCF 结果可解释性更强(“因为您喜欢黄山,所以推荐泰山”),在线维护成本也低。

2. 协同过滤算法核心原理与落地实现

2.1 算法的整体计算流程

ItemCF 看起来玄乎,拆开就四步:

  1. 构建用户-景点评分矩阵。
  2. 用评分矩阵计算景点之间的相似度。
  3. 结合当前用户的历史评分,对未评分景点做预测评分。
  4. 按预测评分排序,取 TopN 返回。

很多人卡在第二步和第三步,所以我用一个最小例子完整走一遍。假设当前有四个景点:A 张家界、B 九寨沟、C 故宫、D 乌镇。三个用户给它们打了分,1 到 5 分,0 表示没去过,评分矩阵如下:

用户张家界(A)九寨沟(B)故宫(C)乌镇(D)
用户15401
用户24502
用户31045

目标:给用户3推荐一个新景点。

2.2 相似度计算:余弦相似度的完整手算

相似度计算是协同过滤最核心的数学操作,专门用来计算两个景点距离。方法有几种,最常用是余弦相似度,公式如下:

cosine(A, B) = A · B / (|A| × |B|)

把景点 A 和景点 B 分别看成一个向量,向量的每个维度是不同用户的评分。对用户1到用户3:

A 向量 = [5, 4, 1] B 向量 = [4, 5, 0]

分子(点积):5×4 + 4×5 + 1×0 = 20 + 20 + 0 = 40

分母是模长乘积:

|A| = sqrt(5² + 4² + 1²) = sqrt(25 + 16 + 1) = sqrt(42) ≈ 6.48 |B| = sqrt(4² + 5² + 0²) = sqrt(16 + 25 + 0) = sqrt(41) ≈ 6.40

所以 cosine(A, B) = 40 / (6.48 × 6.40) ≈ 40 / 41.47 ≈ 0.96

A 和 B 的相似度高达 0.96,说明张家界和九寨沟在三个用户心中的评分模式高度一致。这种计算结果符合直觉:都是自然风光类景区,喜欢一个的人往往也喜欢另一个。

再看 C 和 D:

C 向量 = [0, 0, 4] D 向量 = [1, 2, 5]

分子 = 0×1 + 0×2 + 4×5 = 20 |C| = sqrt(0 + 0 + 16) = 4 |D| = sqrt(1 + 4 + 25) = sqrt(30) ≈ 5.48

cosine(C, D) = 20 / (4 × 5.48) ≈ 20 / 21.91 ≈ 0.91

故宫和乌镇也相似,一个偏人文历史、一个偏江南水乡,评分模式同样接近。

这种方式的意义在于:它不考虑评分绝对值高低,只关注评分模式的相对趋势。有的用户手松全打高分,有的用户手紧全打低分,如果直接对比欧式距离,手松手紧的差异会干扰相似度,余弦相似度做了归一化,天然规避了这个问题。

2.3 预测评分与 TopN 推荐生成

有了景点之间的相似度,就可以给用户3预测他对未去过的景点 A 和 B 的评分。

预测评分公式有很多变体,最常用的是加权求和:

预测评分 = 用户已评分景点的评分 × 相似度 的累加和 / 相似度的绝对值之和

分母上的相似度绝对值归一化,是为了避免“用户只评过一个景点”时预测值无条件等于那个景点的评分。

用户3的历史行为:C 评了4分,D 评了5分。

预测用户3对 A 的评分:

需要相似度 sim(A, C) 和 sim(A, D)。刚才没算这两个,补上:

sim(A, C): A = [5, 4, 1],C = [0, 0, 4] 分子 = 5×0 + 4×0 + 1×4 = 4 |A| ≈ 6.48,|C| = 4 sim(A, C) = 4 / (6.48 × 4) ≈ 4 / 25.92 ≈ 0.15

sim(A, D): D = [1, 2, 5] 分子 = 5×1 + 4×2 + 1×5 = 5 + 8 + 5 = 18 |D| ≈ 5.48 sim(A, D) = 18 / (6.48 × 5.48) ≈ 18 / 35.51 ≈ 0.51

预测评分 A = (4 × 0.15 + 5 × 0.51) / (0.15 + 0.51) = (0.6 + 2.55) / 0.66 = 3.15 / 0.66 ≈ 4.77

同理预测 B:

sim(B, C): B = [4, 5, 0],C = [0, 0, 4] 分子 = 0,相似度 0 sim(B, D): B × D = 4×1 + 5×2 + 0×5 = 4 + 10 = 14 |B| ≈ 6.40,|D| ≈ 5.48 sim(B, D) = 14 / (6.40 × 5.48) ≈ 14 / 35.07 ≈ 0.40

预测评分 B = (4 × 0 + 5 × 0.40) / (0 + 0.40) = 2 / 0.40 = 5

于是系统预测用户3对九寨沟的评分是5分,张家界4.77分。排序后优先推荐九寨沟,张家界次之。

这个例子建议你亲手用 Excel 或者纸笔算一遍。算法本身不复杂,难点在于理解“矩阵 -> 相似度 -> 预测评分”这条链路,算过一次之后,代码怎么写都不会慌。

2.4 Java 代码实现思路

理解了公式,代码就只是翻译。实际项目中我推荐用一个单独的服务类封装整个推荐流程,比如RecommendService。核心逻辑可以分成三个方法:

  • 根据评分表加载用户-景点评分矩阵。简单做法是把 rating 表全量查出来,在内存里构造成Map<Long, Map<Long, Double>>,外层 key 是景点 ID,内层 key 是用户 ID。景点数量不超过一千时,全量加载完全没问题。
  • 计算任意两个景点的余弦相似度。可以预先算好所有景点两两之间的相似度,存到Map<Long, Map<Long, Double>>里;也可以写一个similarity(Long spotIdA, Long spotIdB)实时计算,根据用户已评分的景点数按需算。
  • 生成推荐列表。对当前用户没评分过的景点逐个做预测评分,排序后取前 N 个。

核心代码轮廓如下:

public List<Long> recommend(Long userId, int topN) { // 1. 获取当前用户已评分的景点及分数 Map<Long, Double> userRated = ratingMapper.selectByUserId(userId); // 2. 获取候选景点(所有景点排除已评分的) List<Long> candidates = spotMapper.selectAllIds() .stream() .filter(id -> !userRated.containsKey(id)) .collect(Collectors.toList()); // 3. 对候选景点计算预测评分 List<Map.Entry<Long, Double>> scored = new ArrayList<>(); for (Long spotId : candidates) { double predictScore = predict(userRated, spotId); scored.add(new AbstractMap.SimpleEntry<>(spotId, predictScore)); } // 4. 排序 + 截断 scored.sort((a, b) -> Double.compare(b.getValue(), a.getValue())); return scored.stream().limit(topN).map(Map.Entry::getKey).collect(Collectors.toList()); } private double predict(Map<Long, Double> userRated, Long targetSpotId) { double numerator = 0.0; double denominator = 0.0; for (Map.Entry<Long, Double> entry : userRated.entrySet()) { double sim = similarity(entry.getKey(), targetSpotId); numerator += sim * entry.getValue(); denominator += Math.abs(sim); } return denominator == 0 ? 0 : numerator / denominator; }

similarity方法里需要按照余弦公式循环用户维度累加,这里不再展开,数据量小,直接双层循环即可。算法代码总量不超过 200 行,核心点在于逻辑清晰,答辩时可以现场逐步讲解。

2.5 冷启动和稀疏问题怎么处理

上课时老师会提冷启动和稀疏性,这是算法落地时必须面对的问题。项目里数据是自己造的,后面我建议你至少准备五十个用户、一两百条评分记录,让推荐效果看得见。

冷启动分两种。新用户没有评分记录,协同过滤无从算起,我的做法是给新用户返回全局热门景点 Top10(按评分人数或平均分排序),前端做个“热门推荐”区块。新景点没有人评过分,任何推荐都不会包含它,可以结合管理员后台手动置顶或者用基于内容的方式作为补充。

稀疏问题体现在“用户评过的景点很少”,导致相似度计算不稳定。常规做法是引入惩罚因子,两个景点共同被评分的用户数少于一定阈值时,把相似度乘以一个衰减系数,比如sim * (common / (common + 5))。这个细节讲给答辩老师听,基本能确定你对推荐系统的理解不是停留在抄代码层面。

3. 后端 SpringBoot3 实现与关键配置

3.1 数据库怎么设计

我建议三张核心表加一张可选的管理员表。三张核心表配合主外键就能支撑整个推荐链路。

用户表t_user:

字段类型说明
idbigint主键自增
usernamevarchar(50)登录名,唯一
passwordvarchar(100)BCrypt 加密后的密码
nicknamevarchar(50)昵称
avatarvarchar(255)头像 URL
created_timedatetime注册时间

景点表t_spot:

字段类型说明
idbigint主键
namevarchar(100)景点名称
locationvarchar(100)所在地
descriptiontext详细介绍
cover_imgvarchar(255)封面图
categoryvarchar(50)分类,如自然风光、人文古迹
scoredecimal(2,1)平均分冗余字段,便于列表展示

评分表t_rating:

字段类型说明
idbigint主键
user_idbigint用户 ID,索引
spot_idbigint景点 ID,索引
ratingtinyint1 到 5 分
create_timedatetime评分时间

重点提醒:t_rating表一定要为(user_id, spot_id)建唯一索引,否则同一个用户对同一个景点插入了多条评分记录,后面构建评分矩阵时就会数据混乱。唯一索引还能顺手解决重复提交的问题。

平均分冗余在景点表里,评分写入主表后通过事务同步更新景点表的平均分。虽然有点冗余,但列表页展示平均分时不用频繁查评分表,性能友好很多。

3.2 后端工程结构怎么搭

用 IDEA 新建 SpringBoot3 工程时,建议直接勾选这些依赖:

  • Spring Web
  • Spring Data JPA 或 MyBatis-Plus(二选一,我更推荐 MyBatis-Plus,国内资料多,简单场景写起来快)
  • MySQL Driver
  • Lombok
  • Validation

我个人比较推荐用 MyBatis-Plus,因为毕设项目的 SQL 大多是单表操作,不需要写复杂的 XML,lambdaQuery()一套就能搞定绝大多数查询。

工程目录结构建议这样:

com.example.travel ├── TravelApplication.java ├── controller │ ├── UserController.java │ ├── SpotController.java │ ├── RatingController.java │ └── RecommendController.java ├── service │ ├── UserService.java │ ├── SpotService.java │ ├── RatingService.java │ └── RecommendService.java ├── mapper │ ├── UserMapper.java │ ├── SpotMapper.java │ └── RatingMapper.java ├── entity │ ├── User.java │ ├── Spot.java │ └── Rating.java ├── dto │ ├── LoginDTO.java │ ├── RatingDTO.java │ └── RecommendVO.java └── common ├── Result.java └── GlobalExceptionHandler.java

common包里放统一的返回体和全局异常处理器,这个看起来不起眼,但能让所有接口的输出格式保持一致,前端对接时省一大半力气。我习惯的返回结构是:

{ "code": 200, "message": "操作成功", "data": {} }

3.3 推荐接口的设计与实现

推荐接口的核心逻辑按照第 2 章的算法流程来。为了让前端展示效果好,RecommendVO里除了景点信息,还带上“推荐分数”和“推荐原因”。推荐原因对 ItemCF 来说可以说是天然优势,实现也简单:预测评分最高的候选景点,就是和历史评分景点相似度最高的那个,原因文案可以写成“因为您喜欢黄山,它与九寨沟相似度较高”。

接口设计可以这样组织:

路径方法说明
/api/user/registerPOST注册
/api/user/loginPOST登录,返回 token
/api/spot/listGET分页获取景点列表
/api/spot/{id}GET景点详情
/api/rating/submitPOST提交评分
/api/recommendGET获取当前用户的景点推荐

推荐接口建议加一个userId参数,方便前端在登录后携带用户身份。也可以做成从 token 中解析,毕设场景从参数拿会更直观。

3.4 后端避坑实录

第一个坑是 N+1 查询。推荐结果返回时需要对每个景点补全名称、封面、分类,如果循环查库,推荐 20 个景点就要查 20 次景点表。正确做法是查出景点 ID 集合后,用WHERE id IN (...)一次查出全部景点信息,再在内存里组装。这条优化在答辩演示时如果被问到“推荐接口会不会慢”,你直接说用了批量查询,平均响应时间在几十毫秒内,老师基本满意。

第二个坑是事务。评分提交接口需要同时做两件事:插入评分记录、更新景点表的平均分。这两步必须放在同一个事务里。用 Spring 的@Transactional注解即可,注意要打在 public 方法上,否则不生效。

第三个坑是跨域。前端 Vite 默认端口是 5173,后端是 8080,前后端端口不同必然触发跨域。最常见解法是后端写一个配置类实现WebMvcConfigurer,统一放行所有跨域请求:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

.allowedOriginPatterns("*")是 SpringBoot 后端应对携带 cookie 的凭证请求的正确写法,直接写allowedOrigins("*")反而会报错。

4. 前端 Vue3 页面实现与业务串联

4.1 前端工程初始化

前端我用 Vite 创建,命令很简单:

npm create vite@latest travel-frontend -- --template vue cd travel-frontend npm install

随后安装核心依赖:

npm install vue-router@4 pinia axios element-plus

很多人纠结要不要用 Pinia,如果只是毕设,不写全局状态管理也完全能跑。但一旦登录态需要在多个页面共享,比如首页头部显示用户名、个人中心显示用户信息,Pinia 就派上用场了。

目录结构建议:

src ├── api │ ├── request.js // axios 实例与拦截器 │ ├── spot.js │ ├── rating.js │ └── recommend.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── HomeView.vue │ ├── SpotDetailView.vue │ ├── LoginView.vue │ └── ProfileView.vue ├── components │ ├── SpotCard.vue │ └── RatingDialog.vue └── App.vue

4.2 页面规划与交互设计

做在线旅游景点推荐系统,页面不需要多奢华,但要让人一眼看出“这是推荐系统,不是普通列表”。

  • 首页:顶部轮播 Banner 放热门景点,下面分成“为你推荐”和“热门景点”两个区块。推荐结果来自/api/recommend,热门结果来自/api/spot/list按评分排序。
  • 景点详情页:展示图片、介绍、位置、当前用户对该景点的评分(如果有),还有一个“评分”按钮,点击弹出 1 到 5 分的星级选择器。
  • 登录注册页:账号密码表单,登录后把 token 存到 localStorage,同时写入 Pinia。
  • 个人中心:展示用户信息、用户评分过的景点列表,支持取消评分。

首页推荐区块的展示逻辑,建议用卡片瀑布流。每个卡片包括封面图、景点名称、分类标签、平均分、推荐理由。推荐理由这个位置是体现系统智能感的关键,可以用一行浅色文字标注“因为您喜欢九寨沟,所以推荐张家界”,同样一个推荐系统,这样的细节能让演示效果提升一个档次。

4.3 Axios 请求封装与拦截器

前端很多同学直接在每个组件里调axios.get(),代码全都散落着。我建议封装一个request.js:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { alert(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { alert('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

这里有个关键的配置:axios 的baseURL设置为/api,然后在vite.config.js里配置开发环境代理,把请求转发到后端 8080 端口:

export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样做的好处是,开发时前端请求带的是相对路径/api/...,由 Vite 代理转发,绕开跨域问题。打包上线时,只需要在 Nginx 配一条反向代理规则,代码里一行不用改。这种“开发代理 + 部署反代”的思路,是前后端分离项目的标准姿势,值得养成习惯。

4.4 推荐列表的渲染逻辑

推荐列表数据拿到后,前端要做三件事:切割封面图、格式化推荐分数、拼接推荐理由。举个例子,后端返回的推荐分数是 4.77,前端可以保留一位小数显示。推荐理由可以这样处理:如果当前用户评分过的景点不多,就只展示“智能推荐”四个字,如果有关联景点,就展开成“因为您喜欢九寨沟,相似度 0.96,所以推荐张家界”。

这里有一个很多新手会踩的坑:后端返回的 list 是推荐景点 ID 列表,如果前端拿到 ID 再逐个请求详情,会产生大量请求,首页加载会非常慢。所以我在后端接口设计时,就直接让/api/recommend返回完整的景点信息列表,而不是只返回 ID。

4.5 前端常见问题

渲染v-for列表时一定要加key,而且不要用索引做 key,否则列表更新时 Vue 的 diff 会错乱。可以用景点 ID 做 key。

Element Plus 的按需引入需要额外配置,如果只是毕设,直接在main.js里app.use(ElementPlus)全局引入就行。全量引入虽然体积大一点,但省去按需配置的复杂性,开发阶段体验更顺畅。

图片加载失败会造成视觉空白,可以在<img>上绑一个error事件,替换成默认占位图:

<img :src="spot.coverImg" @error="e => e.target.src = fallbackImg" />

评分组件用el-rate,注意它绑定的是数值模型,提交前要判断是否已经登录,没登录就跳转登录页。这里有一个交互细节:用户在景点详情页点击评分,如果评分成功,页面上要即时刷新平均分和“我的评分”,不要等整页刷新。

5. 常见问题排查与答辩加分技巧

5.1 常见问题速查表

现象原因解决办法
前端请求后端报 CORS 错误后端没有配置跨域在 SpringBoot 里配置 CorsConfig,见 3.4 节
Maven 依赖下载超慢默认中央仓库settings.xml 配置阿里云镜像
前端页面能开但接口 404代理没生效检查 vite.config.js 的 proxy 配置
登录后页面刷新就掉登录态token 只存在内存登录时同时存 localStorage 和 Pinia
数据库中文乱码连接串没指定编码JDBC URL 加characterEncoding=utf8
SpringBoot3 启动报 JDK 版本错误JDK 版本低于 17安装 JDK17,并检查 IDEA 的 Project SDK
推荐接口返回为空当前用户没有评分数据先给当前用户造几条评分数据
列表页图片全部裂开前端和数据库图片 URL 无法访问开发时用本地静态资源或者公网免费图床

这里最容易被忽视的是最后一条。很多零基础同学从网上找了一堆图片 URL,第二天打开页面发现全裂了。我建议把景点封面图直接放在前端项目的public/images/目录下,图片路径存相对路径,开发时用/src/assets或 public 引用,这样不依赖任何外部服务,演示最稳。

5.2 演示环境的数据准备

推荐系统最怕“看起来不智能”。如果你只造了十个用户、每个人只评了三四条数据,推荐出来的结果往往乱七八糟。我的经验是数据量要适中,既有规律又不能过于明显。

我造数据时的做法:设计四五个“用户偏好原型”,比如“自然风光爱好者”集中给张家界、九寨沟、黄山打高分;“历史人文爱好者”集中给故宫、颐和园、平遥古城打高分;“休闲度假型”集中给三亚、大理、丽江打高分。然后按一定概率给这些用户生成评分记录,同时随机穿插少量低分和噪声,模拟真实行为。

这样造出来的数据,推荐结果基本能符合预期。你甚至可以自己以身作则:先用某个账号给两三个自然风光景点打 5 分,再调用推荐接口,看系统是否真的把九寨沟排到前面。演示前花十分钟造数据,效果比答辩时临时搞一堆随机数据好一百倍。

5.3 可扩展方向与答辩话术

如果你的项目时间有余量,可以考虑以下扩展方向,每一个都能在答辩时作为亮点:

  • 混合推荐:协同过滤结果和基于热门的结果按比例融合,冷启动体验更好。
  • 相似景点展示:景点详情页展示“与该景点相似的其他景点”,直接复用 ItemCF 的相似度计算结果。
  • 推荐理由生成:把相似度最高的历史评分景点名称拼进推荐原因,体现可解释性。
  • Redis 缓存:把全量景点相似度矩阵缓存到 Redis,减少计算耗时。
  • 实时热门榜:按近 7 天评分人数动态计算热门景点,放在首页独立区块。

答辩时老师如果问“协同过滤的缺点是什么”,你可以诚实回答:冷启动和数据稀疏是两大痛点,然后立刻接上“所以我在项目中加入了热门推荐的兜底策略,并对相似度做了惩罚因子修正”。这种“知道缺点 + 有解决方案”的应答模式,比背概念要好得多。

5.4 拿这个项目做毕设的最后几点建议

项目不要只停留在能跑,要能讲。你自己亲手敲一遍的代码,和抄一遍git clone下来的代码,在答辩时会被一眼看穿。重点把推荐模块的逻辑吃透,讲清楚“评分矩阵怎么构建、相似度怎么算、TopN 怎么选取”,这一块讲明白了,整个项目就立住了。

还有时间的话,给项目加上一个简单的管理员端页面,哪怕只有“景点管理”和“评分数据查看”两个功能,也能让项目的完整性提升一个档次。

我个人实际操作中的体会是:这个项目真正的难点不在 Vue3 语法,也不在 SpringBoot 注解,而在于把算法和工程打通的那种“串联感”。当你看到自己给某个景点打了分,刷新推荐页真的排出了相似的景点,那种成就感比单纯跑通一个 CRUD 强太多了。项目里如果因为版本问题遇到奇怪报错,优先检查 JDK 版本和 Maven 依赖版本,SpringBoot3 对版本的要求比 SpringBoot2 严格不少。

最后再分享一个小技巧:答辩演示时,提前准备一个只评过两三个景点的测试账号,现场登录、现场评分、现场刷新推荐页面。整个过程不超过一分钟,但“算法实时生效”的现场感,比任何 PPT 截图都有说服力。

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

Git Flow分支模型全解析:从五类分支到发布与热修复的工程实践

1. 为什么要重新认识 Git Flow Git Flow 这个名字在团队协作里被反复提及&#xff0c;但我发现很多人对它的理解停留在“一套分支规范”这个层面。老实说&#xff0c;这样的认识太浅了。Git Flow 是一套把软件开发流程、发布节奏、热修复机制全部纳管起来的完整工程实践&#x…

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

信恒支付源码部署与第四方支付系统实战解析

简介&#xff1a;这是一套完整的第四方支付系统源码&#xff0c;适用于PHP开发者、支付平台二次开发人员及中小型金融科技团队&#xff0c;用于快速搭建或研究聚合支付底层架构。资源基于ThinkPHP框架开发&#xff0c;完整保留宝塔环境下的部署结构与配置逻辑&#xff0c;支持L…

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

FrankenPHP实战:用Caddy和Worker模式替代Nginx+PHP-FPM提升性能

1. 为什么我现在推荐用 FrankePHP 替代 Nginx PHP-FPM这几年 PHP 常驻内存的方案其实已经不少&#xff0c;但 FrankenPHP 一出来&#xff0c;我还是专门熬夜测了一整晚。它跟 RoadRunner、Swoole 这类方案不太一样&#xff0c;是把 PHP-FPM 直接整合进了 Caddy 这个 Web 服务器…

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

SpringBoot+Vue个人理财系统开发实战:从数据库设计到部署上线

最近在整理手头的源码项目&#xff0c;发现这套个人理财系统挺有代表性——SpringBootVue前后端分离&#xff0c;MyBatis负责持久层&#xff0c;MySQL存数据&#xff0c;标准的企业级管理系统打法。比起那些动辄几十张表的ERP&#xff0c;这个系统业务边界清晰&#xff0c;该有…

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

WinForm/WPF桌面应用自动更新实战:从方案选型到避坑指南

简介&#xff1a;这是一份面向.NET桌面开发者的软件自动更新解决方案源码包&#xff0c;适用于WinForm、WPF等客户端程序的版本迭代场景。方案核心思路是依据文件列表比对哈希值&#xff0c;对本地文件执行下载替换、删除与新增操作&#xff0c;最终启动软件本体&#xff0c;经…

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

Windows基础漏洞防护实战:安全基线、补丁管理与攻击面收敛要点

搞了这么多年Windows系统运维&#xff0c;Windows系统漏洞防护从来不是装个杀毒软件就完事。我见过太多案例&#xff1a;杀软天天更新&#xff0c;墙也开着&#xff0c;结果内网一台机器中招&#xff0c;横向渗透直接把整个部门报销。原因就一个——基础没扎牢。所谓基础漏洞防…

作者头像 李华