
“Spring Boot旅游景点推荐管理系统”这类标题在各大毕设和培训机构项目库里基本是标配了。但你真把它当成一个“随便写写增删改查的课设”来糊弄那就踩坑了。这个名字背后实际上是一整套需求景点的基础信息管理、用户侧的检索收藏、标签体系的搭建以及一个能让用户“逛得下去”的推荐逻辑。单纯把景点列表拉出来展示那不叫推荐系统那叫“景点后台管理”。真正到面试官或答辩老师问起“你的推荐是怎么算的”你得能讲清楚算法逻辑、数据流向和冷启动处理这茬才算真正过关。这篇文章我直接把自己做这类项目时沉淀下来的方案、代码、参数选择和踩坑点全部写出来按从整体设计到细节实现的顺序走你照着推演一遍比自己盲写一个月强得多。1. 项目梳理与技术选型的取舍1.1 项目核心需求到底有哪些先别急着写代码。你把“旅游景点推荐管理系统”这十个字拆开看核心其实四个字景点、推荐。前面的“旅游”和“系统”都是场景限定。真正要考虑的是下面几条景点信息维护管理员能添加、编辑、上下架景点景点通常包含名称、所在城市、门票价格、开放时间、简介、图片、标签等字段。游客浏览路径普通用户注册登录后可以浏览景点列表、按城市或关键词搜索、查看景点详情、收藏感兴趣的景点。推荐逻辑这是项目区别于普通管理系统的关键必须有实际算法参与计算而不是“查个列表按时间排序”糊弄过去。简单统计管理员后台能看到热门景点排名、用户收藏趋势等这属于加分项很多人忽略但答辩老师很吃这套。一句话总结用户数据 景点数据 行为数据是推荐系统的三大输入缺一不可。所以你的数据库设计从一开始就要为“行为数据”留表别像有些人只看景点和用户两张表就开写。1.2 为什么选Spring Boot作为底座这类项目选Spring Boot几乎是最省力的路线没有之一。Spring Boot把配置自动化和依赖管理做到位之后整个后端结构非常清晰Controller接收请求、Service处理业务、Mapper操作数据库标准的三层结构配合Maven管理依赖一个人从零搭项目到完成核心功能正常节奏两到三周能跑通。说几个选Spring Boot而不是其他框架的实际理由自动装配机制省去大量XML配置。以前用SSM框架要手动配数据源、配事务、配MyBatis工厂Spring Boot用spring-boot-starter全家桶就能把环境拉起来。内嵌Tomcat打包成JAR直接跑部署非常方便这对毕设演示或自己玩耍极其友好。生态里整合MyBatis、JPA、Redis、定时任务都是开箱即用后面想加功能不用推翻重来。但注意Spring Boot有一个很坑的点就是版本选择。现在很多教程直接让你上Spring Boot 3.x看起来是“选新不选旧”但你如果是JDK 8环境还得回头改Java版本非常折腾。我建议做成毕设或学习项目就稳稳用Spring Boot 2.7.x JDK 8 MyBatis MySQL 5.7或8.0。这个组合踩坑的人最多网上资料也最全出了诡异问题大概率有现成解法。1.3 前端方案选Vue前后端分离还是模板引擎这是很多初学者第一个纠结的点“我用不用Vue”我的建议很直接如果时间紧、想尽快看到东西用Spring Boot整合Thymeleaf前端页面放在templates目录里ModelAndView直接渲染开发速度最快。但如果你想在简历上写“前后端分离经验”那就老老实实把前端拆出来用Vue 2或Vue 3 Element UI跑在8081端口后端8080通过接口联调。前后端分离的好处不是“看起来高端”而是写代码的时候你天然会关注接口设计——返回什么格式、错误码怎么定义、分页参数怎么传给前端。这些问题在后端面试时非常常考项目里有真实实践的话回答起来完全是加分项。至于Vue打包好之后要不要放进Spring Boot的static目录里如果项目做完想在服务器上单机部署演示建议最后把这步做了。前端npm run build生成dist目录拷贝到src/main/resources/static下JAR包启动后直接访问80端口就能看到页面省得在服务器上还要配Nginx或单独部署前端。2. 系统设计与数据库模型2.1 分包结构与整体设计思路后端包结构我一般按下述方式来组织清晰且符合日常工作习惯com.example.travel ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // 数据访问层MyBatis Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互对象VO ├── config // 配置类CORS拦截器 ├── common // 统一返回结果、异常处理 └── recommend // 推荐算法相关类多说一句common包的必要性。统一返回结果类ResultT几乎是项目标配包含code、msg、data三个字段前端统一按这个结构解析能省很多事。加上全局异常处理器RestControllerAdvice后端就不会动不动把异常栈直接甩给前端看起来很业余。数据库设计上我把核心表列出如下表名核心字段职责说明userid, username, password, nickname, avatar, city用户基础信息scenicid, name, city, price, open_time, description, cover, status, tag_ids景点主表tagid, name标签字典表scenic_tagid, scenic_id, tag_id景点-标签关联表favoriteid, user_id, scenic_id, create_time用户收藏记录ratingid, user_id, scenic_id, score, create_time用户评分记录可选能加就加上browse_logid, user_id, scenic_id, count浏览行为聚合表这里强调一下推荐算法常用的“用户-物品”行为数据在系统里主要由favorite和browse_log提供。很多人做毕设时只在表里设计了收藏没存浏览行为结果做协同过滤时发现用户的行为数据太稀疏根本算不出来。我建议从一开始就加一个浏览日志表用户每次点进景点详情就更新一次count后面做推荐时的数据源就丰富很多。2.2 景点标签体系怎么设计标签是整个推荐系统的润滑剂。没有标签基于内容的推荐就无从下手。我看到的很多毕设项目做了标签表但只是给景点加了个tags字符串字段用逗号分隔这种做法在查询时确实方便但做推荐时还得手动拆分字符串非常别扭。正确做法是拉一张关联表scenic_tag体现“多对多”关系一个景点可以有多个标签一个标签也可以关联多个景点。推荐计算时直接SQLjoin查出一个景点的标签ID集合再和用户兴趣标签集合做集合运算即可。标签内容怎么来不用人工去编太多初始数据够用就行比如历史古迹、自然风光、亲子游、美食打卡、户外徒步、红色旅游如果命题敏感度要注意的话普通项目里建议不要涉及这类容易带出争议方向的标签、城市地标、主题乐园。管理员后台再加一个简单的标签管理功能能增删改查就够了。2.3 关于“热门景点”这个隐藏需求你细心观察旅游类App几乎都有“热门推荐”“本周TOP10”这种模块。放在我们项目里这也属于推荐的一部分——冷启动兜底推荐。热门榜的计算逻辑可以很简单根据收藏数加浏览数加权排序比如score favorite_count * 3 browse_count * 1为什么要加3倍权重因为收藏是强意图行为浏览只是泛兴趣强行为应该比弱行为权重高。这里你用公式就能让答辩老师看出你对业务场景有思考而不是一拍脑袋随便排序。热门榜的实现方式也建议用定时任务每10分钟或每小时算一次缓存到Redis或内存Map里而不是每次用户请求时实时算因为涉及两张表的聚合查询实时算会让数据库压力变大。Spring Boot里用Scheduled(cron 0 0/10 * * * ?)就能搞定。3. 推荐算法从0到1的实现3.1 基于内容的推荐最短路径基于内容推荐的核心逻辑是“给用户推荐和他喜欢的景点相似的景点”。具体落到这套系统里就是把景点变成标签向量然后判断两个景点的相似度。假设目前系统里有三个标签自然风光A、历史古迹B、亲子游C。景点X的标签是[自然风光, 历史古迹]用向量表示就是(1,1,0)景点Y的标签是[自然风光, 亲子游]向量就是(1,0,1)。两者相似度怎么算最简单的就是余弦相似度cos (X · Y) / (|X| * |Y|)代入数值分子 11 10 0*1 1分母 sqrt(2) * sqrt(2) 2所以相似度是0.5。这个值介于0到1之间越接近1表示越相似。公式你不用自己推但要在代码里写清楚别光调库。整个推荐流程走下来是这样取当前用户收藏数量最多的前N个景点作为“兴趣种子集”。对每个种子景点在候选景点池里排除掉“用户已收藏/已浏览过”的景点。用余弦相似度计算种子景点和候选景点的相似度。累加相似度分数按分数取Top10返回给前端。这里的候选景点池不能是全部景点否则每次计算性能很差。项目数据量小无所谓但为了体现工程意识建议按城市过滤一次同城的候选池参与计算即可。旅游场景里用户大概率对同城目的地更感兴趣这也是合理的业务假设。3.2 基于用户的协同过滤另一种思路UserCF的核心是“找到和你兴趣相似的用户看看他们在看什么”。步骤是把用户收藏的景点集合取出来用Jaccard相似度算用户之间的相似度两个用户收藏集合的交集除以并集。取Top5相似用户把这些用户收藏过但当前用户没看过景点找出来。按“有多少相似用户收藏过”进行排序加权推荐。这个方法比基于内容推荐更“个性化”但问题在于数据稀疏。如果系统总共才几十个用户大家收藏行为少Jaccard算出来基本全是0。所以我在这个项目里有一个明确取舍主推荐用基于内容的标签相似度算法UserCF作为辅助推荐只在新用户或热门榜上做补充。这个取舍你在项目答辩时务必主动讲出来而不是等老师问。主动暴露方案的边界和权衡比被动辩解要专业得多。3.3 代码结构怎么组织我在recommend包下面放了三个类RecommendContext存储用户兴趣标签、收藏集合、候选池等中间数据。ContentBasedRecommender执行基于内容推荐的核心逻辑。CollaborativeFilterRecommender执行UserCF逻辑。Service层调用方并不需要关心具体算法直接注入推荐器接口即可。这样设计的好处是算法可以替换我的代码里Recommender接口定义了一个统一方法public interface Recommender { ListScenicVO recommend(Long userId, int topN); }这里注意一点返回的推荐结果建议组装成ScenicVO而不是直接返回Entity。VO里可能多出similarityScore、recommendReason这类展示字段前端可以直接展示“因为你看过西湖所以推荐你灵隐寺”这种文案产品体验和答辩观感都会好很多。3.4 冷启动问题怎么兜底新用户没有收藏没有浏览推荐算法完全算不了。此时直接返回热门景点列表是最稳妥的做法。我在实现时处理了三层第一层如果用户收藏数为0直接返回hotList即热门榜的前10条。第二层如果用户有收藏但候选池计算后推荐结果不足10条用热门榜补位填充到10条。第三层如果用户对某类标签有明显的集中兴趣比如收藏中的7个景点有6个带“自然风光”在推荐结果中把这类景点的比例提到70%这算是一点简单粗糙的“个性化干预”。冷启动你不是写死成“热门兜底”就完事三个层次能让评委看出你对真实推荐系统场景是有理解的。4. 核心模块设计与实操记录4.1 用MyBatis还是MyBatis-Plus我看到热搜里有一串 “springbootmybatis” 相关的词说明这个组合确实是主流。但我要给你建议如果用MyBatis-Plus能少写大量CRUD的XML因为它内置了通用Mapper和分页插件。单表CRUD用BaseMapper接口直接搞定自己只需要在XML里写复杂的多表join和统计查询。但你同时也得注意MyBatis-Plus有几个容易踩的坑逻辑删除配置。如果你给景点表加了deleted字段得用TableLogic注解否则删除后数据还在但查询却查不出来很诡异。分页插件要单独配置MybatisPlusInterceptor不配置Page对象就失效查出来是全量数据这个问题当年坑了我半小时。多表关联查询时要自己写XML并用resultMap做映射关系否则字段对不上。4.2 推荐计算中的SQL怎么写基于内容的推荐第一步要“取用户最感兴趣的标签集合”这一步用SQL就能完成SELECT t.id, t.name, COUNT(*) AS interest_count FROM favorite f JOIN scenic_tag st ON f.scenic_id st.scenic_id JOIN tag t ON st.tag_id t.id WHERE f.user_id #{userId} GROUP BY t.id, t.name ORDER BY interest_count DESC LIMIT 5这条SQL的作用是把用户收藏景点所涉及的标签按频次排序作为用户兴趣标签。要注意JOIN的顺序和别名别写错了。候选池的SQL我放在了Service里用LambdaQueryWrapper拼接目的是把城市过滤和已收藏过滤做到动态SQL里简化XML。4.3 景点图片上传处理要点景点管理里图片上传是必做的很多新手直接存Base64到数据库。我不建议这么做数据库会迅速膨胀而且查询传输效率差。正确的常规做法是文件上传到服务器某个固定目录数据库只存相对路径。Nginx或虚拟路径映射是指向那个目录的。Spring Boot里设置静态资源映射也比较简单搞一个配置类Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath /); } }这样前端拿到/upload/xxx.jpg就能直接访问到图片不用把文件提交给后端再转字节流返回。注意上传文件大小限制。Spring Boot默认最大上传文件是1MB很多图片一传就报错。需要在application.yml里调spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB4.4 接口鉴权别做得太重毕设项目做登录鉴权我强烈建议用JWT而不是搞Session。理由有两个前后端分离场景下Session跨域比较麻烦JWT无状态接口测试也更方便。整体流程通用套路如下用户登录成功后后端生成一个JWT token把userId和username放进去设置过期时间比如7天。前端把token存在localStorage每个请求在Header里带上Authorization: Bearer token。后端写一个拦截器放行登录注册接口其余接口校验token有效则把userId放进请求上下文中。这里有一个点值得提醒JWT的密钥要放到配置文件里不要写死在代码里过期时间设置别太短否则用户逛着逛着就掉线演示的时候非常尴尬。4.5 用户行为数据采集前面已经说了browse_log这张表很重要现在说采集逻辑。用户在打开景点详情时会调用一个POST /api/scenic/{id}/view接口这个接口做两件事当天的浏览记录如果存在则count加1不存在则插入一条新记录count为1。把该景点ID放入一个最近浏览的队列里方便后续“猜你喜欢”排除已浏览项。为了避免每次请求都去数据库查一遍是否存在记录我在这个接口上做了一层本地缓存用一个MapuserId, SetscenicId缓存用户已浏览的景点定时任务每小时清理一次。这个做法属于轻量级的“半持久化”面试讲出来比单纯查库要有亮点。5. 常见问题排查与避坑技巧5.1 MyBatis查询结果为空但数据库有数据这种情况十有八九是字段映射对不上。MySQL的字段是下划线命名如open_timeJava实体是驼峰命名如openTime如果没有开启驼峰映射查出来就是null。解决办法mybatis: configuration: map-underscore-to-camel-case: true如果开了还是不行那大概率是resultMap里没对应上或者SQL里select的列名写错了。5.2 前后端联调一直报跨域错误前端在8081后端8080浏览器会拦截跨域请求。后端配置一个CORS过滤器是最快的Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }但注意如果你用了Spring Security或自定义拦截器拦截器对预检请求OPTIONS也可能拦截掉导致跨域配置失效。这时候要在拦截器里对OPTIONS请求直接放行并返回200这是很多新手卡住的地方。5.3 端口被占用启动直接报错如果IDEA启动Spring Boot时报端口被占用最简单的排查方法是lsof -i :8080 kill -9 PIDWindows的话用netstat -ano | findstr 8080 taskkill /PID PID /F但这只是临时解决。我还见过有人改端口改错位置写到application.properties里但文件根本没生效。Spring Boot的配置文件一定是application.properties或application.yml放在src/main/resources下确认一下命名和路径不要拼错。5.4 中文数据乱码数据库和连接串两边都要设置utf8mb4。连接串上加characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。还有一个细节如果你的数据库表建的时候没指定字符集即使连接串写了utf8插入中文还是乱码建表语句最好显式加一句DEFAULT CHARSETutf8mb4。5.5 Spring Boot自动装配原理简单捋一遍面试问卷也常问“Spring Boot自动装配原理”我帮你捋顺一句话版本Spring Boot启动时SpringBootApplication里的EnableAutoConfiguration会通过AutoConfigurationImportSelector读取所有spring.factories中配置的自动配置类然后根据项目依赖和ConditionalOnClass、ConditionalOnMissingBean等条件判断是否生效。对你项目最直观的影响就是你引入了spring-boot-starter-webSpringBoot会自动帮我们配置好DispatcherServlet、内嵌Tomcat、Jackson等引入了mybatis-plus-boot-starter就会自动配置SqlSessionFactory和Mapper扫描。如果这些自动配置出来的对象名字和你自己定义的冲突就要特别注意别重复定义Bean否则会启动报错。6. 部署联调与项目演示6.1 前端打包放进Spring Boot如果你最后想一个JAR包全搞定在前端项目根目录执行npm run build然后把生成的dist目录里所有文件拷贝到src/main/resources/static/目录下。注意Vue Router如果用history模式刷新页面时会找不到路由建议改成hash模式或者在Spring Boot里加一个Controller做前端路由转发。演示现场出这个问题非常难堪我吃过这个亏。6.2 参数配置管理这类项目的配置不建议写死一个application.yml而是分dev和prod两套。application-dev.yml application-prod.yml application.yml指定激活哪个这样你在本地调试用dev上线部署用prod不用每次改端口或数据库账号。启动参数可以直接用mvn spring-boot:run -Dspring-boot.run.profilesprod或者打成jar后java -jar xxx.jar --spring.profiles.activeprod项目用到的外部参数数据库链接、Redis地址、上传路径都应该放这里改配置不需要重新编译代码演示的时候调整环境那叫一个快。6.3 项目演示时的数据准备这一点有个很土的技巧但非常实用推荐算法要出效果测试数据必须丰富。你演示时随便录3个用户、5个景点推荐结果大概率很丑。我建议你提前准备一份SQL脚本里面预置10个用户、50个景点、每个景点关联2~4个标签再补上一些真实的收藏行为数据。这样演示时打开推荐页效果才像回事。数据来源的话景点信息可以自己编不用刻意追求真实但标签分配要合理。收藏数据要有侧重比如用户A重点关注自然风光就要让他的收藏行为里这类标签景点占比高推荐结果才准。提前把这份数据灌进去比现场瞎点有信心得多。6.4 说一嘴Spring Boot版本相关的坑前面提到版本太高的问题这里展开说。Spring Boot 3.x基于Jakarta EE很多旧教程里引入的包名从javax.*变成了jakarta.*。如果你参考老代码直接复制过来会是编译报错。另外Spring Boot 3.0要求JDK 17以上如果你的开发机还没升到17折腾环境的时间绝对超出预期。作为稳妥方案JDK 8 Spring Boot 2.7系 Maven 3.6以上这套组合在绝大多数旧机器和新机器上都能顺畅跑社区资料也是最多最完善的。别为了追求版本新而去踩没必要填的坑写这项目的首要目标是跑通、能讲、能演示。7. 推荐算法性能与扩展7.1 计算性能要提前想真实场景里推荐计算若要实时跑数据量大时根本不行。我们项目规模小可以每10分钟用定时任务把每个用户的推荐结果算好放进Redis缓存用户发起请求直接读缓存即可。如果不想引入Redis那就用HashMap过期时间做内存缓存代码量不大但效果立竿见影。定时任务的写法用Spring Boot自带的Scheduled就能解决开启定时任务只需在启动类或配置类上加EnableSchedulingComponent public class RecommendTask { Scheduled(cron 0 0/10 * * * ?) public void refreshRecommendCache() { // 1. 遍历活跃用户 // 2. 逐个调用Recommender.recommend() // 3. 写入缓存 } }这里值得说清楚的是cron表达式含义。0 0/10 * * * ?表示每小时的0分、10分、20分……运行一次。定时任务的并发可以默认单线程但如果用户量上来了建议在方法上加上Async交给线程池执行避免阻塞主线程。7.2 推荐结果总感觉不够聪明这个项目写完之后很多人的困惑是“为什么推荐的景点用户不感兴趣”。原因可能有两个一是基于内容推荐天然有“信息茧房”问题它只能推荐跟用户已有兴趣相似的景点没法帮用户发现新类型二是标签体系的粒度太粗导致所有自然风光的景点相似度都差不多没有区分度。想改善可以在策略上做个小改动推荐结果里留20%的位置给“探索类”景点即随机从用户未浏览的、热门度中等偏上的景点里抽取放在推荐列表末尾。这样既不伤害整体体验又能扩展用户的兴趣边界项目答辩时讲出来也是加分项。7.3 关于“Spring Boot整合Flink”热搜词里有个“springboot整合flink”我提一嘴只是提醒你别盲目上重型框架。旅游景点推荐管理系统这种数据量单机推荐计算就绰绰有余引入Flink这类流计算框架完全是杀鸡用牛刀还会把项目复杂度拉到另一个量级对你的毕设或线下项目是巨大负担。要把基础版本的推荐逻辑先跑通真对大数据方向感兴趣那是后续项目的事别在这个标题下面强行嫁接。8. 写在最后的实操经验这个项目给我最大的教训就是推荐系统的核心不在算法本身多高深而在于你如何把行为数据采集完整、如何设计标签体系、如何兜底冷启动。这三件事做扎实了哪怕你用的只是余弦相似度这种最基础的算法整个系统的实用性和说服力也远远强于“看着很乱但各种高深技术乱炖”的demo。如果你是一个人从零开始做我给的建议是先把景点管理和用户登录跑通不要在推荐算法上一口气写太多等基础功能稳定了再把推荐逻辑迭代式地加进去每加一步就自测一步。工程节奏比技术能力更决定项目成败。最后再分享一个小技巧强烈建议好好利用项目的README文件把技术选型、模块设计、数据库ER图、推荐算法说明写清楚。你答辩时评委大概率会直接翻你的README一份逻辑清晰的README比你代码注释写得再花哨都有用得多。