ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis+MySQL高校饮食推荐系统实战

SpringBoot+Vue+MyBatis+MySQL高校饮食推荐系统实战 最近在整理高校学生饮食推荐系统这类全栈项目发现后台私信里问的最多的就是SpringBootVueMyBatisMySQL这套组合到底怎么串起来。说实话这个项目看起来是典型的毕业设计题但真要动手做从数据库设计到前后端联调坑不少。今天我就把自己折腾这套系统时踩过的坑、用过的方案从头到尾捋一遍顺便把推荐逻辑怎么落地、MyBatis分页缓存怎么配置这些细节都摊开讲清楚希望能帮到正在做类似项目的朋友。1. 先把这个项目看明白需求拆解与技术选型思路1.1 这个系统到底要解决什么问题高校学生饮食推荐系统名字听着高大上拆开看其实就两件事一是食堂档口、菜品的数字化管理二是根据学生的口味偏好、营养需求做推荐。说白了就是给食堂做了一个带推荐功能的管理后台加手机端浏览页面。做这类项目最容易犯的毛病是一上来就写代码结果做到一半发现表结构不对、接口设计不合理。我建议第一步先画清楚角色和业务流。系统里至少要有两类角色学生用户和管理员。学生端要能浏览菜品、按分类或营养标签筛选、查看推荐结果、收藏和评价管理员端要做菜品维护、分类管理、推荐规则的配置最好还能看一些简单的统计报表。把这个需求想清楚之后整个项目其实就是三个大块基础信息管理菜品、分类、用户、推荐引擎基于标签和规则的匹配、前端展示与交互。搞清楚业务边界之后技术选型就顺理成章了。1.2 为什么是SpringBoot Vue MyBatis MySQL这套组合这套技术栈能成为绝大多数高校项目的标配不是没有原因的。SpringBoot把Spring繁琐的Bean配置基本干掉了起步依赖加一个java -jar就能跑出个Web服务上手成本低Vue的响应式数据绑定和组件化开发做后台管理界面和移动端H5都很快MyBatis半自动ORM的特性让它写复杂SQL非常灵活尤其是多表关联和动态查询这种场景比全自动的JPA更可控MySQL本身就是开源免费学生项目跑在2G内存的云服务器上也毫无压力。有人可能会问那Spring Data JPA不也挺好MyBatis-Plus不是更省事吗说实话这个项目用JPA也能做但饮食推荐里免不了要根据多个条件动态拼接SQL用JPA写Specification那套东西反而绕。MyBatis-Plus确实香但传统MyBatis更能让你把SQL掌控在手里而且面试时问MyBatis缓存、分页插件这类问题也有东西可以聊。1.3 功能模块全梳理从用户端到管理员端我把功能模块大致列一下方便你对照自己的进度查漏补缺用户端账号注册登录建议用手机号或者学号配一个简单的JWT鉴权就够了不用搞OAuth那套复杂的东西菜品浏览和分类筛选按食堂档口、菜品种类、价格区间、口味标签推荐页根据当前用户的偏好标签和点餐记录每天给出一批推荐菜品菜品详情页展示营养信息、热量、评分收藏与评价管理员端菜品、分类、标签的增删改查菜品图片建议存静态路径而不是存数据库二进制推荐规则的权重配置比如口味标签权重、价格灵敏度用户反馈管理和基础统计访问量、热门菜品排行这些功能做完整套系统的主干就有了。剩下的就是一步一步把细节填实。2. 数据库设计与MyBatis落地最容易翻车的一环2.1 表结构设计要点菜品、偏好、推荐记录怎么建做饮食推荐系统表结构我建议这样规划。核心是五张表用户表、菜品表、分类表、用户偏好表、推荐记录表再加上辅助的收藏表和评价表。用户表字段不多但有一个点要注意偏好信息不要直接塞在用户表里单独拆一张user_preference表会更灵活因为一个人的口味是会变的拆开后每次update只需要动一个字段避免频繁整行更新。菜品表是核心设计时有个容易忽略的细节。价格一定用decimal类型别用float因为浮点数在MySQL比较时会有精度问题。营养信息比如热量、蛋白质这些用int或者decimal(5,1)都可以。菜品标签这种多对多关系稍微简单一点可以用逗号分隔的varchar字段存比如辣,高蛋白,低卡查询时用FIND_IN_SET。如果数据量上来了再拆菜品标签关联表也不迟一开始别过度设计。推荐记录表的创建时间字段建议加上索引因为推荐列表一般按天查询索引能避免全表扫描。我的经验是凡是涉及按时间范围查询的表create_time都要建索引。在设计表的时候还有一个容易忽略的点字符集和排序规则。建议统一用utf8mb4因为MySQL早期utf8字符集存不了emoji菜品名、评价内容里万一出现表情符号查询就会直接报错这个坑我踩过一次。2.2 MyBatis缓存机制别稀里糊涂吃二级缓存的亏MyBatis缓存分一级缓存和二级缓存。一级缓存是SqlSession级别的默认开启同一个SqlSession里查同一条SQL会命中缓存这个没啥问题。二级缓存是namespace级别的跨SqlSession生效但用不好就是大坑。实操中我一般不建议在默认的Mapper XML里配置二级缓存除非你很清楚缓存失效策略。因为MyBatis的二级缓存在多表联查场景下有一个经典坑如果B表被A表join查询后缓存了B表数据更新时不会自动清掉跟A表相关的缓存除非你把B表自己的缓存也配了flushInterval或者trigger。这种脏数据查出来会让你查半天查不出原因。如果你确实想用缓存提升性能我建议这样搞单表查询且数据基本不变的比如菜品分类表可以开二级缓存涉及联查的、数据频繁变更的宁愿不开。还有一个更省心的方案就是把缓存交给RedisSpringBoot整合Redis缓存注解那套机制遇到复杂场景可控性远高于傻乎乎地依赖MyBatis二级缓存。2.3 分页插件PageHelper的配置和使用细节分页是这类管理系统里避免不了的功能。MyBatis手写分页比较痛苦得自己拼接limit还要查count。用PageHelper分页插件就省事很多几行配置就能用。我用的版本对应关系是SpringBoot 2.x配PageHelper 5.3.x兼容性最好。配置方式在application.yml里加上一段helper-dialect配置成mysqlreasonable设为true这样页码超过总页码时会自动回退到最后一页不会报异常。用法上有个关键细节必须注意PageHelper.startPage(pageNum, pageSize)这一行必须紧跟在你想要分页的那条Mapper查询语句之前中间不能插入其他查询逻辑。因为PageHelper是基于ThreadLocal实现的你调用startPage之后下一个执行的MyBatis查询方法才会被拦截分页。如果你在中间又查了什么分页就会作用到那条查询上出现莫名其妙的pageNum取不到数据的问题。这个是我见过最多的误用场景之一。另外告诉新手一个小技巧分页查询返回的数据用PageInfo对象包一下它能直接拿到总条数、总页数、当前页码这些信息。前端分页组件需要的数据PageInfo里基本都有不用自己再写一遍统计逻辑。2.4 慢SQL排查Update执行慢的常见原因用MyBatis注解写Update执行慢的排查思路跟普通SQL一样核心就是看索引和锁。最常见的几个原因第一update的where条件字段没建索引导致全表扫描定位行数据第二更新字段过多且表数据量大row大小太大第三事务没及时提交行锁被其他事务堵住了。我遇到过最隐蔽的一个问题是在for循环里一条一条执行update每条update耗时100毫秒导致整个接口响应超过10秒。当时第一反应是SQL写得不行其实问题出现在应用层——没有批量更新。后来改成用case when拼一条update或者用批量更新工具类速度直接提升了一个数量级。如果你排查慢SQL不知道从哪下手打开MySQL的慢查询日志把long_query_time设置成1秒然后复现一次操作看日志里记录的耗时和扫描行数。基本上扫描行数特别大的就是索引失效耗时稳定但行数少的就是锁问题。这个判断方法实测下来挺准的。3. SpringBoot后端与Vue前端联调的那些事3.1 SpringBoot配置多环境profile与版本兼容项目开发过程中本地环境和服务器环境的配置肯定不一样。数据库地址、端口、上传路径这些不可能每次打包前改一次。SpringBoot的profile机制就是干这个的我习惯把配置文件拆成三个application.yml、application-dev.yml、application-prod.yml。application.yml只放公共配置比如MyBatis的mapper-locations剩下来的环境差异放各自profile里启动时用--spring.profiles.activedev指定。版本兼容性这块差点把我折磨疯。SpringBoot选版本别一味追新start.spring.io上默认的3.x版本跟很多老一点的MyBatis starter或者PageHelper版本不一定兼容。我踩过的坑是SpringBoot 3.x配一套老版本依赖启动直接报ClassNotFoundError查了一圈发现是包里的javax被换成了jakarta命名空间。如果你不想费这个神直接选SpringBoot 2.7.x是最稳妥的方案MyBatis、PageHelper、Druid这些组件都是跟这个版本磨合过的网上教程也多。3.2 跨域与接口统一返回前后端联调的第一道坎前端开发服务器跑在localhost:8080后端跑在8081端口不一样跨域问题马上就会冒出来。解决方式有很多最简单的是在后端单独加一个WebMvcConfigurer配置类加上addCorsMappings方法允许的来源、请求头、请求方法都写上。注意allowedOriginPatterns要用pattern形式这样处理凭证的时候不容易出问题。我的建议是开发环境用后端CORS配置兜底生产环境用Nginx反向来解决这样后端代码里其实可以不放CORS配置。因为生产环境下前后端用Nginx代理到同一个域名和端口浏览器就认为同源了跨域问题根本不存在。统一返回结构这件事强烈建议从第一个接口开始就规范起来。我一般定义一个R对象包含code、message、data三个字段。code为200表示成功其他业务码由约定确定比如401未登录、403无权限。别整成功了只返回data失败了又甩一个异常堆栈给前端联调的时候你会被折磨死。最好再加一个统一异常处理RestControllerAdvice把业务异常和系统异常都拦下来统一转成R结构返回这样前端只需要按R结构解数据就行。3.3 Vue路由、状态管理与接口封装前端部分我用Vue2或Vue3都做过这个项目体量用Vue2 Vue Router 3 Vuex足够。Vue3的话用Pinia写法更简洁。重点说几个实操细节。路由配置建议按模块拆用懒加载方式component: () import(/views/menu/MenuList.vue)。懒加载之后首屏加载速度明显会快不然把所有页面一次性打进去开发时感觉不明显打包上线后首屏白屏时间就会很长。路由跳转传参这块新人容易写的里面有this.$router.push({ path: detail, params: { id: 1 } })刷新后参数直接丢失因为params参数不带进URL。正确做法是用query传递this.$router.push({ path: detail, query: { id: 1 } })。这个坑我翻过几次车后来就形成习惯了能用query绝不用params。axios封装也是前面做的。我习惯建一个request.js文件创建axios实例设置baseURL和超时时间然后加上请求拦截器和响应拦截器。请求拦截器里从localStorage取token放到headers里响应拦截器里统一判断code如果code是401就清掉本地用户信息跳回登录页。这样业务代码里基本只需要写这行axios调用剩下的拦截器都帮你处理干净了。3.4 部署方案jar包加Nginx怎么配部署这块我建议用最省事的方案后端打包成jar前端npm run build打出dist目录然后用Nginx托管前端静态资源和做反向代理。后端启动命令就是nohup java -jar system.jar --spring.profiles.activeprod日志输出到指定文件。Nginx配置有两点比较关键。第一前端路由为history模式时直接访问某个子路由会404需要在location块里加try_files $uri $uri/ /index.html;把所有找不到的路径都回退到index.html让前端路由自己接管。第二接口的/api路径要代理到后端服务地址proxy_pass http://127.0.0.1:8081;。这两个配置不写对部署上去就是要么刷新404要么接口连不上。还要提醒一点打包前检查一下后端有没有写死本地路径比如图片上传路径是C:/upload这种部署到Linux服务器上肯定起不来。建议用配置项读取上传目录在配置文件里写数据目录放data/upload下面这样迁移环境时改个配置就行。4. 推荐逻辑怎么写得有模有样4.1 基于标签的简单推荐从SQL层面实现饮食推荐系统的核心是推荐逻辑。既然面向高校学生先做基于标签和规则的推荐就够了别说协同过滤那些算法学生项目的评委和实用场景用不上那么复杂。我的数据结构里用户偏好表存的是用户的标签和权重。这怎么理解打个比方一个用户偏好辣值权重设为5偏好低卡权重设为8不喜欢甜食权重为负。菜品表里存了每道菜的标签比如水煮肉片是辣、高蛋白蔬菜沙拉是低卡、清淡。推荐的时候把用户每个偏好标签和菜品标签做匹配计算加权得分按分值排序取前20个作为今天推荐列表。这个计算在SQL里就能实现。标签存储如果用了逗号分隔可以先在SQL层面做FIND_IN_SET匹配再在Java里算加权分。菜品数量不太多时性能完全没问题。建议推荐结果不要每次暴力算完就展示把结果写进推荐记录表这样用户第二天打开找推荐页时不用重新计算直接从推荐记录表里读昨天生成的结果。而且后续要加“每日推荐量”“推荐点击率”这种统计也都有数据可用。4.2 后续可以升级的推荐方案如果做完基础版本想加点亮点可以尝试协作过滤。基于用户的协同过滤原理简单说就是找到与当前用户口味最相似的一批用户把那些用户喜欢但当前用户还没吃过的菜品推荐过来。相似度计算用余弦相似度就行数据量小可以直接在Java里算不需要引入复杂的计算引擎。另外可以加一个季节时段因子比如夏季多推荐清淡低卡类的菜品冬天多推荐热汤类高热量菜品。这种规则型的调整其实很好实现就是给菜品标签再加一个时令季节属性推荐时把季节标签的权重拉高。要提醒的是别为了推荐算法而推荐算法。这个项目的核心还是系统本身完整推荐逻辑简单、透明、可解释反而比一个调不好的黑盒算法更有说服力。评分反馈才是迭代推荐效果的关键数据把评价数据先收集起来后面优化就好办了。5. 常见问题排查与避坑实录5.1 数据库连接报错2002/时区/SSL问题MySQL连接报错是发生率最高的问题。我遇到过的情况大概这几类Linux服务器上装好MySQL后用命令连接提示ERROR 2002 (HY000): Cant connect to local MySQL server through socket。这个大概率是MySQL服务没启动或者socket路径配得不对。检查方法就是运行service mysql status看服务是不是在跑如果没跑就启动然后再连。还有一类连接报错和驱动参数有关SpringBoot连接MySQL时报时区错误或SSL告警。解决办法就是在JDBC连接串上加上useSSLfalse和serverTimezoneAsia/Shanghai。URL里不写这些参数MySQL 8的驱动默认会用本地时区和服务器时区对不上数据时间就差8小时排查起来一头雾水。数据库连接池我用的Druid配置里有个initialSize和maxActive新手容易把maxActive设得太大比如200结果服务器只有2G内存启动时CPU直接飙满。内存不充裕的服务器建议maxActive设到20-30就够了一个学生项目并发根本到不了那个量级。5.2 Maven依赖冲突与版本太高的坑项目编译没问题启动的时候报一堆NoClassDefFoundError或BeanCreationException十有八九是依赖冲突。排查方法其实不难。在IDEA的控制台或者执行mvn dependency:tree看依赖树找有没有同一个包被引入了不同版本。常见冲突点是Druid和MyBatis starter里都带了logback或者slf4j相关包版本不一致就会导致启动时日志类加载失败。解决方式是全局排除掉重复的传递依赖保留自己指定的版本。关于版本太高这个问题SpringBoot的版本选择我之前说过别一上来就选最新的3.x。很多老教程的依赖版本是按2.x写的混着用很容易踩坑。我的建议是要么全套用SpringBoot 3.x MyBatis官方starter 新的JDBC驱动要么全套用2.x的老牌组合。最怕就是混搭比如SpringBoot 3.x加上一个为2.x设计的旧分页插件启动时不报错一查数据就炸。5.3 前端打包后404、路由参数丢失问题前端问题里最高频的是这两个一是history模式打包部署后刷新页面或者在地址栏直接输入子路由链接页面404。前面讲过Nginx加try_files配置就能解决。如果你用的是二级路径部署还得改vue.config.js里的publicPath和路由的base这两处不配好资源路径就会错乱白屏加控制台报资源找不到。第二个是路由参数丢失。用params传参然后刷新页面数据不见的问题前面讲过改用query。还有一个细节是query传对象时刷新后会变成[object Object]因为对象被自动toString了。想传对象要么用JSON.stringify字符串传过去再parse回来要么就配合状态管理存一份。我之前图省事直接传对象结果刷新之后详情页崩了后来补了个fallback机制才稳下来。5.4 其他高频报错速查表我把另外几个常见的报错和解决办法整理成一个小表方便你排查。SpringBoot项目启动报Port 8080 was already in use服务器上之前有一个进程占用了端口用netstat -tulnp | grep 8080找到进程号kill掉或者改端口启动。MyBatis提示org.apache.ibatis.binding.BindingException: Invalid bound statementMapper接口和XML文件没对应上。检查XML的namespace是不是接口全限定名检查application.yml里mapper-locations路径是否匹配classpath*:mapper/*.xml。前端请求接口报401token过期或者没带token去请求拦截器重新登录拿新token。接口返回500并提示DataIntegrityViolationException一般是数据库字段有非空约束前端没传对应字段。看异常信息里卡在哪一列补上就行别傻乎乎的去清表数据。上传图片后访问404上传目录没有配置静态资源映射。后端要配置一个虚拟路径映射到本地上传目录SpringBoot里就是写个WebMvcConfigurer的addResourceHandlers方法。排查这些问题的通用思路我的经验是按顺序看先看后端控制台有没有完整异常堆栈再看前端network面板请求的状态码和响应体最后看MySQL的日志。大多数同学第一步就漏了去看数据库里有没有数据其实根本问题在改了表结构没同步XML。另外再分享一个调试MyBatis的小技巧在application.yml里配置mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样MyBatis执行每一条SQL和参数都会打印到控制台。把打印出来的SQL复制到Navicat里跑一遍就能快速确认到底是SQL写错还是数据有问题。这个在排查数据对不上的场景里很好用。说了这么多其实做这个项目的正确节奏就是先把业务表设计好再把接口文档草稿写出来前端页面按接口来开发。我见过太多同学把前后端完全分开做最后联调时才发现接口字段对不上来回扯皮好几天。前后端最好并行但接口的字段名、类型一定要提前约定清楚哪怕只是写在Excel里也比没有强。这套系统做完你不仅把SpringBoot和Vue的流程跑通了下一回碰到类似的业务基本就是套这个框架换套皮的事。
返回列表