ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis+MySQL前后端分离厨艺交流平台开发实战

SpringBoot+Vue+MyBatis+MySQL前后端分离厨艺交流平台开发实战 前后端分离厨艺交流平台这名字听着挺接地气但真要把菜谱发布、图片上传、评论互动、用户系统这些功能完整跑通涉及的技术点一点不比电商系统少。我前前后后帮朋友搭过几个类似的内容社区项目踩过不少坑下面把这套SpringBootVueMyBatisMySQL的完整实现思路、核心代码逻辑和部署细节一次性讲透想照着做一套属于自己的厨艺分享站点的朋友可以直接抄作业。1. 项目整体设计与架构拆解1.1 为什么选择前后端分离先说结论内容社区类项目前后端分离是当前性价比最高的方案没有之一。厨艺交流平台的核心场景是用户发菜谱、看菜谱、评论互动这意味着前端需要频繁处理列表刷新、图片预览、分页加载这类交互密集型操作。如果走传统的服务端渲染每次点击都要刷新整页体验会非常糟糕。前后端分离之后Vue负责页面渲染和用户交互后端只提供JSON数据接口两边各干各的开发和调试都清爽很多。开发效率上的优势更明显。前端工程师只需要盯着Vue组件和接口文档后端工程师专注SpringBoot的Service和Mapper层两个人可以并行开发只要提前约定好接口格式就行。我一个朋友的公司团队就是这么干的后端写接口的同时前端用Mock数据先画页面等接口好了直接替换整个项目周期比原来快了将近三分之一。部署层面也灵活。前端打包成静态文件丢到Nginx后端打成Jar包独立运行哪里出问题排查哪里互不干扰。后续如果想把图片服务拆出去单独做CDN加速架构上也留有余地。1.2 技术栈选型的真实理由这套技术栈不是随便拼的每个组件都有它不可替代的位置SpringBootJava后端开发的事实标准。它解决了Spring框架最头疼的XML配置问题内嵌Tomcat一个java -jar命令就能跑起来。版本选型上我建议用SpringBoot 2.7.x稳定且生态兼容性好。我最早用过2.3后来升过3.0说实话3.0的Jakarta命名空间迁移对新手不太友好很多老教程直接跑不通2.7是目前最稳的一个版本。Vue前端框架里上手曲线最平缓的。厨艺交流平台这种项目页面主要是列表、详情、表单三类Vue的单文件组件配合Vue Router就能很漂亮地组织代码。Vue 3的Composition API我强烈建议直接用代码复用逻辑比Options API舒服太多。MyBatis半自动ORM的核心价值在于你可以完全掌控SQL。厨艺平台的查询场景极其复杂按菜系分类、按食材标签筛选、按发布时间排序、关键词模糊搜索、关联查询出作者和评论数。这些SQL写MyBatis的XML里想怎么优化就怎么优化完全不受框架限制。用JPA在这种场景下反而会因为复杂的动态查询而变得捉襟见肘。MySQL这个体量的项目用MySQL 8.0完全够用。InnoDB引擎支持事务处理用户注册、菜谱发布这些写入场景有保障。配合Navicat或者命令行工具做数据管理都很方便。注意前后端分离不是说前后端完全割裂接口契约一定要提前定死。我自己习惯用Postman建一个共享的接口集合每次改接口都同步更新最大程度避免后端以为返回的是A前端以为收到的是B这种经典事故。1.3 功能模块划分与核心业务链路一个合格的厨艺交流平台至少要包含这几个模块用户模块注册、登录、个人信息管理。登录推荐用JWT做无状态认证后端签发Token前端每次请求把Token塞在Header里。菜谱模块菜谱发布含图片上传、菜谱列表分页浏览、菜谱详情展示、按分类/关键词检索。互动模块评论、收藏、点赞。这三个互动行为是社区活跃度的核心指标。分类模块菜系分类和标签分类比如川菜、粤菜、烘焙、家常菜等。核心业务链路其实很清晰用户登录后进入菜谱广场按分类浏览或直接搜索点进详情页后可以收藏或发表评论用户自己也可以进入个人中心发布新菜谱。这条链路覆盖了内容生产、内容消费、内容互动三个环节对于入门级前后端分离项目来说业务复杂度刚刚好。2. 数据库设计与关键表结构2.1 核心表设计与关系分析数据模型决定了业务的上限建表之前一定要想清楚表之间的关系。我设计的核心表一共四张外加两张关联表用户表 user字段名类型说明idbigint主键自增usernamevarchar(50)用户名唯一索引passwordvarchar(100)加密后的密码BCryptnicknamevarchar(50)昵称展示用avatarvarchar(255)头像路径create_timedatetime注册时间菜谱表 recipe字段名类型说明idbigint主键user_idbigint发布者ID外键关联用户表titlevarchar(100)菜谱标题cover_imagevarchar(255)封面图路径descriptiontext菜谱简介ingredientstext食材清单JSON字符串存储stepstext步骤描述JSON字符串存储category_idbigint分类IDview_countint浏览数like_countint点赞数create_timedatetime发布时间分类表 category字段名类型说明idbigint主键namevarchar(50)分类名称sortint排序权重评论表 comment字段名类型说明idbigint主键recipe_idbigint关联菜谱IDuser_idbigint评论人IDcontentvarchar(500)评论内容create_timedatetime评论时间收藏关系我刻意用了一张独立关联表而不是在菜谱表里加is_collected字段。原因很简单收藏行为是用户维度的状态对同一道菜用户A收藏了但用户B没收藏如果用单个字段就没法表达。这种多对多关系拆一张表出来查询和维护都干净。CREATE TABLE recipe_favorite ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, user_id bigint NOT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_recipe_user (recipe_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;recipe_id和user_id建联合唯一索引防止用户重复收藏。这个细节很重要不然前端连续点两次收藏按钮数据库里就会出现两条脏数据。2.2 建表SQL的关键细节字符集一定要用utf8mb4而不是utf8。MySQL的utf8实际只能存3字节的字符而现在用户昵称和评论里经常出现Emoji表情比如‍这类3字节根本存不下直接报错或者乱码。utf8mb4是完整的4字节编码兼容所有Unicode字符这是我在生产环境踩过最痛的一个坑当初一群人的昵称全变成问号排查了很久才发现是字符集的问题。时间字段推荐用datetime而不是timestamp。timestamp有2038年上限的问题而且会受时区影响datetime的存储范围更宽语义更直观。如果后续项目要做多时区支持可以用datetime加bigint毫秒值双保险。所有表的id都用BIGINT而不是INT。对于内容社区这种用户量增长潜力大的场景INT的上限21亿看着够用但一旦上了缓存、做了读写分离主键的生成策略就会变复杂。BIGINT配合MyBatis Plus的雪花算法或数据库自增都能无缝切换。外键我建议只在逻辑层面维护不在物理层面建真正的FOREIGN KEY。物理外键在数据量上来之后会影响写入性能而且做分库分表的时候外键会变成绊脚石。业务层代码里通过查询关联来保证数据一致性完全够了。3. 后端SpringBootMyBatis实战3.1 后端项目结构与分层我习惯的分层是经典的Controller-Service-Mapper三层src/main/java/com/cookblog ├── controller # 接口层接收请求、返回结果 ├── service # 业务逻辑层 ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体 ├── dto # 传输对象前后端交互的数据结构 ├── config # 配置类跨域、拦截器等 └── common # 通用工具与统一返回结果Controller层要薄只做参数接收和结果封装业务逻辑全下沉到Service层。很多人刚写后端容易把SQL拼接写在Controller里短期能跑后期维护就是噩梦。比如发布菜谱需要同时插入菜谱表和更新用户菜谱数这个操作拆成Service里的事务方法后任何一个步骤失败都能整体回滚。统一返回结果是绝对不能省的。我定义一个ResultT类包含code、message、data三个字段所有接口都返回这个结构。前端Axios里做一次统一拦截判断code是否为200不是的话直接弹出错误提示。这个习惯能让你少写无数个if-else判断成功失败的分支。3.2 核心接口设计与实现菜谱列表接口是核心中的核心它要同时支持分页、分类筛选、关键词搜索、排序。我用一个RecipeQueryDTO接收查询参数通过MyBatis的动态SQL拼查询条件Data public class RecipeQueryDTO { private Integer page 1; private Integer pageSize 10; private Long categoryId; private String keyword; private String sortBy; // latest / hot }Mapper接口ListRecipeVO selectRecipeList(Param(query) RecipeQueryDTO query); Long countRecipeList(Param(query) RecipeQueryDTO query);对应的XML核心片段select idselectRecipeList resultTypecom.cookblog.dto.RecipeVO SELECT r.id, r.title, r.cover_image, r.view_count, r.like_count, r.create_time, u.nickname AS author_name FROM recipe r LEFT JOIN user u ON r.user_id u.id where if testquery.categoryId ! null AND r.category_id #{query.categoryId} /if if testquery.keyword ! null and query.keyword ! AND (r.title LIKE CONCAT(%, #{query.keyword}, %) OR r.description LIKE CONCAT(%, #{query.keyword}, %)) /if /where ORDER BY choose when testquery.sortBy hotr.like_count DESC/when otherwiser.create_time DESC/otherwise /choose LIMIT #{query.pageSize} OFFSET #{query.pageSize} * (#{query.page} - 1) /select这里有两个关键细节。第一分页查询必须写两个SQL一个查列表一个查总数。原因是MySQL执行COUNT和SELECT同样走一遍索引如果只在列表SQL里用SQL_CALC_FOUND_ROWS数据量大时性能差别明显。第二排序字段不能直接用前端参数拼接必须用choose做白名单映射防止SQL注入。sortBy传什么进来都由后端决定最终拼哪个列这个习惯在接第三方接口时尤其重要。菜谱详情接口要一次性把菜谱信息、作者信息、评论列表、收藏状态都返回给前端避免前端发四五次请求。我用一个聚合查询在Mapper里直接LEFT JOIN三张表配合一个is_favorite子查询判断当前登录用户是否已收藏。一次HTTP请求搞定所有事情前端组件渲染起来也非常顺畅。图片上传建议单独做一个接口接收MultipartFile存到服务器本地磁盘或者OSS数据库里只存访问路径。千万不要把图片存成Base64字符串塞进MySQL一个长宽正常的菜品照片转成Base64之后能有几百KB数据库瞬间变成巨型文件存储性能直接崩。3.3 MyBatis配置要点与缓存机制MyBatis的配置主要分两部分全局配置和Mapper映射。全局配置里我开了驼峰映射和日志mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xmlmap-underscore-to-camel-case这个配置必须开。数据库字段是create_timeJava实体是createTime开了这个映射后MyBatis自动转换不然你得在XML里给每一列写as xxx别名写到你手酸。日志打印也建议在开发阶段开着每条SQL的执行语句和参数都会直接输出控制台排查MyBatis映射问题的时候这个是最直接的线索。我曾经见过一个字段映射错位查出来的用户昵称和头像完全对不上就是靠SQL日志一眼定位到的。MyBatis缓存默认是开启一级缓存的也就是同一个SqlSession内的相同查询会命中缓存。但这里要特别提醒跨SqlSession的二级缓存我默认关闭。原因是厨艺平台是读写频繁的社区系统数据实时性要求高而MyBatis自带的二级缓存基于Ehcache或Redis实现缓存失效策略比较粗糙很容易出现用户发了新菜谱别人刷新后还是看不到的尴尬情况。真要做缓存建议在Service层引入Redis做手动缓存控制精确度会高很多。4. 前端Vue完整落地4.1 前端项目结构与路由设计前端我用Vue 3 Vite构建项目结构按功能模块划分src ├── api # 接口请求模块 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── stores # Pinia状态管理 ├── views # 页面组件 │ ├── HomeView.vue │ ├── RecipeList.vue │ ├── RecipeDetail.vue │ ├── PublishView.vue │ └── ProfileView.vue └── utils # 工具函数axios封装等路由设计用了动态路由配合懒加载。对于菜谱详情这类页面路径设计为/recipe/:id组件内部通过route.params.id发起详情请求。懒加载用() import(../views/RecipeDetail.vue)Vite会自动按路由拆分代码块首页加载速度会明显更快实用价值很大。路由守卫里我做了一个登录校验需要登录的页面发布菜谱、个人中心挂上meta: { requiresAuth: true }在全局前置守卫里检查本地Token是否存在不存在就router.push(/login)。这个方式比在每个组件里写if判断要优雅得多而且后续加页面只需要在路由表里标记一个字段即可。4.2 Axios封装与统一请求处理Axios封装是前后端分离项目前端的必做事项。我的封装思路是写一个request.js导出已经配置好拦截器的axios实例所有API接口都基于这个实例调用。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这套封装解决三个核心痛点。第一Token自动注入不用每个请求手动写Headers第二后端返回的统一结果结构被剥壳接口调用的地方直接拿到data不用再写res.data.data这种丑陋代码第三401状态码统一踢回登录页防止用户在登录失效后还在页面里反复点。前端接口模块单独放API文件夹比如src/api/recipe.js里集中管理所有菜谱相关请求。好处是后端接口一旦调整你只需要改一个文件前端组件完全不受影响。这是一层很必要的防腐层很多新人在没体会到接口变更的痛苦之前都喜欢在组件里散落着写下axios.get等后端改字段名的时候才会悔不当初。4.3 核心页面与组件实现菜谱列表页是平台的门面。我推荐用卡片流布局每张卡片展示封面图、标题、作者昵称、点赞数点击进入详情。列表分页用触底加载的方式实现这种交互对移动端也友好。每次触底时把page加1调用后端接口取下一页数据追加到现有列表尾部const loadMore async () { if (loading.value || noMore.value) return loading.value true const params { page: page.value, pageSize: 10 } if (categoryId.value) params.categoryId categoryId.value if (keyword.value) params.keyword keyword.value const list await getRecipeList(params) recipeList.value.push(...list) page.value if (list.length 10) noMore.value true loading.value false }这个写法里没有复杂的边界处理但loading和noMore两个标志位能很好地防止重复请求和无效请求。如果不加这两个判断快速滚动时loadMore可能被触发十几次接口被打爆是小事数据重复才是大问题。菜谱发布页是互动环节的核心。我做了动态表单食材清单用v-for渲染一组输入框支持添加和删除行步骤描述用文本域配合序号展示。表单校验用的是Element Plus的Form组件规则标题必填、食材至少一条、步骤至少一条这些规则在发布前就拦住大部分无效提交。图片上传组件我封装了一个子组件内部处理图片预览和上传进度。上传后组件把返回的图片路径通过v-model回传给父表单这样用户能看到即将发布的预览效果体验比单纯的上传URL要好很多。上传过程中要显示进度条要用Axios的上传进度事件onUploadProgress来更新进度数据这个细节很容易被遗漏。4.4 前后端联调中的经典坑联调阶段有个高频坑跨域问题。前后端分离项目里前端运行在http://localhost:5173后端运行在http://localhost:8080浏览器会拦截跨域请求。解决办法有两种。开发阶段最简单的是在Vite配置文件里设置代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }注意这里配合了Axios里baseURL: /api的路径前缀开发环境所有请求都走Vite代理转发到后端完全规避跨域。生产环境则靠Nginx做反向代理把/api前缀转发到后端服务。这种方式的好处是前端代码里不需要区分环境部署时只需保证代理配置正确。后端也要同时放开SpringBoot的跨域配置作为兜底。我在config包里写一个CorsConfig用WebMvcConfigurer注册跨域映射允许本地开发地址访问。这样做的好处是即使有人绕过Nginx直连后端接口也不会被浏览器拦截降低排查噪音。5. 部署上线与常见问题排查5.1 本地启动全流程本地开发启动的顺序有讲究乱序会出现各种莫名其妙的连接错误。第一步装MySQL。我用的是MySQL 8.0Windows下安装时记得选utf8mb4字符集root密码设置后要记牢。安装完后用命令行或Navicat执行项目里的init.sql脚本初始化数据表。这里插一句MySQL安装后很多人会遇到客户端不支持服务器请求的身份验证协议的报错。原因是你本地装的MySQL 8.0默认用caching_sha2_password认证而某些老版本驱动不支持。解决办法要么换最新的MySQL驱动包要么在MySQL里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。我建议直接换驱动兼容性才好。第二步启动后端。用IDEA打开后端工程等Maven把依赖下载完。先检查application.yml里的数据库账号密码是否匹配然后运行主类。启动成功后控制台会打印Tomcat启动的端口默认8080。第三步启动前端。用VSCode打开前端工程执行npm install装依赖再执行npm run dev启动Vite开发服务器。看到终端输出Local: http://localhost:5173/后浏览器打开这个地址就能访问到首页。三个服务前端开发服务器、后端SpringBoot、MySQL缺一不可每次启动前先确认MySQL服务在运行别等到页面打不开才想起来数据库没启动。5.2 生产环境打包与Nginx部署前后端分离项目上线部署的完整姿势是前端打成静态文件用Nginx托管后端打成Jar包用java命令启动MySQL用系统服务独立运行。前端打包npm run build打包完成后在dist目录下生成一堆静态文件。把这些文件直接扔到服务器的/var/www/cookblog目录里。Nginx配置server { listen 80; server_name your-domain.com; root /var/www/cookblog; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个Vue路由模式的坑。如果你的路由用的history模式刷新一个非首页的路径比如/recipe/3如果Nginx没有try_files $uri $uri/ /index.html;这行配置服务器会返回404。原理是Vue Router的history模式把路由交给了前端处理后端实际上没有这个路径必须把所有请求都引导回index.html由前端路由接管。后端启动java -jar cookblog-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生产环境我建议用prod配置文件把日志输出到文件数据库连接池调大关闭MyBatis的SQL日志。Jar包直接丢在服务器上配合systemd或supervisor做进程守护进程挂掉能自动拉起大半夜不至于被报警惊醒。5.3 高频问题速查表现象原因解决方案http://localhost:5173 页面白屏后端没启动或启动报错先看后端控制台日志确认8080端口可访问404 错误接口找不到代理配置错误或后端接口路径不一致检查Vite proxy和后端RequestMapping路径是否匹配请求状态码 401Token失效或未登录刷新页面重新登录检查拦截器逻辑数据库连接失败MySQL服务未启动或密码错误检查application.yml里的数据库配置中文乱码数据库字符集不对ALTER DATABASE xxx CHARACTER SET utf8mb4;上传图片后无法访问静态资源映射缺失后端配置spring.web.resources.static-locations页面刷新404Nginx没配try_files在location中增加try_files配置MyBatis报Invalid bound statement (not found)怎么办。这个报错可以说是新手高频噩梦。原因通常是Mapper接口和XML文件没有在同一个包路径下或者XML里的namespace写错了。检查你自己的MapperScan注解和mapper-locations配置是否对应XML文件有没有被Maven编译进target/classes目录。我自己的习惯是新建Mapper接口后第一时间创建同名XMLnamespace直接复制接口全限定名减少手抖犯错的可能。部署时MySQL连接报SSL错误怎么办。MySQL 8.0默认启用SSL但有的服务器上没有配置证书连接时报Communications link failure。解决方法是JDBC连接串里加个参数url: jdbc:mysql://localhost:3306/cookblog?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4这三个参数几乎是必加的。useSSLfalse跳过SSL检查serverTimezone避免时区报错characterEncoding保证中文正确。很多本地能跑、部署到服务器就各种报错一半以上是连接串这三个参数没配齐。我个人在实际项目中的体会是前后端分离项目的难不在某个单一技术上而在接口契约的一致性和环境的完整性。SpringBoot、Vue、MyBatis、MySQL每一个单独拿出来都不算难难的是把四个组件串起来形成一套稳定可运行的系统。做这个厨艺交流平台时我最大的收获不是写代码的速度变快了而是建立了一套从数据表设计、接口定义、前端联调到服务器部署的完整工程化思维。建议你自己动手复现的时候先按数据库设计、后端接口、前端页面的顺序推进哪里卡住就回到接口文档重新对齐这个项目的调试经验会迁移到未来很多类似的Web项目中。最后分享一个小技巧开发阶段前端和后端的控制台日志一定要保留且开详细级别很多联调问题例如JSON字段拼写错误、返回类型不匹配本质上是看不到数据导致的。把后端MyBatis的SQL日志和前端Axios的请求日志同时打开两边一对照问题基本上三分钟内就能定位。这个习惯我从做这个项目保持到现在省下的排查时间非常可观。
返回列表