ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3美食推荐系统实战:从数据库设计到协同过滤落地

SpringBoot+Vue3美食推荐系统实战:从数据库设计到协同过滤落地 1. 项目概述与需求拆解美食信息推荐系统到底解决什么问题做这类“Java Web美食信息推荐系统”的人通常只有两种一种是正在赶毕业设计的学生另一种是想把 Java 后端和 Vue 前端完整串起来练手的新人。我拿到这个项目时目标非常明确用 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 四件套做一个能跑通完整业务闭环的实战项目——用户注册登录、菜品浏览、评分收藏、智能推荐、后台管理一个环节都不能少。很多人看到“推荐系统”四个字就腿软觉得要上机器学习模型、要做协同过滤矩阵分解。这是典型的过度设计。毕设和企业里的中小型项目推荐模块最合理的定位是基于用户行为的可解释推荐用户能感知到“推荐结果确实符合我的口味”评审老师能看到“推荐逻辑有数据支撑、有业务闭环”这就够了。整套链路最容易被卡住的点其实不在推荐算法而在 MySQL8.0 的环境配置、前后端跨域、MyBatis-Plus 分页插件这三处——后面我会用一整章做排错记录。这篇内容适合三类人群选题相关的同学、想在简历里写一个“讲得清推荐逻辑”的 Java Web 项目的开发者、想看看 SpringBoot2 和 Vue3 前后端分离项目真实协作方式的新手。我会按实际开发顺序来拆数据库设计 → 后端接口 → 前端页面 → 推荐逻辑 → 部署运行最后附一份实测过的报错速查表保证你看完能直接照着复现。1.1 系统边界不贪多把核心链路做扎实美食信息推荐系统的核心链路是用户产生行为数据评分、收藏、浏览标签→ 后端基于行为数据计算推荐结果 → 前端展示推荐列表和推荐理由 → 用户继续产生行为数据。这是一个可循环的数据闭环。基于这个定位我划定了三个模块作为必要项其余全部是可选项用户端核心注册登录、菜品检索、评分收藏、猜你喜欢推荐管理端核心菜品上下架、分类管理、用户管理、基础数据统计推荐端核心标签偏好推荐、评分协同推荐、热度兜底推荐不建议再扩展什么社区发帖、外卖下单、厨师接单之类的内容。每多一个模块答辩被追问的风险就多一分。你宁可把评分推荐逻辑讲透也不要塞五个半成品功能进去。1.2 功能清单与页面规划这个系统的功能清单我按“前台展示 后台管理”两条线来规划共四个主要页面组首页推荐区轮播 banner、猜你喜欢、高分菜品榜、分类导航菜品详情页菜品大图、口味标签、价格、评分展示、收藏按钮、推荐理由区用户中心个人信息编辑、我的收藏、我的评分记录、偏好标签设置管理后台菜品管理表格、分类管理、用户列表、推荐参数配置页面数量控制在八个以内一个人在一个学期内做完并且能讲明白是合理的。2. 技术选型取舍为什么这套组合最适合中文 Java Web 项目2.1 SpringBoot 2.7.x不是最新但一定是最稳的2025 年 SpringBoot3 已经不算新鲜但对我来说做这类项目时 SpringBoot2.7.x 依然是最优解。原因很现实SpringBoot3 强制要求 JDK17而 JDK8 环境下能跑的老组件版本非常多遇到报错能搜到答案的概率高得多。SpringBoot2.7.14 配合 IntelJ IDEA 2023 之前的版本使用完全不需要折腾 JDK 环境。我在项目里锁定的是 spring-boot-starter-parent 2.7.14。注意不要随便改这个 version 字段。2.7.14 已经帮你把 druid 连接池、mysql-connector-j、lombok、hutool 这些常用组件的版本锁好了自己单独引入其它依赖时如果不带 version反而容易踩版本冲突。用 parent 的统一依赖管理是 SpringBoot 项目的第一课。2.2 Vue3 ViteComposition API 带来的是代码组织方式的变化Vue3 相比 Vue2 最大的变化不是语法糖更多了而是逻辑组织方式从“选项式”变成了“组合式”。举一个美食项目里的真实场景推荐页需要同时维护推荐列表数据、加载状态、错误信息、翻页参数、下拉刷新事件五样东西。在 Vue2 里你要分别写到 data、methods、mounted、computed 四个区域改一遍逻辑得跳四次位置Vue3 的 setup 函数里这五样东西可以全部聚在一个代码块里从“状态定义”到“逻辑处理”到“声明周期调用”读代码的时候一条线扫下来非常顺畅。脚手架我推荐 Vite 而不是 Vue CLI。理由只有一个冷启动快。Vite 开发服务器是秒级启动保存代码后的热更新也是毫秒级对比 Vue CLI 那种动辄三五秒的编译等待效率提升直接体感可见。唯一的坑是代理配置位置不同Vue CLI 写在 vue.config.jsVite 写在 vite.config.js 的 server.proxy 字段这点在联调章节我会展开。2.3 MyBatis-Plus把单表 CRUD 从“写 SQL”变成“调用方法”MyBatis-Plus 解决的痛点是一张表的基础增删改查本质上就是模板代码写 SQL 纯属浪费时间。它的 BaseMapper 接口自带 selectById、insert、updateById、deleteById、selectPage 等方法IService 接口更是直接给你 getById、save、updateById、page、list 这一整套业务层方法。更实用的是 LambdaQueryWrapper 条件构造器。比如我要查“川菜分类下面辣度大于两级的菜品”用new LambdaQueryWrapperDish().eq(Dish::getCuisine, 川).gt(Dish::getSpicyLevel, 2)就完成了。字段引用是方法引用编译期就能检查拼写错误不会像字符串拼 SQL 一样运行期才报警。分页则依靠分页插件 PaginationInnerInterceptor配合 Page 对象三行代码出一个分页结果。2.4 MySQL8.0字符集和认证插件是绕不开的两个坎MySQL8.0 相比 5.7 进步很多默认字符集升级为 utf8mb4存 emoji 和生僻字都没有压力。但有两个问题不提前处理会耽误不少时间。第一个是认证插件。MySQL8.0 默认的认证插件是 caching_sha2_password老版本的可视化工具和一部分 JDBC 驱动会连不上报 “Public Key Retrieval is not allowed”。最省事的方法是在安装时选择 “Use Legacy Password Encryption”或者装完以后在 my.ini 配置default-authentication-pluginmysql_native_password。第二个是时区。MySQL8.0 的 JDBC 连接串里必须带上serverTimezoneAsia/Shanghai或?serverTimezoneUTC否则插入时间字段会偏差 8 个小时。项目里如果时间比较多建议统一使用LocalDateTime类型结合 MyBatis-Plus 的自动填充功能处理 create_time 和 update_time会比 Date 类型顺手很多。3. 数据库设计与后端核心实现3.1 六张核心表字段怎么设计才够用又不冗余美食推荐系统的表结构可以精简但语义要完整。我最终保留了六张表其中业务表四张、关联表两张user用户表id、username、password、nickname、avatar、taste_tags偏好标签逗号分隔、create_timecategory分类表id、name、icon、sortdish菜品表id、name、category_id、image、description、price、spicy_level、cuisine菜系、main_ingredient、heat热度值、status上架状态、create_timerating评分表id、user_id、dish_id、score1-5 分、comment、create_timefavorite收藏表id、user_id、dish_id、create_timeadmin管理员表id、username、password为什么 taste_tags 不用单独建表因为在这个项目规模里偏好标签只需要在推荐模块里做字符串匹配用逗号分隔存一个字段查询时FIND_IN_SET或者 Java 端 split 再匹配都行。如果你要把它做成多对多标签体系再拆 tag 表和 dish_tag 表那是可扩展方向但毕设阶段属于过度建模。3.2 通用 CRUD 的落地四件套模板用 MyBatis-Plus 写一套标准 CRUD 需要四个类实体类TableName(dish)注解标明表名主键字段用TableId(type IdType.AUTO)Mapper 接口继承BaseMapperDishService 接口继承IServiceDishServiceImpl 实现类继承ServiceImplDishMapper, Dish并实现对应接口这四个类写完后Dish 表的基础增删改查方法就全部就绪了。Controller 层直接调用dishService.page(page, wrapper)就可以返回分页结果。我实际测下来同样是写用户管理接口传统 MyBatis 方式要写 XML 和 SQL 至少半天MyBatis-Plus 方式一小时能写完所有模块的 CRUD。// 一个标准的分页查询接口三行完成 PageDish page new Page(current, size); LambdaQueryWrapperDish wrapper new LambdaQueryWrapper(); wrapper.eq(Dish::getStatus, 1).orderByDesc(Dish::getHeat); PageDish result dishService.page(page, wrapper);3.3 响应格式统一前后端联调最容易被忽略的细节前后端分离项目里接口返回的 JSON 格式如果不统一前端拿到数据要写一堆防御代码维护起来非常痛苦。我在项目里统一封装了一个ResultT对象三个字段code200 成功 / 401 未登录 / 500 业务失败、message提示信息、data业务数据。后端所有接口都返回ResultT。前端封装 axios 实例在响应拦截器里统一判断 codecode 为 200 就直接取response.data.data使用code 为 401 就清空本地 token 并跳转登录页code 为 500 就弹出 message 提示。这样前端业务代码里完全不需要关心错误分支只管拿到数据渲染页面就行。登录认证我采用简化版方案登录成功后后端生成一段 UUID 作为 token存入内存 Map查询所有需要登录的接口时通过拦截器校验请求头 Authorization 字段里的 token 是否存在于 Map。这个方案比写完整的 JWT 简单得多缺点是服务重启后 token 全部失效但本地开发和演示完全够用。4. 推荐功能怎么实现才不算“糊弄”4.1 第一版基于标签偏好匹配最先实现的是基于用户明确偏好标签的推荐。注册时让用户选择口味偏好比如“辣、川菜、荤菜”每道菜品在创建时打上对应属性如麻婆豆腐对应的 cuisine川、spicyLevel3、mainIngredient牛肉。推荐时遍历全部上架的菜品计算匹配分命中一个标签加 1 分最后按分数降序取前十条。这个逻辑很简单跑起来也有效果但它有一个致命缺陷——只考虑静态偏好不考虑动态行为。用户明明选的是“清淡”却连续给辣菜打了五星这时候系统还在推荐清汤寡水那就闹笑话了。所以必须在标签匹配之上叠加一层基于评分行为的推荐。4.2 第二版评分驱动的简化协同过滤协同过滤的核心思想是**“和你口味相似的用户喜欢的东西你大概率也喜欢”**。这里不需要做真正的矩阵分解用最朴素的思路就能跑通假设用户 A 给了“麻婆豆腐”5 分系统用这几条 SQL 查询联动完成一次推荐周期查询用户 A 的评分记录得到他评分过的菜品集合 D遍历 D 中的每道菜查询所有也评分过这道菜的其他用户得到候选用户集合 U对 U 中的每个用户找出他们对哪些菜品给过高分把这些菜品的得分加权累加所有权重求和后再排序权重计算我用了一个很粗糙但效果还行的方法两个用户共同评分的菜品数量除以两人评分差的平均绝对值。简单说共同口味越多、评分偏差越小相似度权重越高。加权公式为推荐值(菜品 j) Σ(候选用户 u 对菜品 j 的评分 × 权重(u, 当前用户)) 权重(u, 当前用户) 共同评分菜品数 / (1 平均评分差)这个逻辑的代码量不大但效果比纯标签匹配提升明显。尤其当数据积累到几十个用户互相评分后推荐结果开始有“猜你喜欢”的味道了。4.3 冷启动与推荐理由可解释性不是加分项是必答项冷启动问题新用户没有任何评分记录协同过滤直接失效。这时系统回退到标签匹配如果标签也没选就按热度值 heat 倒序推荐热门菜品。三级回退逻辑保证任何状态下首页都有内容可推。推荐理由的实现很简单但也容易被忽略在标签匹配阶段记录每个推荐菜品的匹配标签并拼成一个 reason 字符串返回给前端比如“因为你选择了标签辣、川菜为你推荐麻婆豆腐”。前端把 reason 展示到菜品卡片底部。用户看到这条理由的瞬间就理解了推荐机制答辩时评审老师也会觉得你考虑了“系统的可解释性”——这在推荐系统领域是正经的研究方向。5. Vue3 前端搭建与联调实战5.1 Vite 项目中 directory 怎么规划才清晰前端根目录下我分了六个目录views页面组件、components公共组件、api接口请求函数、router路由表、storePinia 状态、utils工具函数。api 目录按后端控制器拆分例如api/dish.js、api/user.js、api/recommend.js每个文件里导出请求函数组件里只 import 对应的函数不直接写 axios 调用。这样后端接口地址调整时只需要改 api 目录页面不用动。5.2 推荐页面的 Composition API 写法实录推荐页面是前端最核心的页面需要管理的数据项包括推荐列表、加载状态、报错信息、当前页码四个。用 Composition API 组织代码结构如下import { ref, onMounted } from vue import { getRecommendList } from /api/recommend export default { setup() { const recommends ref([]) const loading ref(true) const error ref() const page ref(1) const fetchRecommends async () { loading.value true try { const res await getRecommendList({ page: page.value }) recommends.value res.data } catch (e) { error.value 推荐数据加载失败 } finally { loading.value false } } onMounted(fetchRecommends) return { recommends, loading, error, fetchRecommends } } }这个写法对比 Vue2 的 data methods mounted 三分法最大的好处就是所有推荐相关逻辑聚合成一个 setup 函数后续加“下拉刷新”功能你只需要在 setup 里多定义一个刷新函数别的区域完全不用碰。5.3 跨域联调Vite 代理配置与后端 CorsFilter 双保险前后端分离开发时前端地址是localhost:5173后端是localhost:8080浏览器直接跨域。解决方案不是后端拼 CORS 注解而是配置开发代理。Vite 的写法// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这里有一个容易踩的坑前端请求路径写成/api/dish/list代理转发到后端时会把/api前缀去掉所以后端 Controller 的 RequestMapping 里不要再写/api前缀。我一开始两边都写了/api结果请求路径变成http://localhost:8080/api/api/dish/list404 排查了半小时才发现。另外建议后端加一层全局 CORS 配置作为双保险。部署到生产时前端 build 产物直接放到 SpringBoot 的 static 目录下同源访问就不需要代理了。6. 环境准备、运行部署与排错手册6.1 MySQL8.0 安装与初始化要点本地安装 MySQL8.0 时几个关键点按顺序走就不会出问题安装类型选 Custom只勾选 MySQL Server 和 MySQL Workbench认证方式选 “Use Legacy Password Encryption”这一步直接避开认证插件兼容问题数据库字符集选择 utf8mb4排序规则选 utf8mb4_general_ci初始化完成后用 Workbench 新建数据库food_db导入项目自带的food_project.sql如果你习惯用 Docker 跑 MySQL8.0同样需要注意时区参数和挂载目录配置否则重装容器后数据容易丢。docker 方式我一般建议本地调试用真正毕设答辩演示还是本地 MySQL 实例更稳。6.2 一键启动运行全流程运行顺序很重要串错一个就可能报连接拒绝先启动 MySQL 服务确认 3306 端口正常监听启动后端 SpringBoot 项目等待日志打印 “Started” 或 “Tomcat started on port 8080”启动前端npm install安装依赖后运行npm run dev浏览器访问http://localhost:5173用文档里提供的测试账号登录后端项目通过 IDEA 打开后修改 application.yml 里的数据库用户名和密码直接运行主类即可。第一次启动如果依赖下载慢建议先配好阿里云 Maven 镜像能省不少时间。6.3 常见报错速查表实测版这套项目是复现率很高的技术栈我将实际运行中遇到的典型问题整理成速查表方便你和跑通流程遇到问题时对照定位报错现象根本原因解决方式MySQL 连接报 Public Key Retrieval is not allowedMySQL8.0 的 caching_sha2_password 插件与驱动不兼容JDBC 连接串加allowPublicKeyRetrievaltrue和useSSLfalse分页查询 total 为 0 或不分页MyBatis-Plus 分页插件未注册配置类中注册MybatisPlusInterceptor并添加PaginationInnerInterceptor前端请求跨域Vite 代理未配置或配置位置不对修改 vite.config.js 的 server.proxy 字段后端加 CorsFilter 双保险登录后请求仍被拦截为未认证请求头未携带 tokenaxios 请求拦截器对配置 object 统一添加 Authorization header时间字段差 8 小时数据库与连接串时区未统一连接串添加serverTimezoneAsia/Shanghai引入其它组件后编译冲突依赖版本被覆盖删除显式版本号让 spring-boot-starter-parent 统一管理版本6.4 项目源码与文档结构说明项目附带的文档不是那种随便写写的“说明文档”它包含完整的系统概述、需求分析、ER 图、数据字典、接口列表和部署说明。答辩时你的需求分析章节可以直接从文档的思路里延伸。建议拿到项目后照着三个步骤做先按文档把系统完整运行起来首页推荐能看到数据通读后端 service 目录理解每个接口的业务逻辑尝试改动一个点比如推荐策略的权重参数或菜品列表的排序规则观察系统行为变化做完这三步你对这个项目的理解深度绝对能支撑起答辩的任何追问。很多人栽在“上来就改功能”代码没跑通就动刀最后越改越乱。我的经验是先把整套系统完全理解再动手磨刀不误砍柴工。最后再说一个实际体验推荐功能做得是否“可解释”比推荐算法本身是否“高级”重要得多。哪怕只是标签匹配加上“因为你选择了辣和川菜”这样的推荐理由用户的感知完全不一样。后续如果你想升级这个项目可以从两个方向发力把评分协同过滤改成真正的用户相似度矩阵计算或者把前端推荐卡片加上滑动动画和加载骨架屏。项目整体的完成度已经足够撑起一个完整的“用户-行为-推荐”闭环剩下的就是给这个闭环加一些产品感和细节了。
返回列表