尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot的美食推荐系统实战:协同过滤算法与部署解析

基于SpringBoot的美食推荐系统实战:协同过滤算法与部署解析 每年到这个时间段我的私信里总是涌入同一类问题SpringBoot学完了但没项目练手怎么办课程设计选什么题能不撞车又拿高分面试时项目经历讲不出亮点怎么办今天就把我打磨过很多遍的一个实战项目——基于SpringBoot的美食推荐系统的完整脉络摊开讲清楚涵盖源码结构、部署文档、代码讲解三个维度的全部细节。这个项目最打动我的地方在于它几乎覆盖了一个真实业务系统的主要模块——用户管理、美食浏览、评分收藏、个性化推荐、后台管理而且技术栈非常主流不会过时也不会太冷门。不管你是准备课程答辩的在校生、刚入行的Java工程师还是想补一个完整项目经历的求职者这套项目都值得你从头到尾跟一遍。很多人拿到项目第一反应是去翻代码、跑Demo其实这是效率最低的路径。这个项目的价值不在代码量多少而在于它把“从零到部署”的完整链路浓缩进了一个看得见摸得着的业务场景里。我尽量不写那些教科书里能查到的东西而是把我在反复搭建、改版、带学生复现过程中总结出来的真实经验一条一条拆给你们。1. 这个项目为什么值得做课程设计、毕设还是求职项目都合适先说选题逻辑。每年课程设计和毕业设计的选题名单里电商系统、图书管理系统、博客系统基本被选烂了答辩老师看一眼题目就失去兴趣。美食推荐系统这个名字天然带一点“智能”属性但实现难度又没有高到离谱属于典型的性价比极高的选题方向。再从能力覆盖的角度看这个项目实际上揉合了三层能力第一层是SpringBoot的基础CRUD用户注册登录、美食列表、分类浏览这是最基础的框架运用能力第二层是稍微进阶的业务逻辑比如评分、收藏、推荐列表的生成这里涉及表关联、数据聚合、相似度计算第三层是工程化能力包括数据库设计、项目打包、环境部署、异常处理。三层的难度梯度摆在这里你可以根据自己当前的水平决定做深还是做浅但无论深浅这个项目都能清晰地反映在你最终的简历或答辩PPT上。还要提一个隐藏价值——面试里的项目讨论环节。市面上大部分简历项目都停留在“增删改查”层面面试官问两句就露馅了。而这个项目最容易被追问的点就是“推荐列表是怎么算出来的”你需要讲清楚相似度计算的思路、数据从哪张表来、计算是实时还是离线、数据量大了怎么办。光这一块就能自然展开五到十分钟的深度交流等于把一个普通项目直接拔高成了“有算法痕迹”的项目。对于应届生和转行者来说这种项目是最容易让面试官留下印象的。2. 技术选型和版本决策为什么我推荐这套组合技术选型这件事我在不同阶段反复改过很多版。最开始用的是最简单的SpringBoot MyBatis Thymeleaf服务端渲染后来引入Vue做了前后端分离再往后加入Redis做缓存。最终稳定下来的组合是这套组件推荐版本选型理由JDK1.8 / 11稳定、生态兼容性最好1.8尤其适合学习和课程设计SpringBoot2.3.x ~ 2.7.x不要追最新大版本2.x文档多、坑少和JDK1.8兼容MyBatis-Plus3.4.x内置分页插件、代码生成器省掉大量手写SQLMySQL5.7 / 8.05.7最稳8.0也完全没问题注意驱动差异Redis5.x可选做缓存和热门榜单非必需但加了这个是加分项Maven3.6依赖管理配置阿里云镜像前端Vue 2 ElementUI 或 Thymeleaf二选一取决于你想练前端还是想专注后端2.1 SpringBoot版本太新反而容易踩坑这个词条在很多搜索记录里都出现过——“springboot版本太高”。实话实说用最新版SpringBoot 3.x的时候第一道坎就是JDK版本要求变成了17很多人的电脑上装的还是JDK 1.8直接编译不过。再往下走javax.servlet包名变成了jakarta.servlet一堆老教程里的import全部报红。我见过太多人卡在这里项目还没跑起来就已经想放弃了。所以我的建议非常直接老老实实用SpringBoot 2.7.x配JDK 1.8。这套组合经过了最大规模用户验证你在网上遇到的所有报错基本都能搜到解决方案。学习项目也好、课程设计也好核心目标是跑通和理解业务逻辑不是在版本适配问题上消耗耐心。2.2 MyBatis-Plus带来的开发效率提升很多人在Mapper层写大量XML对于一张表的CRUD来说这完全是重复劳动。MyBatis-Plus把单表CRUD封装好了你只需要继承BaseMapperT接口大部分数据库操作直接调用现成方法。最关键的是它的分页插件配置一个MybatisPlusInterceptor分页查询一行代码搞定比手写LIMIT ? , ?要舒服得多而且面试被问到MyBatis分页插件用法时正好能答上。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }2.3 前端选Vue还是Thymeleaf主要看你缺什么这个项目我两个版本都做过。如果目标是课程设计时间紧任务重用Thymeleaf直接写页面套模板一个单体应用从头写到尾两天就能跑通。如果目标是求职面试建议用前后端分离SpringBoot只写REST接口前端用Vue ElementUI部署的时候一个jar包加一个静态文件目录。这个架构在面试时更有得聊也更能体现你对现代Web开发的理解。3. 数据库设计五张核心表撑起整个业务数据库设计是这个项目最不能跳过的一步。我第一版做的时候图省事把评分和收藏都塞在用户表里用逗号分隔结果后面写推荐算法的时候整个人都崩溃了只能推倒重来。数据库设计不只是存数据它等于在帮你把业务逻辑提前梳理一遍。我在这里抽出五个最核心的表每一张都对应一个功能和一段代码逻辑非常清晰表名核心字段作用userid, username, password, nickname, avatar用户注册与登录categoryid, name, sort美食分类川菜、粤菜、甜品等foodid, name, category_id, description, price, image, rating_avg美食信息主体ratingid, user_id, food_id, score, create_time用户评分记录推荐算法核心数据源favoriteid, user_id, food_id, create_time收藏关系同样用于偏好计算3.1 用户评分表和收藏表为什么要分开建如果你把这两个合并成一张表用字段区分类型看起来省了一张表但后续的SQL查询会变得非常别扭。评分是一个连续数值收藏是一个布尔行为它们在推荐算法里代表的信息量完全不同。评分能直接告诉你用户对某个菜品的态度好坏收藏只能说明用户有点兴趣两者的权重天然不同分开建表方便各自的聚合统计互不干扰。3.2 美食表的冗余字段设计food表里我特意保留了一个rating_avg字段用来存这道菜的平均分。严格讲平均分可以从rating表里实时算出来但每次用户浏览列表时都去做一次实时聚合数据库压力会很大。这个冗余字段的更新时机放在用户提交评分之后由Service层同步更新代码上多写三行性能上节省了大量计算。这个设计思路在面试中是可以拿出来讲的它体现的是“用空间换时间”的工程思维。数据库初始化脚本一定要一次性整理清楚包括建库语句、建表语句、测试数据。项目里默认会放一份sql/init.sql落库之后直接有一批菜品数据可看不用自己手工慢慢录。4. 推荐算法的落地从协同过滤原理到Java代码实现推荐功能是整个项目的灵魂也是最容易被追问的部分。我采用的是基于物品的协同过滤Item-Based Collaborative Filtering思路一句话就能讲明白如果两个菜品被同一批用户喜欢那么它们之间的相似度就高当你喜欢了其中一个系统就把另一个推荐给你。4.1 为什么选物品协同过滤而不是用户协同过滤基于用户的协同过滤需要先找到和你口味相似的人群再把这些人喜欢的菜推荐给你。这套逻辑在用户量很大时才比较准确而课程设计场景下用户数据通常只有几十条用户相似度算出来非常稀疏效果很差。基于物品的协同过滤有一个天然优势美食的数量是有限的菜品之间的相似关系相对稳定即使数据量不大也能算出有意义的结果。所以在小型系统里物品协同过滤是明显更合理的选择。4.2 相似度计算用余弦相似度假设菜品A有三位用户的评分为 [5, 4, 0]0表示未评分菜品B对应的评分是 [3, 0, 4]余弦相似度的公式是similarity (A·B) / (|A| × |B|)拆开来看就是A·B 5×3 4×0 0×4 15 |A| sqrt(5² 4² 0²) ≈ 6.40 |B| sqrt(3² 0² 4²) 5.00 similarity ≈ 15 / (6.40 × 5.00) ≈ 0.47相似度范围在0到1之间越接近1表示两个菜品越相似。算法里只需要用纯Java代码就能完成我一开始也是用SQL硬算后来发现数据一变就要重写SQL结构非常痛苦。改成Java代码后逻辑一目了然调试也方便很多。public double cosineSimilarity(MapLong, Double ratingsForA, MapLong, Double ratingsForB) { double dotProduct 0; double normA 0; double normB 0; for (Double score : ratingsForA.values()) { normA score * score; } for (Double score : ratingsForB.values()) { normB score * score; } for (Map.EntryLong, Double entry : ratingsForA.entrySet()) { Double scoreB ratingsForB.get(entry.getKey()); if (scoreB ! null) { dotProduct entry.getValue() * scoreB; } } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }算完所有菜品之间的相似度后生成推荐列表的逻辑分三步取出你评分最高的几道菜找到与它们最相似的其他菜品排除你已经操作过的菜品按相似度总和排序取前N个。这个算法的时间复杂度在数据量小的时候完全不是问题等数据量大了再考虑换成离线预计算但在课程设计和简历项目这个层面实时计算已经足够展示能力了。4.3 冷启动问题的处理方案新用户没有任何评分记录协同过滤算法直接失效。我用的兜底策略是新用户登录后按所有用户的平均评分从高到低返回热门美食列表同时前端页面上提示“为你推荐热门美食”。这个处理虽然简单但非常实用至少保证新用户打开页面不会空荡荡的。把这个逻辑写在代码注释里答辩时主动讲出来老师会觉得你考虑到了算法落地过程中的真实问题。5. 源码讲解的正确打开方式从登录接口到推荐列表的完整链路代码讲解不等于逐行读代码而是要先画出业务链路的全局图景再按链路逐个击破。下面这一段我把整个项目最核心的一条链路拆给你看这也是我讲解项目时的固定主线。5.1 用户登录从Controller到数据库的请求流转用户提交账号密码后前端调用/api/user/login接口请求进入UserController的login方法。Controller不做任何业务处理只负责接收参数、调用Service、返回结果。真正的业务在UserServiceImpl里完成先把密码用MD5或BCrypt加密再调用MyBatis-Plus的selectOne方法查库查到就生成一个Token这里我用的是简单的UUID存到Redis里并返回给前端查不到就抛异常。这条链路的重点是分层思想。很多初学者会把业务逻辑全部写在Controller里一个月后再看自己都懵了。分层之后Controller管接收和响应Service管业务Mapper管数据库各司其职改起来也方便。面试时被问“为什么需要三层架构”直接把这条链路讲一遍就行。5.2 评分接口一张表引发的连带更新评分接口/api/rating/submit接收userId、foodId和score三个参数。Service层要做的事有三件先判断这个用户之前有没有给这道菜评过分有则更新没有则新增更新完成后重新计算这道菜的平均分最后把平均分写回food表的rating_avg字段。这三步就是一次完整的事务任何一步失败都要回滚所以方法上加了Transactional注解。Transactional public void submitRating(Long userId, Long foodId, Integer score) { Rating rating ratingMapper.selectOne(...); if (rating null) { rating new Rating(); rating.setUserId(userId); rating.setFoodId(foodId); ratingMapper.insert(rating); } else { rating.setScore(score); ratingMapper.updateById(rating); } // 重新计算平均分并更新food表 Double avgScore ratingMapper.selectAvgScoreByFoodId(foodId); Food food foodMapper.selectById(foodId); food.setRatingAvg(avgScore); foodMapper.updateById(food); }建议在提交评分后调用一次推荐算法的计算入口这样用户评完分立即能看到新的推荐列表体验上会流畅很多。5.3 推荐接口核心算法与SQL查询的配合/api/recommend/{userId}这个接口是整个项目技术含量最高的部分。Service层的逻辑是查rating表拿到当前用户的评分记录取出评分最高的几道菜然后遍历所有菜品逐一计算相似度过滤掉用户已经评分或收藏的菜品对剩余菜品按相似度加权和排序取前N个返回。这个过程中最需要注意的性能坑点是不要循环查库。我第一版代码是在计算相似度时对每一对菜品都查一次评分表结果接口响应一点几秒完全没法用。优化方案是先把所有菜品的评分一次性查出按foodId分组存进Map里后面的计算全部基于内存数据响应时间直接降到几十毫秒。代码讲解的时候把这两个版本对比着讲就能很自然地引出“N1查询问题”这个高频面试考点。6. 部署文档的重灾区我从零到上线走过的弯路全记录部署环节是很多人栽跟头的地方。代码在本地能跑一到服务器就各种报错而且报错信息千奇百怪。这一节我按时间顺序把最容易踩的坑全部列出来每一段背后都是血泪教训。6.1 IDEA创建SpringBoot项目时的JDK版本坑很多人在IDEA里新建SpringBoot项目时发现选不了JDK 1.8或者项目创建成功后pom文件直接飘红。这是因为新版SpringBoot Initializr默认只支持新版本JDK。解法有两条要么手动把pom.xml里的SpringBoot版本降级成2.7.x要么直接到阿里云脚手架的地址去创建项目。说实话我用了无数遍之后还是觉得改pom最省事——复制一个正确版本号的完整pom模板改一下项目名就行根本不需要在IDEA向导里纠结。6.2 Maven依赖下载慢或失败的终极解法Maven中央仓库在国内访问不稳定经常出现依赖下载到一半就卡住。修改Maven的settings.xml加上阿里云镜像。配置内容全网都能搜到但有几个细节容易被忽略镜像要配置在mirrors节点里同时给仓库配置mirrorOf值为central表示中央仓库的请求都走这个镜像JDK版本编译参数也要在profile里声明否则Maven默认用JDK1.5编译代码里的Lambda表达式全部报错。改完之后清理一下本地仓库的lastUpdated文件重新reimport才能保证干净地拉一遍依赖。6.3 数据库初始化和时区问题MySQL 8.0以上版本连接时需要在JDBC URL后面加上时区参数serverTimezoneAsia/Shanghai。不加这个参数控制台会提示The server time zone value错误中文环境下还容易遇到编码问题URL里还要加characterEncodingutf8。导入SQL脚本前先用Navicat或命令行创建空数据库然后执行source /路径/init.sql检查一下food表的数据是否有中文乱码有乱码就说明数据库连接的不是UTF8编码需要在MySQL配置文件里把默认字符集改成utf8mb4。6.4 打包与服务器发布这里给没有Linux经验的同学一套可以照抄的操作顺序和命令。在IDEA右侧Maven面板执行mvn clean package -DskipTests打包完成后target目录下会生成一个可执行jar包。将这个jar包上传到服务器的指定目录例如/opt/app/。在服务器上先确认Java环境没问题java -version必须显示你项目所用的JDK版本。启动命令建议用nohup java -jar food-recommend-0.0.1-SNAPSHOT.jar app.log 21 这样即使断开SSH连接服务也不会停。查看日志用tail -f app.log看到Started XxxApplication就说明启动成功了。直接访问http://服务器IP:8080/如果页面打不开先检查阿里云或腾讯云的安全组是否放行了8080端口。安全组是个特别隐蔽的坑防火墙的坑同样隐蔽。 我在部署时经常遇到“服务器本机curl能通外面访问不了”十有八九是云平台的安全组规则没有配置好而不是你的代码有问题。花钱买云服务器或者用学生机的同学务必养成“先排网络再查进程”的排查习惯。6.5 端口被占用和Redis没启动的连环坑部署到服务器上后最常见报错之一是8080端口被其他进程占用。排查命令是netstat -tlnp | grep 8080找到占用进程的PID用kill -9 PID清掉再重启。如果项目里用了Redis服务器上没启动Redis服务会报连接超时错误本地正常运行不代表服务器也正常。把这些中间件的启动脚本写进部署文档每次部署时按脚本执行一遍能少走很多冤枉路。7. 代码讲解时的高频提问点准备这些答辩和面试就不用慌很多同学代码写完了但一被问就懵本质上是没有提前梳理问题集。我发现这个项目的高频提问点非常集中提前准备至少能应对九成以上的问答环节。7.1 为什么用MyBatis-Plus不用原生MyBatis这个问题背后考的是你的工程判断力。原生MyBatis灵活但繁琐单表CRUD要写一堆重复的XMLMyBatis-Plus在单表操作上做到开箱即用但多表关联查询还是需要手写SQL。你在实际项目中采用MyBatis-Plus是为了把开发重点放在业务逻辑而不是重复劳动上但也要明确指出它的适用边界复杂的多表关联、动态报表查询这类场景还是得退回手写SQL。7.2 推荐接口的数据量变大后怎么优化这是一个非常经典的扩展性问题。当前实现是每次请求都实时计算相似度数据量大了以后性能和计算量都会急剧膨胀。优化方向可以分几步先把用户评分数据缓存到Redis减少数据库压力然后离线定时计算所有菜品之间的相似度矩阵存入数据库或Redis线上推荐时只做查表操作不再跑算法。这个方案“离线计算在线查询”的思路在互联网公司也是通用做法。7.3 密码安全怎么处理明文存储密码是低级错误一定要做好安全说明。我在项目里用的是BCrypt加密Spring Security里自带BCryptPasswordEncoder也可以在代码里单独引入。BCrypt的好处是每次生成的密文都不同自带盐值而且不要求额外建字段存盐。答辩时可以顺便提一句真实生产环境还会配合登录验证码、失败次数限制、Token过期机制这些细节足以证明你有安全意识。7.4 搜索和推荐如何协作搜索功能走的是标题关键字匹配用的是MySQL的LIKE %关键词%推荐功能走的是协同过滤算法两者业务完全不同但可以形成互补用户主动搜索时展示搜索列表用户没有明确目标时展示推荐列表。这个设计在真实的美食平台里也很常见首页通常有两个区块——猜你喜欢和你可能感兴趣的品类。8. 扩展进阶思路这个项目还能往哪些方向长如果你时间充裕或者想让项目在简历上更有竞争力这几个方向是按投入产出比排好序的从易到难每一层都能作为独立的项目亮点去讲。加入Redis缓存把首页热门美食、排行榜列表缓存到Redis设置5分钟过期减少数据库查询次数。这一层改动很小但能体现出你对性能的感知。引入Elasticsearch把MySQL里的美食数据同步到Elasticsearch搜索、筛选、排序都走ES搜索体验大幅提升。这个扩展涉及数据同步和索引设计的知识面试时非常加分。加一个简单的后台管理用Vue写一个管理界面做菜品上下架、分类管理、用户管理项目从单终端变成了双终端完整度立刻不一样。把“相似菜品”做成接口在详情页展示与本菜相似的其他菜品既能增加页面停留时长又能复用推荐算法的相似度矩阵代码改动不大但效果明显。时间紧的话优先做Redis缓存和管理后台这两个方向不但体感好而且面试时最容易主动引出话题。最后再分享一个我反复带项目过程中的体会源码、部署、讲解这三件事里最影响最终评价的其实是讲解。能把一条推荐链路清晰完整地讲给别人听说明你从数据库到算法到接口都真正过了脑子讲不清楚的话哪怕代码全对答辩时也很难让老师相信是你独立完成的。建议你花一个晚上把登录接口到推荐列表的整条链路自己在纸上画一遍然后用嘴讲顺讲给室友听也行。这个准备步骤比再多写一百行代码都值钱。
返回列表