ARTICLE DETAIL

资讯详情

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

基于协同过滤的个性化旅游推荐系统:Java+Vue+MySQL实战

基于协同过滤的个性化旅游推荐系统:Java+Vue+MySQL实战 毕业设计或者课程设计做到推荐系统这个方向我建议你直接把目光锁定在协同过滤算法的个性化旅游推荐平台上。市面上很多类似项目核心就三个字协同过滤配上一套Java后端、Vue前端再加MySQL数据库和配套文档这个组合基本上把能演示、能答辩、能写在简历上三个需求全占了。我经手看过好几版这类项目的源码从算法实现到前端联调再到数据库设计踩过的坑和能拿来就用的经验今天一次说清楚。这套系统要解决的事情很直白用户打开网站不应该看到一张千篇一律的景点列表而是根据他自己的浏览记录、收藏记录、评分记录生成一份猜你喜欢的旅行清单。对做项目的人来说最值钱的地方在于它有两条主线——一条是工程线Java Vue 前后端分离怎么搭、MySQL 表怎么设计、接口怎么联调另一条是算法线协同过滤从数学公式到 Java 代码怎么落地。两条线都能学会这项目就不亏。1. 项目全景拆解一个会推荐的旅游网站是怎么设计的1.1 这个项目到底在做一件什么事旅游推荐场景最大的痛点就是信息过载。打开任何旅游网站热门景点永远是那几个长城、故宫、西湖推荐来推荐去都是同一批。但用户的真实需求差异很大——有人喜欢人文古迹有人喜欢自然风光有人带孩子玩需要亲子友好型景点。统称叫热门榜的东西本质上没有个性化。协同过滤算法要解决的问题就是从用户的历史行为中反推他的偏好再基于相似的人喜欢相似的东西或者相似的东西会被同一个人喜欢这两条朴素规律生成一张真正属于这个用户的推荐列表。平台的业务因此分成两块基础业务和推荐业务。基础业务就是普通的旅游网站功能注册登录、景点搜索、景点详情、收藏评分推荐业务才是灵魂拿到基础业务沉淀下来的数据跑协同过滤模型输出个性化推荐结果。没有了推荐这项目就是个普通的 CRUD 系统加上了推荐它才是一个算法项目。1.2 为什么技术栈一定要是 Java Vue这个项目选型几乎已经成了标准答案后端 Java前端 Vue数据库 MySQL。选 Java 倒不是因为协同过滤算法用 Java 写有多优雅主要原因是 Spring Boot 这个生态太成熟了做课程设计和毕业设计找资料最容易遇到问题搜索引擎一搜全是解决方案对新手极度友好。Vue 就更不用说了前后端分离已经是行业主流Vue 的组件化开发做页面非常快景点卡片、评分星星、推荐列表这些 UI 可以直接用 Element UI 拼出来。MySQL 作为数据库稳定、免费、教程多存储用户、景点、评分这些关系型数据非常合适。这套组合还有一个隐藏好处如果你想在简历上写熟悉企业级开发技术栈Java Vue MySQL 恰好就是大多数中小型公司的标配组合项目经历和技术栈是直接对口的。1.3 用户角色与核心业务闭环系统一般拆成两个角色普通用户和管理员。理解角色设计基本就理解了整个项目的功能菜单。普通用户端核心功能是注册登录、浏览景点、搜索景点、查看景点详情、对去过的景点评分和收藏、查看系统给自己生成的个性化推荐列表。注意评分和收藏是数据来源它们不只是功能更是推荐算法的燃料。管理员端核心功能是景点信息管理增删改查、用户管理、基础数据统计更完整的项目还会提供一个推荐参数配置界面让管理员能调整推荐算法的开关、相似度阈值、推荐数量这些参数。核心业务闭环你可以在脑子里过一遍用户注册登录后浏览了几个景点对其中一个打了5分收藏了另外一个这些行为全部写入数据库推荐模块定时或在触发时读取这些行为数据计算用户与景点之间、景点与景点之间的相似关系用户再次登录时主页不再是静态的热门列表而是算法实时生成的猜你喜欢。2. 协同过滤算法在旅游场景中的落地细节2.1 两种协同过滤怎么选UserCF 还是 ItemCF协同过滤分两大类基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。两者的核心逻辑不同适用场景也不同我直接用一张表说清楚。对比维度基于用户UserCF基于物品ItemCF核心思路找到和你口味相似的用户把那些用户喜欢而你没见过的景点推荐给你找到和你历史喜欢的景点相似的景点推荐给你相似度计算对象用户与用户之间景点与景点之间适合场景新闻、短视频用户兴趣变化快的场景图书、电影、旅游景点物品属性相对稳定的场景冷启动表现新用户没有行为很难找相似用户新景点没有行为很难找相似物品可解释性和你相似的用户也喜欢因为你喜欢西湖所以推荐西溪湿地旅游景点这个场景我更推荐把 ItemCF 作为主力算法。原因很简单旅游景点的数量级通常远小于用户数量级而且景点之间的相似关系相对稳定西湖和杭州灵隐寺的相似度不会因为时间变化而剧烈波动。你给一个喜欢人文古迹的用户推荐西安而不是三亚用物品相似度来解释非常自然。我在实际项目里的做法是两套逻辑都实现默认展示 ItemCF 的结果管理后台留一个开关可以切换到 UserCF答辩的时候顺便还能讲一嘴两个算法的区别非常加分。2.2 评分数据从哪来显式评分与隐式行为的统一协同过滤算法需要的核心数据是用户-景点评分矩阵但实际项目中用户很少会主动给景点打分。如果只收集显式评分矩阵会极其稀疏算法基本跑不出来。所以需要把用户的隐式行为也转化为评分。常规做法是给行为赋予权重浏览一次记1分收藏一次记2分搜索点击记1.5分主动评分按 1-5 分原样记录评论行为记3分。把这些分数统一换算到 1-5 分区间后再和显式评分做加权合并。比如一个用户浏览了某个景点3次、收藏了1次、没有主动打分那他对这个景点的综合分就是 3×1 1×2 5封顶到5。这套映射逻辑不需要很复杂但它解决了真实场景里数据太少算法没饭吃的问题。评分矩阵在这个项目里是一个稀疏矩阵用户数和景点数可能是几百乘几百但非零项可能只有几百个。稀疏矩阵的存储不要用二维数组硬存Java 里面用 MapLong, MapLong, Double 这种嵌套结构或者直接用数据库的评分表来构建都能有效节省内存。2.3 相似度计算到底怎么算余弦相似度与皮尔逊相关系数相似度计算是协同过滤的数学核心。最常用的是余弦相似度公式长这样sim(i, j) (Σ Rui × Ruj) / (√Σ Rui² × √Σ Ruj²)其中 Rui 表示用户 u 对景点 i 的评分。这个公式的直觉就是你高中学过的向量夹角——两个景点的评分向量方向越一致夹角越小余弦值越接近1相似度越高。举个例子三个用户对三个景点的评分如下用户西湖灵隐寺西溪湿地小明540小红450小刚005西湖和灵隐寺在这两个有共同评分的用户身上分数方向一致所以它们之间的相似度会很高而西溪湿地与西湖没有共同的评分用户相似度会被算成0。这就是 ItemCF 的基本盘。余弦相似度有一个局限性它不看用户的评分习惯差异。有的用户天生手松到处给5分有的用户很挑剔最多打3分。这时候更稳妥的做法是用皮尔逊相关系数先减去用户自己的平均评分再计算消除评分尺度的影响。我的建议是项目初期直接用余弦相似度跑通为主如果评测发现推荐效果不理想再升级成皮尔逊版本这个小优化在论文和答辩里都是很好的亮点。相似度计算的 Java 实现核心步骤是这样的统计每个景点被哪些用户评过分构建景点的评分向量然后双层循环计算每对景点的余弦相似度最后把结果存入相似度表或者内存缓存。双层循环的时间复杂度是 O(n²)景点数量几百个时完全没有性能压力但如果景点数量过万就一定要考虑离线计算加缓存了。2.4 从相似度到 TopN 推荐列表有了景点之间的相似度矩阵针对某个用户生成推荐就水到渠成了。ItemCF 的推荐分两步走第一步找出用户历史上有过正反馈行为的景点集合第二步对于每个候选景点计算它与用户历史喜欢景点的加权相似度总和公式如下Ru,i Σ (sim(i, j) × Ru,j) / Σ sim(i, j)翻译成人话如果用户很喜欢西湖打了5分而灵隐寺与西湖的相似度是0.8那灵隐寺对这个小明来说就有很高的推荐权重。把所有候选景点的推荐分算出来去掉用户已经去过的或者已经评过分的按分数从高到低取前 N 个就是最终的猜你喜欢列表。N 通常取10到20。写代码的时候有一个小细节容易踩坑必须把用户已经产生过行为的景点排除掉否则推荐结果里会出现用户去过的景点这在演示的时候非常尴尬。另外推荐分数可以做一次归一化除以用户历史评分总和的权重避免喜欢到处评分的用户推荐分虚高。归一化的处理可以在 Service 层做用一个简单的排序加截取不需要引入复杂的计算框架。2.5 冷启动和数据稀疏的问题怎么解任何推荐项目都绕不开冷启动协同过滤对这个问题的回答是它本身不解决冷启动需要规则来兜底。新用户没有任何行为数据相似度计算毫无依据。项目里最常见的方案是给新用户直接展示热门景点榜作为推荐等他产生了几个浏览和评分行为后再切换成协同过滤的个性化结果。这个兜底逻辑用一句话就能实现统计评分表中每个景点的平均评分和评分人数按加权分数排序取前10个。新景点则相反没有任何用户评过它永远无法被推荐。解法是在后台给景点打标签比如自然风光历史文化亲子游乐美食之都然后用基于内容的简单匹配做补充当新景点与用户历史喜欢景点的标签重合度达到一定阈值时直接进入候选集。这就是基于内容的推荐和协同过滤的混合策略虽然实现简单但能把冷启动问题实实在在补上一个缺口。数据稀疏是另一个长期问题解决思路通常是离线计算 在线读取相似度矩阵不需要在用户请求时实时算而是定时任务离线算好存进推荐表或缓存用户请求时直接查结果。真实项目里这能省掉大量重复计算。上一版源码里如果没有定时任务自己加一个 Spring 的 Scheduled 定时方法每天凌晨跑一次相似度计算并更新推荐表就非常够用了。3. 前后端架构与核心功能实现3.1 后端分层设计Controller、Service、Mapper 怎么分工这个项目我强烈建议直接用 Spring Boot 的标准分层架构Controller 只做参数接收和结果返回Service 层写业务逻辑和算法调用Mapper 层用 MyBatis-Plus 操作数据库。分层的目的是让推荐算法可以被独立测试——你不需要启动整套 Web 服务就能在测试类里单独调用推荐 Service计算某个用户的推荐列表。核心接口设计基本如下功能模块接口路径请求方式说明用户注册/登录/api/user/register、/api/user/loginPOST密码加密存储返回 Token景点分页列表/api/spot/pageGET支持按名称、城市、类型筛选景点详情/api/spot/{id}GET返回景点信息、评分均值、评论列表提交评分/api/rating/submitPOST参数spotId、score自动关联当前用户收藏/取消收藏/api/favorite/togglePOST返回当前收藏状态个性化推荐/api/recommend/listGET核心接口返回 TopN 推荐列表热门景点榜/api/spot/hotGET冷启动兜底接口我自己见过很多版本的源码最容易出问题的地方有两个一个是评分接口没有做去重同一个用户对同一个景点重复提交数据库里出现多条记录导致推荐数据混乱解决方法是评分表加唯一索引user_id spot_id提交时做 upsert另一个是登录拦截器只拦了部分接口导致未登录用户也能调用评分接口演示的时候出现未登录也能评分的尴尬解决方法是写一个 WebMvcConfigurer 统一注册拦截器放行登录注册接口其他接口全部要求 Token。3.2 前端页面与组件设计Vue Router 和状态管理前端部分页面一般拆成六个视图登录注册页、景点列表页、景点详情页、个人中心页我的评分和收藏、个性化推荐页、管理后台页景点管理和用户管理。用 Vue Router 管理路由用的比较多的是 Vue 2 Element UI 或 Vue 3 Element Plus 的组合。路由表长这样const routes [ { path: /, component: Home, meta: { title: 首页 } }, { path: /spot/:id, component: SpotDetail, meta: { title: 景点详情 } }, { path: /recommend, component: Recommend, meta: { title: 猜你喜欢, requiresAuth: true } }, { path: /login, component: Login }, { path: /admin, component: Admin, meta: { role: ADMIN } } ]前端有一个关键点用户登录状态要存起来不能每次刷新页面都跳回登录页。做法是在登录成功后把返回的 Token 存到 localStorage同时在 Vue Router 的全局前置守卫里检查目标路由的 requiresAuth 和 localStorage 里的 Token没登录就跳转到登录页。Axios 请求封装里通过请求拦截器统一在 header 里带上 Token响应拦截器统一处理 401 状态码并跳转登录页。页面交互层面景点详情页的评分功能是核心交互用户点星星选分数前端立即给后端发请求成功后刷新页面上的平均分。推荐页的景点卡片展示景点图片、名称、城市、推荐理由推荐理由直接用因为你喜欢西湖所以推荐你灵隐寺这种可解释文案这比干巴巴的列表更有说服力答辩演示时效果也更好。3.3 数据库表设计评分表才是推荐算法的根数据库是整套系统里最不该偷懒的部分。表不需要多但每张表都要为推荐算法考虑。核心表设计如下user 表id、username、password、nickname、avatar、role、create_timespot 表id、name、city、type、description、cover_image、price_min、create_timerating 表id、user_id、spot_id、score、create_time唯一索引 (user_id, spot_id)favorite 表id、user_id、spot_id、create_timerecommend 表id、user_id、spot_id、reason、score、create_time我特别想提醒的是 rating 表的设计。推荐算法的核心输入就是它所以 score 字段不能存 null没评分就是没有记录不是存一条 score 为 null 的记录。另外用户对景区的评分行为天然稀疏——你不可能要求一个用户给几百个景点都打分——所以这张表的记录数规模决定了推荐效果。源码里如果数据量很少建议你自己生成一部分模拟数据至少 20 个用户、50 个景点、800 条评分记录否则算法演示起来推荐结果会非常稀疏效果很难看。recommend 表的用处是为了避免实时计算。定时任务跑完后把推荐结果写入这张表用户访问 /api/recommend/list 时后端直接查表返回性能非常稳定。实时计算适合演示算法生效的感觉但架不住并发访问和重复计算生产环境里离线推荐表是标配。4. 从源码到跑通完整部署流程笔记4.1 环境准备与工程目录解读拿到源码之后第一步不是点运行而是看清目录结构。常见结构是backend 目录装后端 Java 工程frontend 目录装前端 Vue 工程sql 目录放数据库初始化脚本doc 目录放课程设计或毕业设计文档。后端工程里按 controller、service、mapper、model、config 分包推荐算法的核心代码一般放在 service 包的 recommend 子包里。环境准备我建议按这个清单来JDK 1.8 或以上推荐 JDK 8 或 JDK 11兼容性最稳、Maven 3.6、MySQL 5.7 或 8.0、Node.js 14Vue 2 项目 Node 版本不要太高Node 18 可能报 node-sass 安装失败、npm 包管理器。4.2 数据库初始化与配置修改数据库部分打开 Navicat 或者命令行新建一个数据库比如叫 travel_recommend字符集选 utf8mb4排序规则 utf8mb4_general_ci然后导入 sql 目录下的初始化脚本。导入后检查表是否齐全特别是 rating 表和 recommend 表。后端配置文件的修改是新手最容易卡住的地方。打开 application.yml 或 application.properties把数据库的 url、用户名、密码改成自己的。url 里注意带上参数MySQL 8 的驱动要写 com.mysql.cj.jdbc.Driver这两个都是高频报错点spring: datasource: url: jdbc:mysql://localhost:3306/travel_recommend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver如果本地 MySQL 密码是 123456 就把 password 改过来别照抄。4.3 后端和前端启动的具体步骤后端启动前先确认 Maven 依赖能正常下载。在 backend 目录执行 mvn spring-boot:run或者用 IDEA 打开工程等右下角 Maven 依赖加载完直接运行主类。启动日志里看到 Tomcat started on port(s): 8080 就说明后端起来。前端启动相对麻烦一点。在 frontend 目录执行 npm install 安装依赖这一步如果速度很慢可以换成 npmmirror 的国内镜像源。安装完成后 npm run serve 启动开发服务器默认端口一般是 8081 或 5173。注意这里有个前后端联调的坑前端页面里调的接口地址要么用 axios 的相对路径配合 devServer 的跨域配置转发到 8080要么直接写后端完整地址。我建议用 Vue CLI 的 devServer 配置把 /api 开头的请求统一转发到 localhost:8080这样以后部署上线只需要改一处配置。devServer 配置片段devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }启动完成后浏览器访问前端地址注册一个账号随便浏览几个景点、评分、收藏然后在推荐页看效果。如果推荐列表能出来而且和你的行为相关整个系统就通了。4.4 文档和答辩材料怎么用这个项目的配套文档一般包含需求分析、系统设计、数据库设计、核心算法说明、测试报告、总结与展望。如果你是自己做课程设计不建议把文档当成交差材料而是把它当成答辩的剧本。核心算法说明这一章建议你自己重新过一遍从评分矩阵讲到相似度计算从相似度矩阵讲到 TopN 推荐配合系统里的真实接口截图让评委看到的不只是概念而是这个概念在这个系统里是怎么实现的证据。答辩被问到最多的问题就两个为什么选协同过滤以及它和基于内容的推荐有什么区别。把这两个问题在文档里写清楚答辩基本就稳了。5. 常见问题与排查技巧实录5.1 数据库连接失败和中文乱码数据库连不上的报错基本长得差不多Communications link failure 或者 Access denied for user。前者大概率是 url 写错端口或者 MySQL 服务没启动后者是用户名密码不对。检查顺序是服务是否启动、端口是否 3306、用户名密码是否匹配、url 里的数据库名是否存在。中文乱码问题一般出现在导入数据后景点名称变成问号或者前端提交的评分备注乱码。根源基本都在字符集确认三处都统一数据库表的字符集utf8mb4、连接 url 里的 characterEncodingutf8、前端页面的 meta charsetUTF-8。操作系统和数据库的默认字符集不一致时别忘了在 MySQL 配置文件里把 character-set-server 也设成 utf8mb4。5.2 前端白屏或页面 404前端白屏最常见的原因是路由模式。如果项目用了 history 模式本地开发直接访问子路由会报 404最简单的解法是换回 hash 模式或者在 devServer 里配置 historyApiFallback: true。另一个常见原因是 npm install 时报错导致部分依赖没装完页面运行时控制台一堆红色报错这种直接删掉 node_modules 重新 install 基本能解决。5.3 推荐接口报错或推荐结果为空推荐接口是最容易出问题的接口。排查思路要按数据流的方向走先查评分表里有没有数据评分数据是推荐算法的输入空表必然导致空推荐再查相似度表或 recommend 表有没有数据没有说明定时任务没跑或者算法计算出错最后查推荐接口的代码逻辑看看有没有把用户已经点击过的景点全部过滤掉过滤条件写错了也会导致结果为空。一个特别隐蔽的问题很多源码里的定时任务默认是关闭的需要手动调用一次触发接口或者配置 cron 表达式。如果你发现推荐列表一直是空的先找找项目里有没有 /api/recommend/trigger 之类的接口手动调用一次。5.4 推荐结果不理想全是热门景点或全是冷门景点推荐结果全是热门景点说明算法实际上没有生效代码可能在走热门榜兜底逻辑。常见原因是当前用户的行为数据太少触发了冷启动分支。推荐结果全是冷门到没人认识的景点大概率是相似度矩阵里出现了异常值或者没有对评分数据做归一化计算出的相似度全是极端值。处理办法是检查评分分数区间是否统一以及相似度计算时是否对分母为0的情况做了保护。分母为0的相似度直接置为0不然会出现 NaN 跟着排序推荐结果全乱。5.5 常见问题速查表问题现象可能原因排查与解决方案后端启动报端口被占用8080 被其他进程占用改 application.yml 的 server.port或杀掉占用进程前端请求接口 404转发路径或接口路径不一致检查 devServer 的转发规则与后端接口路径是否匹配登录后刷新页面就退出用户状态没有持久化确认 Token 存 localStorage路由守卫重新拉取用户信息评分提交后推荐没变化recommend 表没有更新触发定时任务或调用手动更新接口检查相似度是否已重新计算热点接口返回慢每次请求都实时计算相似度确认 recommend 表被使用避免每次在线跑全量算法最后说两句实际的这个项目真正跑顺之后我自己最深的体会是推荐算法本身并不神秘难的是把数据喂好。评分数据、行为数据、标签数据每一个环节的脏数据都会在最终推荐结果里放大成离谱的错误。所以做这类项目不要急着优化算法先确保基础功能产生的数据是干净、真实的。你哪怕只是把系统的评分接口做严谨、把模拟数据生成到位推荐效果不会差到哪里去。扩展方向上如果你想让这项目再上一个档次可以试试把相似度计算从单机内存换成 Redis 缓存热点结果或者引入一个简单的内容标签匹配做混合推荐。甚至可以加一个定时任务每天把新增的评分数据增量纳入相似度重算。每加一层项目的技术深度和答辩可讲的东西就多一层。做课程设计也好毕业设计也罢这套源码加数据库加文档的底子吃透了绝对够你稳稳拿一个好成绩。
返回列表