一套能直接跑的美食推荐系统:SpringBoot+Vue+MySQL全栈拆解与运行指南
市面上打着"可直接运行"旗号的全栈源码项目不少,但真正下载下来能一次跑通的却不多。这套美食信息推荐系统属于少见的良心项目——SpringBoot做后端接口、Vue做前端页面、MySQL存数据,三件套正好覆盖了当前企业里最主流的技术组合。我拿到源码后从环境准备到功能验证完整过了一遍,整体架构清爽,注释到位,非常适合做毕业设计、课程设计,或者想系统学习前后端分离项目的新手。这篇文章不吹不黑,就把这套系统的设计逻辑、核心代码思路、启动步骤和常见坑一次讲清楚。
1. 项目全貌:一个美食推荐系统到底包含哪些东西
1.1 功能模块:用户端与管理端双视角
先看这个系统解决了什么问题。传统的美食信息展示网站只是单纯列出菜品,用户得自己翻半天才能找到想吃的。这套系统在基础信息管理之上加了个性化推荐——根据用户浏览历史、收藏行为和评分数据,把可能感兴趣的菜品推到首页,这是它区别于普通管理系统的核心卖点。
从角色上划分,系统分两端:
- 用户端:注册登录、浏览菜品列表、按分类筛选、关键词搜索、查看菜品详情、收藏/取消收藏、打分评价、接收个性化推荐列表。
- 管理端:菜品信息管理(增删改查、上下架)、菜品分类管理、用户账号管理、推荐参数配置、系统数据统计概览。
这种双端结构是绝大多数信息管理类系统的标准范式。实际做项目时你会发现,前端的页面再多,后端抽象出来的接口无非就是围绕用户菜品分类收藏评分这几张核心表打转。
1.2 技术栈选型:为什么是这个组合
这套系统的技术栈选择非常务实,没有追逐新奇框架,全是市场验证过的成熟方案:
| 层级 | 技术选型 | 选择理由 |
|---|---|---|
| 后端 | SpringBoot 2.x + MyBatis + JWT | 生态成熟、招工需求大、资料多,遇到坑容易搜到答案 |
| 前端 | Vue 2.x + Vue Router + Axios + Element UI | 上手曲线平缓,组件库齐全,适合快速搭建管理后台 |
| 数据库 | MySQL 5.7 / 8.0 | 中小型项目绝对主力,免费开源,配套工具完善 |
| 构建工具 | Maven + npm | 事实标准,几乎不需要额外学习成本 |
SpringBoot的核心价值在于约定优于配置——你不用手动管理Bean生命周期、不用写一堆XML配置,一个启动类加几个注解就能把Web服务跑起来。Vue的前后端分离模式则让接口开发和页面开发可以并行推进,联调时只需要对齐接口文档。
1.3 项目目录结构:一个合格的前后端分离项目长什么样
后端部分按照经典的分层架构组织:
src/main/java ├── controller # 控制层,接收前端请求,返回JSON ├── service # 业务逻辑层,处理具体业务规则 ├── mapper # 数据访问层,与MySQL交互 ├── entity # 实体类,对应数据库表结构 ├── config # 配置类,跨域、拦截器、全局异常 ├── utils # 工具类,JWT生成解析、加密工具 └── common # 公共返回结果封装、状态码定义前端部分按Vue常规结构组织:
src ├── api # 所有axios请求封装 ├── router # 路由配置 ├── store # Vuex状态管理 ├── views # 页面组件 │ ├── user # 用户端页面 │ └── admin # 管理端页面 ├── components # 公共组件 └── utils # 请求拦截器、工具函数这个结构大概是所有SpringBoot+Vue项目的标准答案,弄懂了这一套,市面上大部分同类项目你拿到手都能快速定位代码位置。
2. 跑通项目的第一步:环境配置中劝退新手的三个关键点
源码再好,环境搭不起来等于白搭。我在实测过程中发现,这个项目最大的门槛恰恰不在代码本身,而是环境版本匹配问题。
2.1 JDK版本:SpringBoot版本和Java版本必须对得上
很多人栽在这里:项目用的是SpringBoot 2.x,却装了Java 17甚至Java 21,结果启动直接报错。SpringBoot 2.x系列默认基于Java 8编译,虽然高版本Java也可能跑起来,但会遇到各种反射、字节码层面的兼容警告,甚至直接启动失败。
我的建议是直接安装JDK 1.8,一劳永逸。安装完成后在命令行验证:
java -version看到版本号显示1.8.x就没问题。然后用IDE打开项目时,务必检查两处:Project Structure里的Project SDK是否选到JDK 1.8,Maven的Settings里JRE是否也指向1.8。很多新手IDE里能编译,一跑就报错,就是这里配置不一致。
2.2 MySQL:5.7还是8.0,连接配置有什么区别
这个项目对MySQL版本兼容性不错,5.7.44和8.0.x都能正常跑。但新手容易在两个地方卡住:
第一次用MySQL 8.0连接时报Public Key Retrieval is not allowed。这是因为8.0默认的caching_sha2_password认证方式需要安全连接才能获取公钥。解决方法是JDBC连接串加一个参数:
spring: datasource: url: jdbc:mysql://localhost:3306/food_recommend?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueMySQL 5.7的驱动名和8.0不同。5.7用com.mysql.jdbc.Driver,8.0用com.mysql.cj.jdbc.Driver。如果项目里写的是旧驱动配新数据库,会直接报ClassNotFoundException。你可以先确认自己装的版本,再去看项目application.yml里的配置是否匹配。
安装数据库后记得把my.ini里的字符集设为utf8mb4,否则导入数据后中文可能是乱码。utf8mb4比utf8支持更多字符(比如emoji),是当前的主流选择。
2.3 Node环境和npm依赖:Vue项目跑不起来的头号元凶
前端部分的坑主要集中在npm依赖安装上。项目里的package.json一般会写Vue版本和Element UI版本,这些依赖在实际安装时经常因为网络问题失败。
我的实操建议是设置npm国内镜像,然后把依赖装到项目下的node_modules:
npm config set registry https://registry.npmmirror.com然后在项目前端目录下执行:
npm install如果装到一半报错,优先看是不是node-sass这类需要本地编译的包在报错。Vue 2项目里node-sass是经典痛点——它需要和Node版本严格匹配。最简单的处理办法是换成npm install node-sass@4.14.1对应的兼容版本,或者干脆用vue-cli脚手架内置的sass代替。
装完依赖后跑一下开发服务:
npm run serve看到命令行输出App running at http://localhost:8081基本就说明前端活过来了。
3. 后端核心设计:从登录鉴权到推荐逻辑的实现思路
3.1 JWT登录鉴权:无状态认证的完整链路
这个系统的登录模块用的是JWT(JSON Web Token),它的核心思想是:服务器不存登录状态,用户登录成功后拿一个加密签名的token,之后每次请求都带上它,服务器只要验签通过就信任这个用户。
代码里的实现链路大致是:
- 用户提交用户名密码,后端查MySQL比对。
- 比对通过后,用
jjwt库生成一个包含用户ID和角色的token,设置过期时间(一般是2到24小时)。 - 前端拿到token后存在
localStorage里,axios每次请求在拦截器中把token拼到请求头。 - 后端写一个
JwtInterceptor拦截器,过滤掉登录接口和白名单路径,剩下的接口先取请求头里的token,验签失败直接返回401。
// 拦截器中校验token的核心逻辑 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { return unauthorized(response); } try { Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { return unauthorized(response); } }实用心得:密码入库前必须用BCrypt加密,绝对不能明文存储。项目里一般用spring-security-crypto包里的BCryptPasswordEncoder,同一个密码每次加密结果都不同,验证时用matches方法比对。这种方式即便数据库泄露,用户的原始密码也不会直接暴露。
3.2 推荐的本质:基于标签和行为的"猜你喜欢"
这是整个项目最有含金量的部分。真实商业级推荐系统会用协同过滤、矩阵分解甚至深度学习,但一套教学/毕设级别的系统通常采用两种简化可行的方法:
基于内容标签的匹配:给每道菜打标签(川菜、甜品、低脂、高蛋白),给每个用户也维护一份偏好标签集合,推荐时计算用户标签和菜品标签的匹配度,取Top N返回。
基于热度加权的排序:菜品的默认得分是浏览量、收藏量和评分的线性加权,比如:
score = 浏览数 * 0.3 + 收藏数 * 0.4 + 平均评分 * 100 * 0.3用户登录时,先从用户最近的浏览和收藏记录里提取高频标签,再查这些标签下的菜品,按这个score公式降序排列,最终取前20条。未登录用户直接看到全站热度最高的菜品列表。
这个逻辑在代码里通常体现为几条SQL的组合:
-- 示例:查询用户最近收藏的菜品标签 SELECT tag FROM collect_record cr JOIN dish d ON cr.dish_id = d.id WHERE cr.user_id = #{userId} GROUP BY d.tag ORDER BY COUNT(*) DESC LIMIT 5;拿到高频标签后,再去菜品牌里找标签匹配且用户没看过的菜品,排除掉用户已经收藏过的记录,排序返回。
3.3 统一返回结果与全局异常处理:企业级开发的隐形规范
看过很多新手项目,Controller层直接返回Map或者裸数据,前端拿到后还得猜字段含义,非常痛苦。这套系统的做法值得学习——封装一个统一的Result<T>对象:
public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; private T data; }所有Controller都返回这个结构,比如:
@PostMapping("/login") public Result<String> login(@RequestBody LoginDTO dto) { String token = userService.login(dto); return Result.success("登录成功", token); }配合@RestControllerAdvice做全局异常处理,业务代码里只管抛异常,统一由异常处理器转成JSON返回,前端拿到code=500就知道请求失败了。这样做的好处是接口协议统一、前端处理逻辑简单、后端代码也清爽。
3.4 跨域问题:前后端分离必过的坎
前端跑在localhost:8081,后端跑在localhost:8080,端口不同就构成跨域。浏览器同源策略会拦截这类请求,前端联网全失败。解决方式在SpringBoot里加一个CORS配置类:
@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }; } }注意allowedOriginPatterns是SpringBoot 2.4+的写法,老版本用allowedOrigins("*"),和allowCredentials(true)一起用时allowedOrigins("*")会出问题。这个细节我见过不少人在开发环境折腾一天最后只改了一行代码解决。
4. 前端页面拆解:Vue端从样式到逻辑的逐个击破
4.1 前端工程结构:按页面组织视图,按模块拆分请求
进入前端代码,你会发现所有的API请求都被集中封装在src/api目录下,每个文件对应一类资源:
// src/api/dish.js import request from '@/utils/request' // 分页查询菜品列表 export function getDishList(params) { return request({ url: '/api/dish/page', method: 'get', params }) } // 获取推荐菜品 export function getRecommendList(userId) { return request({ url: '/api/dish/recommend', method: 'get', params: { userId } }) }@/utils/request.js里统一创建axios实例,配好基础URL、超时时间和请求拦截器。请求拦截器自动从localStorage读取token拼到请求头上,响应拦截器遇到401就跳回登录页。这样业务组件里只需要调用封装好的函数,完全不需要关心token这些细节。
4.2 核心页面:推荐首页、菜品详情、管理后台
推荐首页是这个系统的门面。页面加载时调用getRecommendList拿到推荐菜品数组,用卡片组件循环渲染。每张卡片展示图片、名称、标签、评分,点击进入详情页。这里要注意图片资源路径的配置——开发环境图片放本地public目录就行,正式部署时如果图片很多,建议用OSS或独立的静态资源服务。
菜品详情页包含菜品图文介绍、评分展示(Element UI的el-rate组件)、收藏按钮和评价列表。收藏按钮的交互逻辑是:调接口判断当前用户是否已收藏,如果已收藏则显示"取消收藏",点击后调用对应的API。
管理后台用Element UI的el-table展示数据列表,配合el-dialog做新增和编辑弹窗,el-form做表单校验,这一套组件组合是Vue 2后台项目的黄金搭档。管理员对菜品的增删改查全部走向后端接口,前端表单校验只做基础拦截,真正严格的参数校验还是要在后端做。
4.3 路由设计:为什么需要路由守卫,动态路由的坑在哪
路由模块做了两个层面的工作:
静态路由分两块:用户端的/home、/dish/:id、/user等页面,管理端的/admin、/admin/dish等页面。
路由守卫负责权限控制。用户在未登录状态下直接访问/user个人信息页,会被router.beforeEach拦下来重定向到登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })热搜词里出现"vue动态路由"、"vue路由"说明这是新手关注的高频问题。动态路由的基本原理是先登录拿到当前用户的角色和可访问菜单列表,再用router.addRoutes把权限路由动态添加进路由表。但我要提醒一句:动态路由实现起来比看起来复杂得多,刷新页面路由会丢失,需要配合Vuex重新拉取菜单。如果你做的是毕设或内部系统,静态路由+路由守卫已经足够,不必强行上动态路由。
4.4 前端部署方式:开发环境与生产环境的差异
开发环境是前后端分离跑,生产环境有两种常见选择:
- 用Nginx反向代理,把
/api转发到SpringBoot的8080端口,前端静态文件由Nginx直接托管。 - 把前端打包后的
dist目录整个放进SpringBoot的src/main/resources/static下,直接由一个端口提供服务。
第二种方式适合演示和答辩场景,避免了Nginx配置的复杂性。操作很简单,在项目前端目录执行:
npm run build将生成的dist目录内容复制到后端的resources/static目录下,重新启动SpringBoot,访问http://localhost:8080就能看到完整系统。这也是热搜词"vue打包放进springboot中"对应的操作场景。
5. 数据库设计:推荐系统的数据从哪来到哪去
5.1 核心表结构与字段设计思路
这套系统的数据库表不算多,但每张表都有讲究。核心表大致如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
user | 用户信息 | id, username, password(bcrypt), avatar, role |
category | 菜品分类 | id, name, description |
dish | 菜品信息 | id, name, category_id, tag, price, image, description, status |
collect_record | 收藏记录 | id, user_id, dish_id, create_time |
rating_record | 评分记录 | id, user_id, dish_id, score, create_time |
browse_record | 浏览记录 | id, user_id, dish_id, create_time |
tag字段直接放在dish表里,没有单独建标签表,这个设计对小型项目完全够用。新增"甜点""辣味"这类标签就是一次UPDATE,不涉及多表关联,查询时WHERE tag = "甜品"就完成了推荐系统的候选集提取。
5.2 唯一约束与联合索引:保证数据准确性的基础
收藏记录和评分记录最怕重复插入。collect_record表应当建一个UNIQUE KEY(user_id, dish_id),这样用户重复点击收藏时数据库层面就会拦截,而不是靠业务代码查一遍再决定是否插入。
这几个表都建议建联合索引,因为推荐和统计SQL基本都是按user_id + 类别字段过滤:
ALTER TABLE browse_record ADD INDEX idx_user_dish_time (user_id, dish_id, create_time);5.3 为什么新手项目普遍不用外键
你会发现这套系统表与表之间逻辑上有关联(比如collect_record.user_id对应user.id),但建表语句里没有FOREIGN KEY外键约束。这不是偷懒,而是有意识地取舍。
外键会带来三个麻烦:插入数据必须严格按父子顺序、删除父表记录时子表要么报错要么级联删除、高并发场景下锁竞争更严重。业务层已经通过事务和代码逻辑保证数据一致性,物理外键反而成为性能和灵活性的拖累。面试时能讲清楚这一点,比背概念更能加分。
5.4 数据库初始化:导入SQL脚本的正确方式
项目目录下一般会附带schema.sql或food_recommend.sql文件。导入方法很简单:
mysql -u root -p < food_recommend.sql或者用Navicat等图形工具直接执行SQL文件。导入后重点检查三件事:表是否全部创建成功、是否有初始管理员账号(一般是admin/admin123)、测试数据是否完整(菜品最好有20条以上,太少的话推荐效果不明显)。
6. 从"能跑"到"能演示":启动顺序、排错方法与项目答辩建议
6.1 完整的启动流程:三步起服务
第一步启动MySQL,确保服务端口3306可访问,确认application.yml里的数据库名、用户名、密码和实际环境一致。
第二步启动后端,SpringBoot项目在IDE里直接运行main方法。观察控制台输出,看到Tomcat started on port(s): 8080说明后端启动成功。
第三步启动前端,进入前端目录执行:
npm run serve浏览器访问http://localhost:8081,看到登录页说明系统已就绪。
6.2 常见启动报错的排查链路
实测过程中,我整理了下面几类高频问题及排查思路:
| 报错信息 | 根本原因 | 处理方案 |
|---|---|---|
Access denied for user 'root'@'localhost' | 数据库密码错误或账号不允许本地登录 | 核对yml配置、重置密码 |
Table doesn't exist | SQL脚本没导入或导错库 | 检查数据库是否选对、执行脚本时指定了正确的库 |
Port 8080 was already in use | 端口被占用 | netstat -ano查占用进程,或改yml里的端口 |
npm ERR! code ERESOLVE | 依赖版本冲突 | 删掉node_modules和package-lock.json重新install |
跨域请求失败 | CORS配置没生效或前端proxy没配 | 检查后端CorsConfig是否加载、axios baseURL是否正确 |
一个很重要的排查技巧:分清是前端问题还是后端问题。浏览器按F12打开开发者工具,如果Network里能看到请求发出但报404/500,那就是后端或接口路径问题;如果压根没有请求发出,那就是前端路由或axios配置问题。这个二分法能省掉大量排查时间。
6.3 给做毕设的同学:怎么给这个项目加分
如果这套系统是你的毕业设计或课程项目,建议在保留现有架构的基础上做以下一到两个增强:
引入协同过滤:不用自己实现算法,用Taste或Mahout这样的Java推荐库,把用户评分数据喂进去,输出推荐结果。这比标签匹配"看起来更智能",也更有技术含量。
数据可视化:管理端增加一个统计页,用ECharts展示菜品热度排行、用户活跃曲线、分类占比扇形图。ECharts是纯前端组件,接入成本低,展示效果却很惊艳,答辩时这是最直观的加分项。
搜索优化:MySQL用LIKE '%关键词%'做搜索在数据量小时没问题,但数据过万后性能堪忧。可以把搜索改成Elasticsearch或至少用全文索引,这也是面试时很好的谈资。
6.4 这套系统的边界与常见误解
我必须泼一点冷水:这套系统的架构适合学习和演示,但不要天真地以为它就是生产级推荐系统。真实业务场景下,推荐结果需要处理冷启动问题(新用户没有任何行为数据怎么办)、需要实时更新用户兴趣、需要A/B测试验证推荐效果,这些都是当前源码没有覆盖的。另外MySQL单库的容量上限在千万级数据量附近,超过了就得考虑分库分表。
理解这些边界不是坏事,反而说明你真的看懂了系统——你知道每块砖是怎么砌的,也知道墙在哪个位置之后不能再加高了。
写在最后的一点建议
这套源码项目给的是一份标准答案,但真正把前后端分离、JWT鉴权、推荐逻辑这些知识点变成你自己的能力,还是要动手改代码。我的经验是拿到项目后先不改功能,而是沿着一条请求链路完整走一遍——比如从点击"收藏"按钮开始,前端怎么发请求、后端怎么校验token、Service层怎么查重、Mapper层怎么执行SQL、最后怎么返回结果。把这条链路读通,比抄代码十遍都有用。改完一处功能后,再把推荐算法从"标签匹配"换成"协同过滤",你才算把这套框架玩明白了。