ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的旅游打卡点推荐系统实战解析

基于SpringBoot+Vue的旅游打卡点推荐系统实战解析 作为一个带过不少届毕业设计、也帮人排过不少雷的老程序员每次看到“XX推荐系统”这种题目第一反应都是这题可大可小关键看你怎么设计。往浅了做一个“最新发布”列表加个“猜你喜欢”查询就能交差往深了做协同过滤、用户画像、冷启动策略、AB实验全都沾得上。这篇博文我想结合一个具体的项目——基于SpringBootVue的旅游打卡点推荐系统把从技术选型、数据库设计、算法实现到前后端联调的完整链路讲清楚尤其会把推荐算法部分摊开揉碎了讲明白让准备做类似题目的朋友少走弯路也能给已经在做的人一些优化灵感。先说这次项目的定位。你没有给我具体的可运行代码也没有一份详细的说明文档标题就是“基于SpringBootVue的旅游打卡点的推荐系统设计与实现”这个标题本身信息量已经足够后端SpringBoot前端Vue核心业务是旅游打卡点的推荐。结合搜索热度来看这类题目在毕业设计、课程设计、甚至个人项目中都非常常见属于典型的“前后端分离经典推荐算法”的组合既能展示工程能力又能体现算法素养非常适合作为综合项目锻炼自己。1. 整体设计思路为什么选这套技术组合1.1 技术选型的底层逻辑先聊技术选型。SpringBootVue这套组合现在基本算是Java全栈开发的“标准答案”了但标准答案也得分清楚“为什么标准”。SpringBoot的核心价值是“约定优于配置”你不需要像传统SSH那样写一堆XML配置文件一个spring-boot-starter-web就能起一个Web服务内置Tomcat开发调试非常轻量。对于推荐系统这种业务复杂度尚可、但需要快速迭代接口的项目来说这种特性非常实用。Vue这边前端采用组件化开发模式把地图展示、打卡点列表、推荐卡片、评论区域拆成独立组件每个组件只关心自己的数据和交互逻辑这对旅游打卡点这种信息密集型的页面来说非常关键。比如同一个打卡点在推荐列表里展示的是摘要卡片在详情页里展示的是完整信息如果不用组件化代码复用就会变得很痛苦。还有一个选型细节容易被忽略数据库和ORM。这类项目主流选择是MySQL加MyBatis-PlusMySQL不用多说MyBatis-Plus的价值在于免去了大量单表CRUD的SQL编写内置的分页插件、条件构造器对做推荐系统的“筛选排序”非常顺手。如果你用JPA也不是不行但MyBatis-Plus在国内社区的资料更丰富遇到问题更容易搜到解决方案。至于推荐算法本身很多人一上来就纠结“我要用深度学习还是用图神经网络”这个思路跑偏了。对于毕设级别或者中小型实战项目基于协同过滤Collaborative Filtering加基于内容的推荐Content-based Recommendation混合方案才是最优解。原因有三第一数据量不大复杂模型容易过拟合效果反而不如经典算法第二协同过滤和基于内容的推荐原理清晰、代码实现难度适中答辩的时候能讲清楚第三两者天然互补——协同过滤能发现“相似用户的兴趣”基于内容能解决“新用户没行为”的冷启动问题。这套组合拿来做旅游打卡点推荐覆盖面很完整。1.2 系统模块与数据流设计整个系统的功能模块我把它分成五个核心块用户模块注册、登录、个人信息维护这是系统的基础也是推荐系统获取用户偏好的入口。打卡点管理模块管理员对打卡点信息的增删改查包括名称、分类、地理位置、图片、描述、开放时间等。推荐模块系统的核心为用户生成个性化推荐列表。内部拆分成离线计算和在线服务两部分离线算好结果存表在线直接读取。社交互动模块用户对打卡点的评分、收藏、评论、打卡记录。这些行为数据是推荐算法的“燃料”。前端展示模块Vue页面包括首页推荐流、打卡点详情、个人中心、地图展示等。数据流的走向是这样用户在前端产生行为比如给某个打卡点评了4分行为数据通过接口写入MySQL推荐模块定期比如每天凌晨跑算法任务读取所有用户的行为数据计算相似度矩阵和推荐列表把结果写回推荐结果表用户再次打开首页时后端直接查推荐结果表返回给前端展示。这样做的好处是推荐计算不阻塞在线请求用户体验不受影响也方便控制算法运行频率和资源消耗。这里有个设计经验我要特别强调推荐算法的计算过程写死在Service层不放到定时任务里硬编码。更好的做法是抽出一个独立的RecommendService接口提供generateRecommendations(Long userId)和generateAllRecommendations()两个方法既支持单个用户的重算比如用户刚登录时也支持全局的全量重算定时任务调用。后面优化算法或者换成Flink做实时计算也不至于推倒重来。2. 核心模块与数据库设计2.1 用户、打卡点与互动数据建模很多人在推荐系统上栽跟头不是栽在算法代码上而是栽在数据库设计上——行为数据表设计得不合理后面算法实现时取数就非常痛苦。我把核心表结构和设计思路一条条拆给你看。第一张核心表是用户表除了常规的id、username、password、avatar、created_at之外我建议加两个关键字段preferred_category偏好分类和home_city所在城市。preferred_category用于冷启动阶段的基于内容推荐——用户注册时可以勾选感兴趣的分类如“自然风光”“人文古迹”“城市地标”这就是最粗糙但有效的用户画像home_city则用于地理位置初筛旅游推荐系统如果不考虑地理位置推荐去离家几千公里外的景点体验会非常差。第二张核心表是打卡点表tourist_spot字段设计要尽量完整CREATE TABLE tourist_spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 打卡点名称, category VARCHAR(50) NOT NULL COMMENT 分类自然风光/人文古迹/城市地标/美食街区, city VARCHAR(50) NOT NULL COMMENT 所在城市, address VARCHAR(255) COMMENT 详细地址, longitude DECIMAL(10, 6) COMMENT 经度, latitude DECIMAL(10, 6) COMMENT 纬度, cover_image VARCHAR(255) COMMENT 封面图URL, description TEXT COMMENT 简介, avg_rating DECIMAL(2, 1) DEFAULT 0.0 COMMENT 平均评分, rating_count INT DEFAULT 0 COMMENT 评分人数, open_time VARCHAR(50) COMMENT 开放时间, is_active TINYINT(1) DEFAULT 1 COMMENT 是否上架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category), INDEX idx_city (city), INDEX idx_active (is_active) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计细节值得说。longitude和latitude用DECIMAL(10,6)而不是FLOAT或DOUBLE因为浮点数在经纬度这种精度敏感的数据上容易产生误差DECIMAL是精确类型六位小数足够精确到米级别。avg_rating和rating_count属于冗余字段它们可以从评分表里实时聚合出来但每次推荐列表都要用到平均评分排序实时聚合性能压力很大不如在写入评分时同步更新这两列。代价是增加了一点逻辑复杂度但换来的是读性能成数量级的提升——这就是典型的“以写换读”思路。第三张核心表是用户行为表这一步很关键。推荐的直接依据就是用户与物品的交互历史。我先说结论不要只建一张行为表而是拆成评分表、收藏表、打卡记录表三张。原因在于这三类行为的数据语义和权重完全不同评分反映用户对打卡点的好恶程度收藏反映意愿强但需要翻译成评分打卡记录反映用户真实去过而这个行为历史还涉及隐私问题后续算法计算时三类行为要分别以不同权重参与计算。拆分开来代码写起来更清爽不至于出现“一张表里既有评分字段又是收藏字段又是打卡字段”这种四不像结构。建议的评分表设计如下CREATE TABLE user_rating ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, score TINYINT NOT NULL COMMENT 评分1-5, comment_content VARCHAR(500) COMMENT 评论文本, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_spot (user_id, spot_id), INDEX idx_user (user_id), INDEX idx_spot (spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;UNIQUE KEY uk_user_spot这个唯一索引非常关键——它从数据库层面保证了同一个用户对同一个打卡点只能有一条评分记录后续用INSERT ... ON DUPLICATE KEY UPDATE做更新操作时非常方便也避免算法计算时出现重复数据导致评分矩阵错乱。2.2 建表语句与索引设计注意事项数据库设计这块踩过不少坑我总结几条避坑经验。一是字符集一定要用utf8mb4。虽然这里不发emoji但打卡点的描述文本里很可能出现特殊符号、生僻人名比如景点出处utf8mb4能覆盖所有Unicode字符。项目初期用utf8上线后才发现某些数据写不进去再改字符集既慢又容易出幺蛾子。二是尽量使用逻辑删除而不是物理删除。推荐系统的数据挖掘中用户历史行为是宝贵资源管理员误删打卡点时如果直接DELETE这个打卡点在历史行为数据里就消失了用户行为矩阵就会产生空洞。我的做法是给tourist_spot表加is_active字段下架打卡点只是把is_active置为0推荐列表查询自动过滤但历史行为记录保留。这个小小的设计决策能给后期数据分析和算法重算留足余地。三是行为数据表只做简单统计查询不搞复杂联表。用户行为表只保留user_id、spot_id和具体行为字段需要打卡点名称、分类时再通过Service层二次查询并组装。如果一上来就在SQL里LEFT JOIN三四张表看起来效率高实际上在数据量大之后会成为性能瓶颈而且SQL写复杂了后期维护非常痛苦。推荐系统本身就是“数据搬运工”系统明确每一条数据从哪里来、到哪里去才是正路。3. SpringBoot后端推荐算法与接口实现3.1 分层架构与接口设计后端架构我还是用标准的Controller-Service-Mapper三层这个结构虽然老套但对于这个规模的项目来说逻辑清晰度最高。Controller层只做参数接收和结果封装不写任何业务逻辑Service层放推荐算法核心逻辑Mapper层对应MyBatis-Plus的BaseMapper接口做CRUD。额外加一层dto和vodto用于接收前端参数和内部方法之间传参vo用于返回给前端的展示对象。把它分开的原因很简单——尤其是做推荐系统内部要算相似度、推荐分中间产物的结构跟数据库实体完全不对等硬塞在一个对象里只会越来越混乱。核心接口设计我直接列出来接口方法说明/api/registerPOST用户注册返回token/api/loginPOST用户登录返回token/api/spotsGET分页获取打卡点列表支持分类/城市筛选/api/spot/{id}GET打卡点详情/api/recommend/{userId}GET核心推荐接口返回推荐列表/api/spot/ratePOST用户给打卡点评分/api/spot/favoritePOST用户收藏打卡点/api/user/historyGET获取用户历史行为/api/recommend/{userId}是核心中的核心它返回的内容不是简单从数据库随机捞几条而是带着推荐理由的数据结构比如“和你一样喜欢自然风光的小李也收藏了这里”这种推荐理由能显著提升用户体验。返回结构设计成下面这样{ code: 200, data: { spots: [ { spotId: 12, name: 鼓浪屿, category: 人文古迹, city: 厦门, coverImage: http://..., avgRating: 4.7, recommendReason: 与你收藏的海滨风光类打卡点风格相似 } ], total: 20 } }3.2 基于用户的协同过滤算法实现推荐算法的核心实现来了。基于用户的协同过滤User-Based Collaborative Filtering的基本假设是如果两个用户过去对某些打卡点的评分/行为相似那么他们未来对其他打卡点的偏好也相似。这句话翻译成工程步骤就是三步第一步构建“用户-打卡点”评分矩阵。这里的评分不是简单拿user_rating表里的分值直接用而是要把不同类型的隐式行为合并成统一评分。我常用的做法是评分行为直接取1-5分收藏行为默认4分因为收藏说明用户明确表达了喜欢浏览/打卡行为默认3分代表中度兴趣没有行为则为0。加权合并后得到一个稀疏矩阵矩阵的行是用户列是打卡点值是用户对打卡点的兴趣程度。第二步计算用户之间的相似度。相似度算法我选的是余弦相似度。为什么要用余弦而不是皮尔逊相关系数原因是评分矩阵特别稀疏大量用户只对少数打卡点打过评分皮尔逊相关系数要求至少对同一组物品评分过才能计算在稀疏场景下容易计算出无法解释的负值。余弦相似度只关注向量方向一致性天然能处理稀疏向量。两个用户u1和u2的余弦相似度计算方式如下similarity(u1, u2) (u1向量 · u2向量) / (|u1向量| * |u2向量|)第三步预测目标用户的未评分物品分数并排序。对用户u找到相似度最高的K个用户K取30左右用这K个用户的评分加权平均来预测用户u对打卡点p的兴趣程度prediction(u, p) sum(sim(u, v) * rating(v, p)) / sum(sim(u, v))其中rating(v, p)是相似用户v对打卡点p的评分sim(u, v)是用户u和用户v的相似度。最后把所有未评分打卡点的预测分数算出来按分数从高到低取前20个作为推荐列表。代码实现我用Java来写核心逻辑放在UserCFRecommender类里public class UserCFRecommender { // 用户-物品评分矩阵key是用户idvalue是Map打卡点id, 评分 private MapLong, MapLong, Double userItemMatrix; // 预先计算好的用户相似度矩阵 private MapLong, MapLong, Double userSimMatrix; public ListLong recommend(Long targetUserId, int topN) { // 1. 获取目标用户的评分Map MapLong, Double targetUserRatings userItemMatrix.get(targetUserId); if (targetUserRatings null) { return Collections.emptyList(); // 冷启动用户交由其他策略处理 } // 2. 找相似用户 MapLong, Double simScores userSimMatrix.get(targetUserId); if (simScores null) { return Collections.emptyList(); } // 3. 优先取相似度Top 30的用户 ListMap.EntryLong, Double topSimUsers simScores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(30) .collect(Collectors.toList()); // 4. 加权计算候选打卡点得分 MapLong, Double scores new HashMap(); MapLong, Double simSum new HashMap(); for (Map.EntryLong, Double simUser : topSimUsers) { Long simUserId simUser.getKey(); Double simValue simUser.getValue(); MapLong, Double simUserRatings userItemMatrix.get(simUserId); for (Map.EntryLong, Double rating : simUserRatings.entrySet()) { Long spotId rating.getKey(); // 过滤掉目标用户已经打过分的打卡点不推荐已经去过的 if (targetUserRatings.containsKey(spotId)) { continue; } scores.put(spotId, scores.getOrDefault(spotId, 0.0) simValue * rating.getValue()); simSum.put(spotId, simSum.getOrDefault(spotId, 0.0) simValue); } } // 5. 归一化得分并排序 ListLong candidates new ArrayList(scores.keySet()); candidates.sort((a, b) - { double scoreA scores.get(a) / simSum.get(a); double scoreB scores.get(b) / simSum.get(b); return Double.compare(scoreB, scoreA); }); return candidates.stream().limit(topN).collect(Collectors.toList()); } }注意代码里的几个细节过滤已经评过分的打卡点否则系统反复推荐用户已经去过的地方体验极差相似度归一化时用simSum分母做加权平均避免高频用户对结果产生过大影响K值选30是经验值K太小推荐结果容易局限在极少数相似用户的喜好上K太大会引入大量弱相关用户噪声很大。还有一个实际工程问题用户相似度矩阵在用户量大时计算量是O(N²)对性能挑战很大。处理思路是“计算出相似用户只看候选集不遍历全部用户”——先通过用户所在的同城、已收藏同类打卡点等粗粒度条件圈出一批候选用户只在这个子集内计算精确相似度。这个策略在数据量不大时体现不明显但这是学术型算法到工程型算法的关键一步。3.3 基于内容的推荐与冷启动处理协同过滤虽然强大但它有一个天生缺陷——冷启动问题。新用户没有任何行为数据新打卡点没有任何用户评分协同过滤对它们直接失效。这时候就需要第二套算法兜底基于内容的推荐。基于内容的推荐思路很直接根据打卡点本身的属性分类、城市、标签计算物品之间的相似度然后推荐与用户曾经感兴趣的打卡点相似的新打卡点。翻译成实现步骤把打卡点的分类、城市、标签文本拼接成一个特征向量。计算两个打卡点的特征向量相似度。这里我用的是TF-IDF加余弦相似度。用户对某个打卡点产生过正向行为后找到和它最相似的N个打卡点推荐给用户。实际代码里我实现了一个ContentBasedRecommender类步骤是从用户行为表里取出用户评分最高的打卡点提取它的分类、城市、标签然后从打卡点表里找出同分类的数据按TF-IDF相似度排序过滤掉用户已交互过的取前N个返回。这个逻辑在上面的协同过滤代码之外单独实现两个推荐器通过一个RecommenderRouter做路由——用户行为数据量大于阈值时走协同过滤小于等于阈值时走基于内容最后再叠加热门打卡点作为托底。热门托底逻辑很简单但不能不做取全站rating_count大于某个阈值且avg_rating较高的打卡点按活跃度排序取前N条。这样做有两个作用系统刚上线没有用户行为数据时首页有东西可展示同时在后端实现里作为协同过滤和基于内容推荐都为空时的最后兜底方案。这里有一个Engineering判断系统追求的目标不是“永远推荐最精准”而应该是“永远保证有推荐”兜底策略保证了整个推荐链路不出现空缺。4. Vue前端从页面到交互的落地细节4.1 项目初始化与路由设计前端部分我用Vue 3加Element Plus脚手架用Vite。Vue 3的Composition API在处理推荐列表这种频繁数据更新的场景下体验很好ref、reactive、computed的组合让状态管理变得清晰。Vite作为构建工具比Webpack轻快不少本地开发热更新基本秒级生效。前端项目的目录结构我按功能模块拆src/ api/ # axios请求封装每个后端接口对应一个函数 assets/ # 静态资源 components/ # 公共组件 SpotCard.vue # 打卡点卡片推荐流/列表页通用 SpotMap.vue # 地图组件 RatingStars.vue # 评分星星组件 EmptyState.vue # 空状态组件 router/ # vue-router 路由配置 views/ HomeView.vue # 首页推荐流 SpotDetailView.vue # 打卡点详情 LoginView.vue RegisterView.vue ProfileView.vue # 个人中心 AdminView.vue # 管理后台 stores/ # Pinia状态管理主要存用户登录态路由设计这里有一点容易忽略推荐系统页面中“用户身份”是核心参数。/recommend/{userId}这种路由设计虽然直观但暴露了用户ID的安全隐患。我的做法是路由参数里不传userId推荐请求走axios拦截器统一从Pinia store里取当前登录用户的ID配合后端JWT做身份校验。因为默认路由配置时后端校验只验证token有效性请求时可以顺手在拦截器里读store取userId——这样更安全也更方便。4.2 推荐页面的组件化实现推荐首页组件HomeView.vue是核心页面结构分三块顶部搜索栏、筛选条件区分类Tab、城市下拉、推荐列表区。推荐列表区是一个可无限滚动的纵向列表核心组件是SpotCard.vue。SpotCard组件设计我先说思路再给代码。每个景点卡片要展示封面图、名称、分类标签、评分、一句话推荐理由。推荐理由来源于后端接口返回的recommendReason字段推荐结果那里没增加这个信息前端就是不完整的体验。卡片底部放两个操作按钮“评分”和“收藏”。代码核心部分template div classspot-card clickgoDetail el-image :srcspot.coverImage fitcover classspot-cover / div classspot-info div classspot-header span classspot-name{{ spot.name }}/span el-tag sizesmall{{ spot.category }}/el-tag /div div classspot-meta el-rate :model-valuespot.avgRating disabled / span{{ spot.avgRating }} ({{ spot.ratingCount }}人评价)/span /div p classrecommend-reason{{ spot.recommendReason }}/p /div /div /template script setup import { useRouter } from vue-router import { useUserStore } from ../stores/user const props defineProps({ spot: { type: Object, required: true } }) const router useRouter() const userStore useUserStore() const goDetail () { // 只有登录用户才能查看推荐理由的完整信息 if (!userStore.isLoggedIn) { router.push(/login) return } router.push(/spot/${props.spot.id}) } /script这里有个很关键的交互设计逻辑打卡点信息是公开数据但“推荐理由”是千人千面的个性化数据必须依赖用户身份。所以组件里判断登录态未登录用户点击卡片先跳登录页。登录后推荐接口会重新计算前端展示的推荐理由就是针对当前用户的了。这种体验比“上来全是统一内容”高级很多。无限滚动推荐列表的实现我直接用el-scrollbar监听滚动事件滚动到底部阈值时触发loadMore调用/api/recommend/{userId}?page2继续拉取数据追加到spots数组。这里要注意推荐算法每次重算的结果可能是变化的前端分页时必须启用后端缓存按userId页码做缓存否则用户滑到第二页时发现和第一页重复了体验直接崩掉。4.3 前后端联调与打包部署前后端联调阶段最大的痛点是跨域问题。开发时期Vite默认端口5173SpringBoot默认端口8080端口不同必然存在跨域。我在后端加了一个全局CORS配置类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); } }这个配置解决的是开发阶段的Debug体验问题。注意allowedOriginPatterns(*)配合allowCredentials(true)不能像以前那样用allowedOrigins(*)否则浏览器会拦截携带凭证的跨域请求。生产环境部署时为了让前端代码被打包进SpringBoot常见做法有两种一是把Vue构建产物放入src/main/resources/static目录SpringBoot自动托管二是用Nginx做反向代理前端请求走相对路径转发到后端。第一种适合毕设演示或小规模部署第二种适合大规模生产环境这里看项目具体需求选择。用热词里提到的“vue打包放进springboot中”的方法具体步骤是# 前端构建 npm run build # 把 dist 目录内容复制到 SpringBoot 的 static 目录 cp -r dist/* src/main/resources/static/ # 重新打包后端 mvn clean package -DskipTests java -jar target/recommend-system-0.0.1-SNAPSHOT.jar这样一个JAR包就能同时承载页面和后端接口部署时只需要配好MySQL连接信息即可。但有一个注意点打包进JAR的前端页面如果后续要改需要重新构建整包。如果项目还在迭代表态建议还是前后端分离部署不要图省事打包在一起。5. 常见问题与排查技巧实录做这个项目过程中我踩过不少坑挑几个典型的记录下来帮后来人省点时间。5.1 算法层面的坑与应对问题一推荐结果全是热门景点找不到个性化。如果你发现协同过滤推荐出来的列表和“全站热门”列表几乎一样大概率是用户相似度计算出现了问题——要么是相似用户选取的K值太大热门景点权重过高要么是评分矩阵里热门景点的评分数量远高于长尾景点相似度计算被稀疏性带偏了。解决思路做物品的流行度归一化热门程度越高的物品在相似度计算中的贡献越小。问题二新用户和新打卡点完全没有推荐。这是冷启动混淆问题系统可能已经在数据库里有用户但用户还没有任何行为此时协同过滤直接返回空列表。解决方法是把推荐链路的兜底策略做扎实。我在服务业里叫“推荐漏斗”基于内容推荐 → 协同过滤 → 热门兜底一层层往下走。这个漏斗每一次都应该打日志记录每个路径推荐出来的数量方便你在后台查看系统健康度。问题三评分预测算法算出的分数全是某个固定值。通常是编码时用了int做除法simValue * rating.getValue()除以simSum时整数相除丢了精度。记住Java里所有涉及分数、相似度的计算一律用double分子分母收集阶段用Double包装类型最后再归一化。5.2 联调与部署阶段的坑后端问题MyBatis-Plus分页不生效。这是最常见的坑之一。MyBatis-Plus的分页插件需要显式配置PaginationInnerInterceptor很多人忘了加拦截器导致Page参数传进去了但SQL没有LIMIT一页返回全量数据。这个问题排查方式很直接在控制台打印SQL日志看SQL末尾有没有LIMIT。前端问题Vue打包后首屏加载白屏。通常是静态资源路径问题。Vite构建时默认资源路径是绝对路径/assets/...部署到子路径时资源找不到。解决方式是在vite.config.js里设置base: ./强制构建产物使用相对路径。// vite.config.js export default defineConfig({ base: ./, plugins: [vue()] })数据库问题中文乱码。如果你用DATETIME和VARCHAR设计表和字段时没指定字符集插入中文数据后全是问号。在建库时统一指定CREATE DATABASE travel_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时jdbc连接串里加characterEncodingutf8双保险。5.3 热词中提到的几个高频问题速查搜索热词里出现了很多SpringBoot和Vue相关的提问这里我挑几个常见的做个快速解答springboot版本太高导致的启动问题Spring Boot 3.x要求JDK 17如果你的本机还是JDK 8Spring Boot 3.x会直接启动报错。如果坚持用JDK 8选择Spring Boot 2.7.x版本即可。这是最稳妥的方案。springboot默认使用cglib代理这说的其实是Spring AOP的代理方式。Spring Boot 2.x开始默认spring.aop.proxy-target-classtrue也就是CGLIB而不是JDK代理。如果你理解这个背景在写切面时就不会莫名其妙地发现接口代理不见了。vue播放m3u8免安装和vue image能显示pdf吗这些偏前端展示问题。打卡点系统里需要展示图片Vue图片展示直接用img配合URL即可如果涉及视频播放m3u8流需要引入hls.js库解析并播放打卡点详情页如果涉及PDF导览图前端可以直接套一个iframe srcpdf路径但要注意和浏览器跨域策略的兼容问题。springboot整合activemq如果你想把推荐系统做成异步架构比如用户行为数据先丢进消息队列再异步消费写入数据库并触发重算用ActiveMQ或RabbitMQ会是很好的后期扩展方向。核心思路是用户评分接口先发消息到队列立即返回成功给前端后台消费者异步更新评分矩阵和推荐结果不影响用户操作。6. 后续优化方向与扩展思路系统能跑起来只是第一步真正的进阶空间很大。我个人建议后续在这个基础上做以下扩展。一是引入混合推荐策略的权重动态调整。现在的混合策略是固定的“协同过滤优先冷启动走内容推荐”效果已经不错。更高级的做法是记录每次推荐结果的曝光和用户点击数据通过小规模的线性回归学习不同推荐策略在当前场景下的最优权重系数。简单说如果这周协同过滤推荐的曝光转化率高就自动加大协同过滤结果的占比。这种“推荐策略的自适应”是工业级推荐系统的标配但用线性回归模型就能在毕设项目中实现不算太难。二是引入实时计算框架。热词里出现了“springboot整合flink”这暗示目前的系统实时性不够。当前架构是离线计算推荐结果用户行为变更后需要等定时任务重算才能生效。如果对实时性有追求可以引入Flink用户行为写入KafkaFlink流式消费更新推荐结果。这个扩展的思路是把推荐链路改造成“离线计算实时增量更新”两条腿走路用Flink维持每个用户最近N次行为的实时画像离线层保留全量用户的历史偏好计算。注意这个方案适合数据量大、后续演进需求明确的场景如果数据量是几万用户级别的先别上这套不然运维成本会很酸爽。三是引入地理位置的推荐加权。旅游推荐系统比通用商品推荐更依赖地理位置。后续可以给打卡点增加城市、经纬度信息在推荐计算时对同城打卡点加权。甚至可以结合高德地图API、Mapbox展示打卡点的地图分布用户在地图上一眼就能看出自己关注的区域的景点分布。实时位置的推荐权重计算是这类旅游系统的天然优化方向值得花时间打磨。四是前端展示的推荐解释模块。产品体验好不好推荐理由起了很大作用。前端界面除了推荐的卡片列表还可以增加一个“为什么推荐”的折叠面板点击后展示推理解释“因为你有3个自然风光的收藏系统为你找到了以下几个同类型的打卡点”这个逻辑在任何推荐系统里都是性价比很高的体验提升点。实现也不复杂后端在生成推荐结果时额外返回推荐理由字段即可。最后分享一个小经验不要让推荐算法吃掉你整个项目的开发时间。很多同学一上来就纠结“我这个算法是不是不够fancy”结果业务系统草草了事。实际上一个业务功能完整、推荐链路闭环离线计算冷启动兜底热门推荐的系统比一个算法花哨但业务千疮百孔的系统分数高得多。这套项目做完之后你可以把真实的使用心得——比如哪个K值在用户数据达到什么规模时表现最好、冷启动数据的覆盖率大概多少——记录下来这些实战数据是任何教材里都学不到的也是你面试或答辩时最能打动人的谈资。
返回列表