ARTICLE DETAIL

资讯详情

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

Spring Boot旅游推荐系统实战:协同过滤与高并发设计

Spring Boot旅游推荐系统实战:协同过滤与高并发设计 1. 项目概述1.1 旅游推荐系统到底在解决什么问题先别急着看技术选型咱们把场景想清楚。做毕设也好做个人项目练手也罢最容易犯的错就是一上来就堆功能列表。你仔细看这个标题基于 Spring Boot 的旅游推荐系统带在线沟通、协同过滤、日志记录、优惠券和积分。这一堆东西放在一起很多人第一反应是“功能真多工作量真大”但真正有经验的人第一反应是这几个模块之间到底是怎么串起来的旅游推荐系统的核心不是“展示景点列表”而是“让用户从海量目的地里快速找到自己感兴趣的东西”。所以整个系统的业务主链路可以拆成两条第一条是用户侧用户注册登录 → 浏览景点和线路 → 系统根据历史行为和偏好做协同过滤推荐 → 用户下单或收藏 → 系统给积分 → 下次再用积分换优惠券 → 优惠券又拉动新一轮浏览和消费。第二条是运营侧管理员维护景点、线路、优惠券、积分规则 → 系统通过日志记录用户行为 → 运营同学靠日志分析哪些景点热门、哪些推荐算法效果好 → 持续调优推荐策略。在线沟通模块则是给用户和客服或管理员之间架一条实时通道用户在旅途中遇到问题能直接问这本身也是提升系统粘性的一个点。所以你看这个项目听起来功能多其实每块都是围绕“推荐 → 转化 → 留存 → 复购”这条业务闭环来设计的。毕设答辩的时候考官最想听到的就是你能把这个闭环讲清楚而不是一个一个功能点背诵。1.2 这套系统适合谁来参考如果你正在准备 Java 方向的毕业设计或者想用 Spring Boot 做一个有完整业务闭环的练手项目这个题目是一个很标准的“中等复杂度”模板。它比单纯的增删改查系统难一档但又没到微服务、分布式那种程度一个人在一个周期内完全能做得完性价比很高。具体的参考价值在于你可以在一个项目里同时练到 Web 开发基础Spring Boot MyBatis MySQL、中间件集成Redis 缓存、WebSocket、消息队列、算法落地协同过滤推荐、AOP 切面编程日志记录、营销模块设计优惠券和积分这几项能力。这五个方向各自拎出来都能单独写一篇博客合在一起就是一个完整性很高、工作量也够看的毕设项目。另外如果你是想转行做 Java 开发、想给简历里加一个有说服力的项目经验这个题目也很合适。面试官问起项目你至少能说出推荐算法怎么落地、日志怎么埋点、缓存怎么用的这些都是实打实的业务场景面试里唠个二十分钟没问题。2. 整体设计与技术选型2.1 为什么主框架选 Spring Boot 而不是 SSH 或 SSM春招秋招和毕设圈子里Spring Boot 几乎已经是默认选项了这不是跟风是它真的适合这种规模的项目。Spring Boot 解决的最大痛点是“配置地狱”。以前写 SSMSpring Spring MVC MyBatis的时候光是一个 spring-mvc.xml、mybatis-config.xml、web.xml 就能让新手折腾两三天而且大部分配置跟业务没关系纯粹是基建。Spring Boot 用自动配置把这一层绝大部分都接管了你写一个 Controller、配一个 DataSource项目就能跑起来省下来的时间都应该投入到业务代码和推荐算法里。更重要的是Spring Boot 的生态位非常契合“单体应用”这个阶段。旅游推荐系统这个体量用户量撑死也就几千上万没必要上微服务。单体应用最怕的是代码写成一坨浆糊而 Spring Boot 的模块化特性——按功能拆包、按业务建 Controller/Service/Mapper 三层——天然逼着你把代码组织清楚。我见过很多同学答辩的时候被问“项目结构怎么设计的”支支吾吾答不上来这就是一开始没把包结构规划好。版本选择上我给一个稳妥的建议Spring Boot 2.7.x 就好别追新。原因很简单2.7.x 这一代在网上能找到海量教程、博客和踩坑记录MyBatis、Redis、WebSocket 这些配起来都有现成的参考答案。你选一个最新的 3.xSpring Security 的配置方式变了、javax 包名改成 jakarta 了网上教程一半都不用遇到问题光排查环境就能耗掉你一个周末。2.2 协同过滤选基于用户还是基于物品协同过滤是推荐系统里最经典的算法也是这个项目里最有技术含量、答辩时最容易被追问的部分。它本身分两类我先把这两类的适用场景说清楚。基于用户的协同过滤User-Based CF核心逻辑是找出和你兴趣相似的一群人把这些人喜欢过但你还没看过的景点推荐给你。它的优点是推荐结果比较“野”能帮你发现跟你不同圈子的人喜欢的东西适合刚注册、行为数据少的冷启动阶段。缺点是用户一多计算用户间相似度的成本会指数级上升而且用户兴趣漂移后结果会失真。基于物品的协同过滤Item-Based CF核心逻辑是找出和你历史喜欢过的景点相似的其他景点把相似的推荐给你。它不关心用户之间像不像只关心物品之间像不像所以算出来的结果更稳定、更容易解释——“因为你喜欢杭州西湖所以推荐乌镇西栅”。在线下业务里这种推荐更容易被用户接受毕竟你给用户展示推荐理由的时候说是“和你喜欢的景点相似”远比“和你相似的用户也喜欢”更有说服力。旅游推荐这个场景我强烈建议主推基于物品的协同过滤原因有这么几条旅游景点是典型的慢变化物品黄山、故宫、西湖十年不变物品相似度矩阵算一次就能缓存很久用户的行为数据天然稀疏大部分用户一辈子也就去十几个城市用户-物品矩阵稀疏度极高用户相似度算出来也不可靠旅游决策本身重内容轻社交用户关心的是景点本身好不好玩而不是别人怎么选。考虑到工作量你可以把协同过滤作为推荐主引擎再用基于用户喜好的冷启动规则兜底新用户没行为数据就按“热门景点 按城市分类的热门Top N”来推荐有行为数据后再切到协同过滤。这在答辩里是一个很完整的策略闭环。2.3 在线沟通怎么实现WebSocket 离线消息在线沟通模块里最容易被人做成“假在线聊天”——前端轮询接口后端存聊天记录。这种做法能跑但体验很差消息延迟按秒算而且服务器压力大。更专业一点的做法是引入 WebSocket。WebSocket 的好处是建立一次连接后服务器可以主动往客户端推消息延迟在毫秒级。它的实现思路是这样的用户打开聊天页面时浏览器发起 WebSocket 握手经过一个专门的 WebSocket 拦截器HandshakeInterceptor验证用户 token连接建立后前端发消息走 STOMP 协议你可以把它理解为 WebSocket 之上的一层消息路由规则后端通过 MessageMapping 注解接收消息并做持久化客服或管理员在线的话消息直接推给对应 WebSocket session不在线的话消息进 Redis 或数据库存离线消息等对方下次上线再拉取。这里面有一个很关键的细节WebSocket 连接状态是会话级的你要在拦截器通道里绑定 userId 和 sessionId 的映射关系存到 ConcurrentHashMap 或 Redis 里。后期如果系统并发上来了可以用 Redis 做 session 注册中心这样就算部署多节点消息也能正确路由到用户实际连接的那台服务器上。虽然毕设没必要上分布式 WebSocket 方案但你在设计文档里提一句“我留了扩展空间”答辩时印象分会好很多。2.4 日志记录推荐用 AOP 切面日志记录最常见的做法是在 Controller 或 Service 的方法里直接写 log.info但真正业务复杂的系统这种埋点方式会把业务代码搞得很脏后期想统一加字段也麻烦。更高级的做法是用 AOP 切面统一记录操作日志。Spring AOP 在这里的具体玩法是定义一个自定义注解 OperationLog注解上可以带上模块名、操作类型比如“查询景点”“领取优惠券”“下单”再写一个切面类用 Around 或 AfterReturning 拦截所有标注了 OperationLog 的方法被拦截的方法执行成功后通过反射读取注解信息再从当前登录用户上下文Session 或 ThreadLocal 存的 UserInfo里拿到 userId、用户名称最后把方法名、入参、出参、耗时、IP 地址、操作时间统一组装成一条日志异步写入数据库或日志文件。这么做的最大优势是业务代码零侵入。将来你想给“点赞”功能也加日志只需要在方法上标一个 OperationLog 注解根本不用动业务逻辑。而且你可以把日志记录做成异步的——方法执行完就立即返回日志通过线程池或 MQ 异步落库避免日志 IO 拖慢主接口。很多人在毕设里忽略日志记录觉得不就是打印几行信息嘛。但只要你把“AOP 自定义注解”这套设计拿出来技术含量立刻就不一样了。而且你想想落脚点这个日志可不是只写给程序员看的它是给运营做用户行为分析的比如“哪些景点被浏览最多”“优惠券领取和核销的比例是多少”所以日志字段设计要兼顾运营需求而不只是开发调试。2.5 优惠券和积分用 Redis 抗并发用定时任务管过期优惠券和积分模块看上去是纯 CRUD实际上坑很多。优惠券的经典场景是“秒杀”或“限量领取”这类功能最怕的是超发。假设一张券库存是 100用户同时有 500 个请求涌进来如果用数据库自增减库存MySQL 的锁压力和“超卖”问题会让你欲哭无泪。解决办法是引入 Redis。领券流程这样设计先把优惠券库存预加载到 Redis 的 String 或 Hash 结构里用户领券时用 Redis 的 DECR 命令原子减库存减成功结果 0再异步落一条数据库记录减失败说明库存不足直接返回“已抢光”。DECR 是单线程的原子操作不存在并发扣减问题流量再大也只是 Redis 的压力比直接打 MySQL 强得多。积分模块稍微简单一点但也要注意规则设计。不要只是简单的手动加积分最好设计一个完善的积分流水表记录每次积分增减的订单号、渠道、类型消费、奖励、退款退积分、积分数值。为什么要有流水因为用户会在“我的积分”页面看到积分变化明细而且客服处理用户争议时也要有据可查。积分过期策略可以用 Spring 的 Scheduled 定时任务每天跑一次把过期积分清零并记录流水或者更简单点查询时实时判断积分是否在有效期内逻辑上更可靠。3. 数据库设计与核心功能实现3.1 核心表结构设计数据库是这个项目的骨架表设计做不好后面写代码全是补丁。我的建议是至少拆出这几张核心表用户表useruserId、用户名、密码BCrypt 加密存储、昵称、头像、注册时间、积分余额景点表scenicscenicId、名称、城市、简介、热度值、评分、图片地址、经纬度可扩展景点评分表ratingratingId、userId、scenicId、评分 1~5、评论时间 —— 这是协同过滤算法的原料表用户行为表behaviorbehaviorId、userId、scenicId、行为类型浏览/收藏/下单、行为时间 —— 日志记录和行为分析的落点优惠券表couponcouponId、名称、类型满减/折扣、面额、满减门槛、库存、有效期用户优惠券表user_couponid、userId、couponId、领取时间、使用时间、状态未使用/已使用/已过期积分流水表points_recordrecordId、userId、变化分值、余额、变动类型、关联订单号、创建时间消息表messagemessageId、会话 ID、发送方、接收方、内容、发送时间、是否已读这些表之间要画清楚外键关系尤其是在你的设计文档和答辩 PPT 里一张清晰的 ER 图比长篇大论好得多。3.2 协同过滤算法的落地实现协同过滤算法的代码实现分三步走构建用户-物品评分矩阵、计算物品间相似度、生成推荐列表。第一步从 rating 表里拉出所有用户评分记录组装成两个 map一个以 userId 为 key存该用户对所有景点的评分一个以 scenicId 为 key存所有给该景点评过分的用户这步是为了后续计算相似度时只看对同一景点评过分的人。第二步计算物品间相似度。最常用的指标是余弦相似度sim(A, B) A·B / (|A| × |B|)把景点 A 和景点 B 都表示成“用户在 A 和 B 上的评分向量”比如景A在所有评分过的用户里的评分数组是 [5, 4, 0, 3]景点B对应的是 [4, 5, 3, 2]0 表示该用户没给这个景点评过分。两个向量做点积除以模长积得到的结果就是相似度数值越接近 1 越相似。实际项目里你还会加一点小技巧比如剔除热门景点——如果一个景点被几百万人评过 5 分它跟谁都相似这个相似度没有区分度需要降权处理。第三步生成推荐列表。用户点击或浏览某个景点后系统找到与该景点相似度最高的 Top N 景点再从中去掉用户已经去过的、已经浏览过的剩下的就是推荐结果。最后按相似度降序排列必要时加一点热度加权。这里有一个可扩展的点协同过滤计算一次后结果矩阵很大建议用定时任务比如每天凌晨预计算好相似度存到 Redis 里用户请求时只做查询不在接口里现算。这个优化你写在文档里又是一个加分项。3.3 推荐引擎的接口设计推荐模块建议单独拆一个 Controller。我的接口设计大致是这样的GET /api/recommend/hot热门景点推荐冷启动用取整体评分均值最高的 Top 20GET /api/recommend/similar/{scenicId}相似景点推荐用物品协同过滤找 Top 10GET /api/recommend/personal个性化推荐综合历史行为 协同过滤按用户维度聚合POST /api/recommend/rate用户给景点评分写 rating 表 触发积分奖励每个接口返回的数据结构要统一比如统一用 Result 对象包一层 code、message、data。你写推荐接口的时候别忘了加缓存热门推荐可以缓存 10 分钟相似景点缓存 1 小时评分数据实时更新后主动失效缓存。这一套做完你的“性能优化”和“缓存设计”环节就能在答辩时给评委讲出东西来。3.4 AOP 日志切面的具体实现Spring AOP 的切面代码不长但有几个细节必须注意。先说切面表达式推荐用自定义注解方式而不是直接切某个包路径。因为切包路径很容易误伤比如切了 service 包结果把内部的调度逻辑也记录进去了日志里全是噪音。自定义注解的好处是想给谁记日志就给谁标注解节奏自己控制。切面代码大致这个形状Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable { long start System.currentTimeMillis(); Object result point.proceed(); long cost System.currentTimeMillis() - start; // 组装日志对象操作人、模块、方法名、入参、耗时、IP、时间 // 异步写入日志表或文件 return result; } }注意两点第一记录操作人的 userId 一定要从登录上下文里拿最常用的做法是自定义一个过滤器Filter拦截请求把已登录用户信息放到 ThreadLocal 里切面里直接取。第二入参和出参序列化成 JSON 时要控制长度有些把整个景点列表打成 JSON 写进日志的场景日志文件会膨胀得很快只记关键字段或者对超长内容截断就好。3.5 Spring Boot 集成 MyBatis 与分页MyBatis 是这类系统的主力 ORM集成其实没什么好说的核心是写 mapper 和 XML 文件时注意动态 SQL。比如景点列表筛选条件可能是城市、评分、价格区间用 标签组装 where 条件比写死 SQL 灵活得多。分页方面推荐 MyBatis 的分页插件 PageHelper使用上简单到令人发指查询前写一行 PageHelper.startPage(pageNum, pageSize)后面的查询自动带上 limit。但要记住一个坑PageHelper 的分页原理是解析当前线程上下文最近的查询如果你在同一个方法里连续执行两条 SQL分页只对第一条生效。所以一定要把 startPage 紧贴着你要分页的那条查询写别中间插别的操作。再提一嘴 MyBatis 的二级缓存。很多人图省事开启二级缓存结果出现脏读——因为你修改了数据但缓存没来得及刷新。我的建议是毕设阶段直接用 Redis 做业务缓存MyBatis 的缓存关掉避免给自己挖坑。4. 实操过程与关键环节实现4.1 开发环境准备工欲善其事必先利其器环境清单如下JDK 8 或 11不要盲目上 17除非 Spring Boot 版本确认兼容Maven 3.6依赖管理和构建MySQL 5.7 或 8.0建议 8.0可用 navicat 或 DBeaver 可视化Redis 6.x本地直接装一个 Windows 版或 Linux 版都行IDEA 2022社区版就够用旗舰版更好如果你遇到 IDEA 里配置 Spring Boot 启动参数的问题有个经验先分享给你IDEA 启动服务时最常改的就是端口号不要直接去代码里写死 server.port而是用 IDEA 的 Edit Configurations 里的 Program arguments 加 --server.port8081或者改 application.yml 里的配置。这样方便多实例同时启动测试。4.2 项目骨架搭建步骤我建议按下面的顺序一步步来每完成一步就启动一遍项目确认能跑不要一口气写完全部代码再调试否则出了问题定位成本会高到你想哭。用 Spring Initializr 生成基础项目选 Web、MyBatis、MySQL Driver、Redis、Lombok 等相关依赖。配置 application.yml数据源、Redis 连接、MyBatis 的 mapper 路径、日志级别。日志级别记得先把 mapper 包调成 DEBUG这样你能在控制台看到 SQL调试事半功倍。建数据库和表结构先用建表 SQL 把表建好再反向生成实体类或者手写实体类。写用户注册/登录模块。注册时密码一定要 BCrypt 加密不要存明文。登录成功后签发 JWT token 或者生成 Session 标识后续所有接口都要校验登录态。写景点 CRUD 和管理员后台。景点列表、详情、搜索页面先把主流程跑通。加 Redis 缓存景点列表和热点推荐数据缓存。写协同过滤推荐引擎接在景点详情接口后面。写 AOP 日志切面和日志表。写优惠券、积分模块接上 Redis 库存扣减和定时任务。写 WebSocket 在线沟通模块。最后统一做前端页面或者用基于 Spring Boot Vue 的前后端分离方案。我强烈建议毕设采用前后端分离后端只写 API前端用 Vue Element UI 或者更轻量的 Vue Vant移动端风格。因为旅游推荐系统天然适合手机端展示用 Vant 做出来的页面在演示时会更讨喜。前端工程打包后可以扔进后端 resources/static 目录也可以单独起一个 Nginx 配置转发具体看你的答辩环境。4.3 优惠券并发领取的实现细节这是整个项目里最考验工程能力的模块我把细节展开来讲。首先在 coupon 表里维护总库存和剩余库存。用户的领券请求进来后先走 Redis 拦截String stockKey coupon:stock: couponId; Long remain redisTemplate.opsForValue().decrement(stockKey); if (remain null || remain 0) { // 回滚防止扣成负数 redisTemplate.opsForValue().increment(stockKey); return 手慢了优惠券已抢光; } // 扣减成功后异步写入 user_coupon 表这里有个经验Redis 扣减之后数据库插入失败或者用户重复领取的问题。解决方法是先做用户去重判断——用 Redis 的 SETNX存在就不重复插入记录用户领券的唯一标识或者查一下 user_coupon 表防止重复领取。生产级的做法还会配合分布式锁比如 Redis 的 SETNX 锁。毕设阶段你只要把库存控制和防重复领取做对就已经很能打了。优惠券有效期建议两张表配合coupon 表存全局有效期比如 7 天user_coupon 表在领取时也精细地写入一个过期时间。用户查询自己可用的优惠券时只查未使用且未过期的记录这样可以在页面上直接过滤掉失效的券体验更好。4.4 积分的增减规则和事务处理积分模块最容易出问题的是数据一致性。比如用户下单成功后如果既要在订单表写订单又要扣商品库存又要给用户加积分这三个操作必须放在一个事务里。任何一个失败全都要回滚否则用户会发现订单没创建成功但积分却加了。用 Spring 的 Transactional 就能解决但我提醒你一句事务不是万能的用的时候一定要保证方法不能被 this 调用否则事务不生效。如果你在同一个类里调用自己的带事务方法因为代理机制事务不会生效——这个坑特别隐蔽测试的时候很难发现做积分加操作前建议先自查一遍调用链路。积分规则可以做成可配置的注册送 100 分、首次下单送 50 分、每日签到送 5 分、景点评分送 10 分。把规则放到数据库里管理员后台可改比写死在代码里好得多。4.5 在线沟通模块的 WebSocket 联调WebSocket 联调比普通接口麻烦这里分享一个调试技巧。浏览器端你可以直接打开控制台用 JavaScript 原生 WebSocket API 连接 ws://localhost:8080/ws不需要前端框架就能验证后端消息通了没有。后端用 Spring 的 WebSocket 配置类注册一个 Endpoint在前端连接时校验 JWT tokenpublic class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { // 从 query 或 header 里取 token 校验绑定 userId 到 attributes return true; } }联调时常见的坑有三个第一个WebSocket 握手不走业务层的拦截器它用的是独立的 WebSocket 配置类你需要在 WebSocketConfigurer 的 registerWebSocketHandlers 里显式添加拦截器第二个如果你前端测试时用了 http 协议跨域问题会阻碍握手需要给 WebSocket 端点添加 .setAllowedOrigins第三个消息体是二进制还是文本必须约定清楚统一 JSON 字符串收发最方便。4.6 日志记录落库还是落文件日志模块落地有两种方案各有利弊。方案一只写文件。用 logback 的滚动策略按天切割日志优点是性能好、省数据库空间缺点是运营想看数据还得翻服务器文件不方便。方案二日志落数据库。优点是可以做管理后台查询页面按用户、按时间、按模块筛选直接展示给运营看缺点是数据库压力大需要有清理策略。我建议折中日常调试日志走文件业务操作日志走数据库。业务操作日志的频率通常不高比如用户领券、下单、评分、登录这些关键节点才记数据库完全扛得住。这样既能给运营做分析报表也不会因为日志把 MySQL 搞垮。另外记得给日志表加一个定期清理任务可以用定时任务每周删除 30 天前的操作日志。你在日志记录表的字段上设计好索引userId、创建时间查询会快很多。4.7 前后端分离部署方案毕设演示时最怕的是现场部署翻车所以我把部署方案也给你讲透。前后端分离项目如果你用 Vue 打包最简单的部署是把 dist 文件夹的内容复制到 Spring Boot 的 resources/static 目录下重新打 jar 包。这样浏览器访问同一个端口后端 API 路径配好 /api 前缀就没有跨域问题了。不过每次前端改完都要重新打包 jar开发期很不方便建议开发期单独跑 vue dev server配置 proxy 把请求转发到后端 8080部署时才用静态合并方案。如果你想把项目部署到服务器上给人演示用宝塔面板里的 Docker 部署 Spring Boot 项目和 Redis、MySQL是一条很成熟的路线。先写好 Dockerfile把 jar 包 COPY 进去再配合 docker-compose.yml 把 MySQL、Redis、服务本身一键拉起。Docker 的好处是答辩现场不需要现场配环境docker-compose up -d 一下全都起来了。有个小坑提一句如果你的服务器内存只有 1G同时跑 MySQL、Redis、Java 应用可能会内存吃紧。建议给 JVM 限制内存启动命令里加 -Xmx256m -Xms256mMySQL 也可以把缓冲池调小够演示就行。5. 常见问题与排查技巧5.1 Spring Boot 版本过高引发的配置不兼容这个问题我在帮别人调项目时遇到太多次了。Spring Boot 3.x 把 javax 包全部换成 jakarta很多老教程里的 import javax.servlet.http.HttpServletRequest 全都编译不过。数据库驱动、MyBatis 版本也要跟着升如果你只是照着 2.x 的博客配 3.x 项目几乎必踩依赖坑。我的建议是毕设求稳不求新统一用 Spring Boot 2.7.x。如果非要用 3.x那 MyBatis 的 starter 要用 mybatis-spring-boot-starter 3.0 或 mybatis-plus 3.5.3导入时看清 groupId 和版本号。5.2 PageHelper 分页失效分页失效的具体表现是查询结果直接返回所有数据limit 没有生效。多数情况下是因为 PageHelper.startPage 和 Mapper 方法之间被其他线程或逻辑干扰了。PageHelper 是用 ThreadLocal 实现的如果你开启了异步线程或者方法里有并发调用分页参数会丢失。解决办法是按“查询前一行、查询紧邻、查询后立即清理”的原则使用。更稳妥的方案是手动写 limit 参数在 DAO 层通过 Param 传 pageNum 和 pageSize自己拼接 LIMIT 语句绕开插件机制。虽然要多写两行代码但对一个毕设来说反而更好排查。5.3 Redis 缓存穿透和缓存雪崩旅游推荐系统里景点详情和推荐列表都是高并发读取的热点数据必须做缓存但缓存有个典型问题。如果某个景点的数据在缓存里过期了同时大量用户请求这个景点的详情这些请求会直接打到数据库这就是缓存击穿。更惨的情况是缓存大面积同时过期导致 MySQL 瞬间被请求淹没叫缓存雪崩。预防方案景点详情缓存可以设置不同的过期时间比如 300 秒到 600 秒之间随机避免同一时间集体失效推荐热门列表用定时任务预先加载不依赖用户请求触发。还可以用“缓存空值”方案如果查询结果为空也把它缓存 60 秒防止恶意请求疯狂刷空数据。5.4 前端调用后端接口的跨域问题前端页面如果单独起服务比如 Vue dev server 跑在 5173 端口后端接口在 8080就会遇到跨域。解决方案有两种一种是在后端写一个 CorsFilter 或使用 CrossOrigin 注解允许指定域名访问另一种更推荐前端用代理在 vite.config.js 里配 proxy 把 /api 请求转发到 8080这样浏览器角度没有跨域后端也不用改。这两种方案我建议你在开发期用前端代理在演示部署时把静态资源合并到后端。两个方案结合使用跨域问题基本不会出现在你身上。5.5 日志记录里取不到用户信息这是 AOP 日志模块最常见的报错。原因是日志切面执行的时候用户登录信息还没进 ThreadLocal或者接口本身就是匿名接口比如登录接口、注册接口。你的切面里要加上判断如果当前线程上下文里没有用户对象就把 userId 记成 -1 或“anonymous”不要直接抛空指针。这个处理逻辑虽然简单但能避免整个系统因为日志模块而挂掉。5.6 在线沟通消息丢消息WebSocket 消息的可靠性一直是个大坑。如果用户断网或者网页不小心刷新未发送成功的消息会直接丢失。我的常用补救方案是在前端加一个消息重发机制发送消息时先在前端本地存一份 pending 状态等后端确认 ack 后再标记为已发送如果没有收到 ack就在网络恢复后自动重发同时生成一个消息唯一 ID 做去重。这个细节虽然毕设里不写也说得过去但写出来就能体现你对“消息可靠性”有认知。6. 项目扩展与答辩亮点建议6.1 秒杀优惠券的并发压测演示毕设答辩时如果你只是掏出 IDEA 启动项目演示一番评委最多觉得“功能齐全”。想拿高分最好准备一个压测演示环节。你可以用 JMeter 对领券接口做一次 200 并发压测对比加 Redis 前后的库存数据和响应时间。具体操作是先不加 Redis用数据库同步扣减开 200 个线程并发领券观察出现超卖多少单然后加上 Redis DECR 方案再压一次结论是库存精确等于设置值平均响应时间大幅下降。你把这个过程截图放进论文的“系统测试”章节比任何文字描述都有说服力。6.2 推荐效果评估维度答辩的时候老师经常会问”你的推荐算法准不准“这个问题如果你答不上来会非常减分。提前准备几个评估维度召回率、准确率、覆盖率、多样性。用测试集数据把这几个指标在论文里列出来。不用做得特别严谨但至少要说清楚你是用什么离线数据、按照什么方式切分训练集和测试集的。推荐效果没有真实用户反馈的话你可以自己做一个小规模模拟准备 20 个用户的评分数据按 80% / 20% 切分历史评分用 80% 训练、20% 验证。把推荐 Top 10 里命中验证集合的比率作为准确率参考。这个加热搜索词里有“协同过滤”说明评委大概率会往算法细节上深挖提前把这些准备好你就赢了一半。6.3 在线沟通模块的聊天记录导出在线沟通模块如果只是实时聊天其实不够完整。再加一个“聊天记录导出”功能会让系统更落地。管理员可以把某位用户的所有沟通记录导出成 Excel 或 PDF用于客服复盘订单问题。实现代码不复杂查询 message 表按会话聚合用 EasyExcel 或 POI 生成文件。这个小小的功能加在系统里会让评委觉得你的“在线沟通”不是摆设而是真正考虑到了运营场景。6.4 日志分析和用户画像的延伸其实你已经有用户行为日志了这些数据可不只是给开发看的。可以设计一个简单的“用户画像”页面根据浏览和收藏行为给用户打标签比如“喜欢历史古迹”“偏爱自然风光”“高消费潜力”。标签来源来自日志记录里累积的用户行为次数统计逻辑用 SQL 分组聚合就能做。这一步的亮点在于它体现了你理解了“日志记录”和“推荐系统”之间是相辅相成的。日志记录不只是辅助排错而是推荐数据闭环的核心环节。你答辩时可以说“我的系统通过 AOP 统一记录用户行为这些行为数据回流到协同过滤的评分矩阵里供推荐引擎持续优化。”这就把整个系统的故事讲成了一个完整的闭环比零散功能罗列强太多。7. 性能优化与安全加固7.1 数据库索引与 SQL 优化说实话毕设功能的数据库表体量很小你只要保证 SQL 走索引性能就不会差。重点检查这几个查询商品/景点列表查询city、rating 字段加索引、用户行为记录查询userId 行为时间联合索引、优惠券查询userId 状态联合索引。写 SQL 时警惕隐式类型转换。比如表里 id 是 bigint但你查的地方传了字符串MySQL 可能把所有行都做一遍类型转换索引就失效了。这个坑在 type 字段上尤其常见因为前端传过来的参数默认都是字符串。7.2 Redis 缓存策略优化我的推荐系统的缓存分为三个层级热门榜单缓存、协同过滤相似度矩阵缓存、用户个性化推荐结果缓存。这三个缓存各有不同的更新策略。热门榜单可以每天凌晨定时任务刷新或者加一个布隆过滤器防止无效请求穿透到数据库协同过滤相似度矩阵以“物品”维度存横向扩散性不大可以缓存很长时间个性化推荐结果直接以 userId 为 key 缓存 10 分钟等用户有新行为时再主动失效。Redis 的 key 设计也要规范推荐统一用“业务前缀:模块:ID”的格式比如 “recommend:similar:101”“coupon:stock:888”这样既不容易冲突排查问题也能一眼看清。7.3 密码安全与接口防刷用户密码加密用 BCrypt 是底线不要用 MD5。MD5 撞库速度为每秒数十亿次BCrypt 加盐之后单次计算要几十毫秒安全性高了几个数量级。注册和登录接口记得加防刷可以用 Redis 计数器限制单个 IP 每分钟的请求次数也可以用简单的验证码机制。优惠券领取接口防刷更重要一个用户一天只能领一张券的限制如果你不做用脚本刷券就会把库存刷穿。7.4 接口幂等性处理“幂等”听起来高端实现很简单。比如用户下单接口网络波动导致用户点了两次“下单”后端可能会生成两条下单记录。解决办法是前端在点击后立即禁用按钮同时后端在接收请求时检查订单号是否已存在或使用 Redis SETNX 做去重。优惠券领取同样要做到“同一用户 同一券”的幂等你在 user_coupon 表上加唯一索引userId, couponId这一层兜底能在极端并发下挡住穿透。8. 写在最后的一些经验这个项目是我认为很适合作为 Spring Boot 练手和毕设的题目因为它每一块功能拿出来都不算难组在一起却能构成一个完整的业务系统。很多人完成度差就差在只实现了功能没有把模块之间的逻辑闭环讲清楚。根据我给朋友改项目的经验最值得花时间打磨的三个地方是第一个是协同过滤算法因为这是标题里最有分量的技术点你至少要理解相似度公式的推导过程和算法伪代码而不是只贴一段代码第二个是日志记录你要能解释清楚 AOP 切面为什么能不加侵入地记录日志以及它和传统日志的区别第三个就是系统的演示脚本把主流程走通从注册、登录、浏览景点、查看推荐、领券下单、获得积分到在线咨询最好一气呵成演完中间不要卡壳。我个人做项目踩过的最大坑是太早介入前端细节导致后端接口反复变动。强烈建议先把所有后端接口文档写好、用 Postman 测通再让前端对接。前后端先约定好返回结构和状态码后面联调阶段的痛苦能少一半。如果时间允许再把这个项目扩展成一个完整的“旅行 UGC 社区”加入用户发帖、游记分享、评论回复功能。你会发现推荐系统的数据来源会瞬间丰富起来——用户的收藏、点赞、评论都可以成为协同过滤的输入。这也是一条很自然的扩展路径以后你想拿这个项目继续迭代、写进简历个人项目描述能讲的内容会越来越多。最后送大家一句话做毕业设计本质不是证明你“会用这个技术”而是证明你“理解这个技术为什么会被用在合适的场景里”。所以重点不只在于代码写得快而是要反复打磨那个“为什么”。祝你开发顺利答辩拿高分。
返回列表