news 2026/10/10 5:32:51

全栈实战:SpringBoot+Vue3+MyBatis+MySQL厨艺平台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
全栈实战:SpringBoot+Vue3+MyBatis+MySQL厨艺平台

最近帮一个朋友搭了一套“厨艺交流平台”,技术栈正好就是标题这串关键词:Java SpringBoot + Vue3 + MyBatis + MySQL,前后端分离,源码跑通之后又给他整理了完整文档。这类型项目在毕设、个人作品集、外包单里出现频率极高,但很多拿到源码的人其实卡在“不知道整体怎么串联”上:后端接口怎么暴露?前端怎么请求?数据库的几张表之间怎么关联?今天我就以一套可运行的厨艺交流平台为例,把从设计到联调部署的关键环节完整拆一遍,顺带把平时容易踩的坑也一并说清楚。

先说清楚这套东西能做什么:一个用户注册登录后,可以发布自己的拿手菜,浏览别人的菜谱,按分类找菜,对感兴趣的菜谱进行点赞、收藏和评论,还可以进入个人中心管理自己发布的内容。整体就是一个小而完整的社区型业务闭环。对于正在学后端开发、准备面试、或者需要交付一个完整全栈项目的朋友,这套源码和拆解思路都有不错的参考价值。整个系统不依赖第三方重型组件,一台装了 JDK、Maven、Node.js 和 MySQL 的电脑就能跑起来。

1. 项目整体设计与技术选型

1.1 为什么是SpringBoot + Vue3 + MyBatis

选这套组合不是因为“流行”,而是它在中小型业务系统里最平衡。SpringBoot 负责把后端服务跑起来,内置 Tomcat,把配置简化到极致;MyBatis 负责和数据库打交道,SQL 由你控制,比 JPA 更直观,排查问题也简单;Vue3 配合 Vite 带来的开发体验非常顺,组件化写法写页面也清晰。前后端分离的核心在于:后端只出 JSON 数据,前端通过 HTTP 请求拿数据渲染页面,两者通过接口约定协作,互不干扰。

有人会问:为什么不用 JSP 做服务端渲染?因为现在的前端交互复杂度已经很高了,尤其是厨艺社区这种有多页面、评论、点赞、用户中心操作的应用,分离式开发还能让前端和后端各自动态发布,部署也更灵活。后端只管 API,前端可以是网页,可以之后套小程序或者 App,这一点非常重要。

1.2 数据库设计:几张核心表撑起整个业务

这套厨艺交流平台的业务不复杂,但表结构设计直接影响后续开发效率。我习惯先梳理核心实体:用户、菜谱、分类、评论、收藏、点赞。对应的 SQL 表如下:

表名说明核心字段
user用户表id, username, password, nickname, avatar, create_time
category分类表id, name, sort
recipe菜谱表id, user_id, category_id, title, cover, description, steps, ingredients, view_count, create_time
comment评论表id, recipe_id, user_id, content, create_time
favorite收藏表id, user_id, recipe_id, create_time
like_record点赞表id, user_id, recipe_id, create_time

注意 recipe 表的 steps 和 ingredients,我建议直接用 TEXT 类型存结构化文本,比如 JSON 数组字符串。这样前端拿到后解析就能渲染步骤列表和用料清单,避免为子数据再建多张表。对于这种轻量级平台完全够用,也比做三张关联表更省事。

另外点赞表和收藏表都做了唯一约束,比如UNIQUE KEY uk_user_recipe (user_id, recipe_id),防止重复数据。数据库默认使用 InnoDB 引擎,utf8mb4 字符集,因为菜谱描述里常有人输入 emoji 表情,utf8mb4 才存得下。

1.3 后端项目结构:分包清晰比炫技重要

后端我用 Maven 构建,工程结构按照传统的 controller / service / mapper / entity 分层。

src/main/java/com/cookhub/ ├── controller # 接收前端请求,返回 JSON ├── service # 业务逻辑层,处理业务规则 ├── mapper # MyBatis 接口,定义数据库操作方法 ├── entity # 数据库实体类 ├── dto # 数据传输对象,接口参数和返回体 ├── config # 配置类,拦截器、跨域等 ├── common # 统一返回结果、异常处理、工具类 └── CookHubApplication.java

这种结构的核心好处是每一层只干一件事。controller 里不做业务判断,service 里不拼 SQL,mapper 里不写业务逻辑。很多人把项目写乱就是因为 controller 里堆了太多代码,最后改需求时牵一发动全身。分层清晰之后,团队协作和后续扩展都很方便。

2. 后端核心实现与关键细节

2.1 SpringBoot 工程搭建与依赖配置

创建工程可以到 Spring Initializr 生成,也可以直接手工建 Maven 项目。我建议直接使用 SpringBoot 2.7.x 或 3.x。如果使用 JDK8,稳妥选择 2.7;如果使用 JDK17,选 3.x。下面给出核心的pom.xml依赖片段:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

SpringBoot 3.x 需要把mybatis-spring-boot-starter换成 3.0 以上版本,而且不能再直接使用spring-boot-starter-web里的旧 API,这个要特别注意,后面常见问题里再展开。配置写在application.yml:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cookhub?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.cookhub.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

map-underscore-to-camel-case这个配置必须要开,它能把数据库的create_time自动映射成实体里的createTime,省掉大量手写 resultMap 的功夫。log-impl在调试阶段可以打印 SQL,正式环境记得去掉,避免日志刷屏。

2.2 MyBatis XML 映射与动态 SQL

接口和 XML 分开是 MyBatis 常见做法,尤其对于稍微复杂一点的查询,比如菜谱列表需要关联分类名称、用户昵称、点赞数、收藏数,用注解拼 SQL 会非常痛苦。我在RecipeMapper.xml里写了一个用于分页查询的 SQL:

<select id="selectRecipePage" resultType="com.cookhub.dto.RecipeListDTO"> SELECT r.id, r.title, r.cover, r.description, c.name AS categoryName, u.nickname AS authorName, r.view_count, COUNT(DISTINCT lr.id) AS likeCount, COUNT(DISTINCT fr.id) AS favoriteCount FROM recipe r LEFT JOIN category c ON r.category_id = c.id LEFT JOIN user u ON r.user_id = u.id LEFT JOIN like_record lr ON r.id = lr.recipe_id LEFT JOIN favorite fr ON r.id = fr.recipe_id <where> <if test="categoryId != null"> AND r.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (r.title LIKE CONCAT('%', #{keyword}, '%') OR r.description LIKE CONCAT('%', #{keyword}, '%')) </if> </where> GROUP BY r.id, r.title, r.cover, r.description, c.name, u.nickname, r.view_count ORDER BY r.create_time DESC </select>

这个 SQL 里的LEFT JOIN很重要,因为用户未点赞、未收藏时,关联表里没有记录,如果使用INNER JOIN会直接把这些菜谱丢掉。GROUP BY配合COUNT(DISTINCT ...)可以避免点赞和收藏两个一对多关系叠加后产生的重复行。很多人第一次写这种统计查询会漏掉 DISTINCT,结果一个赞被数成好几个,数据看着很离谱。

2.3 用户登录鉴权与密码安全处理

用户模块是整个系统的入口。密码绝对不允许明文存库,我用的方案是 MD5 加盐。虽然有人觉得 MD5 不够安全,但作为学习项目完全够用,生产环境可以用 BCrypt,但为了简洁这里我演示一下加盐思路:

public static String encrypt(String password, String salt) { String source = password + salt; return DigestUtils.md5DigestAsHex(source.getBytes(StandardCharsets.UTF_8)); }

注册时为用户生成一个随机盐值,例如 UUID 前 8 位,然后将password + salt加密后存入数据库。登录时从数据库查出盐值,再对输入的密码做同样加密,比对结果。登录成功后,后端使用 Session 保存用户状态,并通过 HTTP Only Cookie 把 SessionID 交给浏览器。这种方式在前后端分离下也适用,只要前端请求携带 Cookie 即可。

我额外加了一个LoginInterceptor,统一拦截需要登录才能访问的接口。比如发布菜谱、点赞、收藏、评论这些操作,如果 Session 里没有用户信息,就返回 401 和统一提示信息。拦截器配置里要注意放行登录注册和首页菜谱列表等公开接口,否则前端一进来就会被拦,闹出“首页都打不开”的笑话。

2.4 文件上传与图片存储方案

菜谱封面图是刚需。最简单的方案是上传到本地磁盘,然后在数据库里存访问路径。SpringBoot 里配置一个虚拟路径映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + System.getProperty("user.dir") + "/upload/"); } }

前端上传文件时,后端接口用MultipartFile接收,生成新的文件名防止路径冲突,使用 UUID 加原始扩展名。文件保存后返回给前端一个相对路径,比如/upload/202501/xxxx.jpg。前端保存菜谱时把这个路径写入 recipe 表的 cover 字段。注意文件上传目录最好放在项目根目录之外,否则重新打包或者版本更新时可能被清掉,我一般单独建一个upload目录,并在配置里指定。

这种方式的好处是不需要额外引入对象存储服务,本地跑通成本最低。如果准备部署到云服务器,之后可以平滑换到 OSS 或 MinIO,只需改文件存储的工具类,前端路径约定保持不变。

3. 前端 Vue3 开发与接口对接

3.1 Vite 初始化项目与工程结构

前端使用 Vue3 配合 Vite,比 Vue CLI 快很多。初始化命令:

npm create vite@latest cookhub-web -- --template vue cd cookhub-web npm install npm install vue-router@4 pinia axios element-plus

我用的是 Vue Router 4 做路由,Pinia 做状态管理,Element Plus 做 UI 组件库。工程结构如下:

src/ ├── api/ # 接口请求函数 ├── assets/ ├── components/ # 通用组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态存储 ├── views/ # 页面组件 ├── utils/ # 工具函数 └── App.vue

在厨艺平台里,我最常使用的是 Element Plus 的卡片、分页、表单和消息提示。比如菜谱广场用el-card展示每一道菜,列表下方用el-pagination做分页。这里有个经验:分页参数要和后端约定好,我习惯用pageNum和pageSize,返回体带total总数。后端封装一个PageResult对象,前端page变化时重新请求接口,逻辑非常清晰。

3.2 基于 Axios 的请求封装与统一异常处理

前端每个页面都去写fetch会很繁琐,我用 Axios 封装了一个公共请求模块,统一配置 baseURL、超时时间、请求拦截器、响应拦截器。

import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res.data }, error => { ElMessage.error(error.response?.data?.msg || '网络异常') return Promise.reject(error) } ) export default request

前后端统一约定返回体格式:{ code: 200, msg: "success", data: ... }。后端使用Result类封装所有接口响应,前端拿data直接用,出错了统一弹提示。这种约定能避免前端每个人写不同的处理逻辑,非常关键。

跨域问题在开发阶段通过 Vite 的 proxy 解决。在vite.config.js里:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端请求/api/recipe/list,会被代理到后端http://localhost:8080/api/recipe/list。生产部署时再用 Nginx 做反向代理。跨域的坑我后面详细说。

3.3 核心页面拆解:菜谱广场、详情页、个人中心

菜谱广场页面是用户第一眼看到的界面,核心是筛选和列表。顶部分类 tab,点击切换到对应分类;中间搜索框按关键词查询;下面菜谱卡片展示封面、标题、作者、点赞数。页面加载时调用getRecipePage接口,参数带上pageNum、pageSize、categoryId、keyword。查询结果通过reactive对象维护,分页变化时重新拉数据。

详情页是核心交互页面。除了展示菜谱基本信息,还要展示用料清单、步骤列表、评论区。这里我用了三个 Tab 来切换“用料”“步骤”“评论”,避免页面过长。页面初始化时同时请求菜谱详情、点赞状态、收藏状态。注意点赞和收藏按钮的状态需要登录后才能判断,所以接口里返回liked和favorited两个布尔值,后端根据当前登录用户查询。前端拿到状态后高亮按钮,点击时调用点赞或取消点赞接口,用乐观更新策略,先改前端状态再请求接口,请求失败再回滚,体验更跟手。

个人中心展示用户发布的菜谱列表和收藏列表。这里需要注意:接口必须从 Session 中获取当前用户 ID,而不是让前端传用户 ID,否则任何人都可以看别人的私密数据。后端代码里不要信任前端传的 userId,关键数据要从登录态中取。

4. 前后端联调与部署上线全流程

4.1 本地开发环境配置与联调

开发环境我推荐手动同时启动前后端。后端直接用 IDEA 启动,前端在cookhub-web目录下执行npm run dev,然后访问http://localhost:5173。前后端联调最容易出问题的是接口路径不一致。我一般会在后端 Controller 里统一加上/api前缀,然后前端 proxy 也匹配/api,这样就保证开发环境和生产环境路径一致。以下是一个典型的 Controller:

@RestController @RequestMapping("/api/recipe") public class RecipeController { @GetMapping("/list") public Result<PageResult<RecipeListDTO>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Integer categoryId, String keyword) { return Result.success(recipeService.queryPage(pageNum, pageSize, categoryId, keyword)); } }

联调阶段,我习惯在后端配置里打开 MyBatis SQL 日志,这样前端一旦请求,控制台就能看到实际执行的 SQL。如果返回数据不对,直接定位是 SQL 错了、参数没传、还是返回结构不对,效率高很多。

4.2 打包部署方案:前端 dist 放入后端

前后端分离项目最常见的有两种部署方式。一种是用 Nginx 托管前端静态资源,同时反向代理后端接口;另一种是更简单的单服务方案:把前端npm run build生成的dist目录放进 SpringBoot 的src/main/resources/static目录下,然后只启动一个 8080 端口。这样访问http://ip:8080就能看到页面,后端接口路径仍然是/api/**。

我推荐学习阶段用第二种,部署简单,不容易出跨域问题。把dist拷入 resources 后需要重新打包。注意前端资源里如果有绝对路径,可能需要调整vite.config.js的base: './',否则打包后资源路径找不到。如果用第一种 Nginx 方案,配置核心是 location 块:

location / { root /var/www/cookhub; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; }

这个配置里try_files是为了配合 Vue Router 的 history 模式,避免刷新页面时出现 404。如果路由是 hash 模式则不一定要配这个,但 history 模式观感更好。

4.3 MySQL 初始化脚本与数据导入

我习惯在项目根目录放一个sql/init.sql,把建库、建表、插入测试数据全部写在里面。这样拿到源码的人可以直接执行脚本跑起来,不用靠猜。脚本开头一般是:

CREATE DATABASE IF NOT EXISTS cookhub DEFAULT CHARACTER SET utf8mb4; USE cookhub;

然后创建表。测试数据我建议至少插入 5 个用户、5 个分类、20 条菜谱,这样前端列表和分类筛选功能一跑起来就有真实感。写菜谱内容时可以多用中文,贴近实际,比如红烧肉、麻婆豆腐。导入数据用 MySQL 命令行或者 Navicat 都可以。如果遇到Data too long错误,检查字段是不是 VARCHAR 太短,像描述和步骤必须用 TEXT。

5. 常见问题与排查经验

5.1 数据库连接报错与时区问题

本地运行时最常碰到的报错是数据库连接失败。第一个检查点:MySQL 服务有没有启动。Windows 下可以在“服务”里看 MySQL 状态,也可以用命令行mysql -uroot -p试连。第二个检查点:application.yml里的密码和实际密码是否一致。第三个点就是时区问题,MySQL 8.0 默认时区导致连接报错,解决办法是 URL 上加上serverTimezone=Asia/Shanghai,或者直接在 MySQL 里执行set global time_zone = '+08:00'。另外一定要加useSSL=false,避免本机 SSL 连接问题。

5.2 MyBatis 映射文件不生效

很多人把RecipeMapper.xml放在了src/main/java目录下,但 Maven 默认不会把 java 目录下的 xml 文件打包到 classpath,导致运行时报Invalid bound statement。解决办法有两种:把 XML 放到src/main/resources/mapper目录,或者在pom.xml中配置<resources>把 xml 也纳入打包。我用的是第一种,简单直接。还有一种情况是mybatis.mapper-locations配置路径写错,比如classpath:mapper/*.xml和实际目录不一致,也会静默不报错但找不到 SQL。建议启动时留意日志,是否打印了Loaded ... mapper信息。

5.3 前端跨域与 Session 失效

开发阶段如果不用 Vite proxy,而是直接让前端请求http://localhost:8080,就会触发跨域。此时浏览器会拦 Cookie 携带,导致 Session 一直失效,登录后刷新又变成未登录。解决办法是我前面说的 Vite proxy,请求路径保持同源。如果使用了 Nginx,也要配好/api的proxy_pass。实在要用后端 CORS 配置,需要注意allowCredentials(true)同时allowedOrigin不能是*,必须是具体的源地址。否则浏览器一样拦截 Cookie,这是很多人调试登录功能时最容易忽视的坑。

5.4 打包后页面空白或404

前端npm run build之后,如果直接打开本地dist/index.html,大概率是空白页,因为资源路径都是绝对路径。解决办法是修改vite.config.js里的base: './',让资源路径变成相对路径。如果部署后刷新页面出现 404,这说明 Vue Router 使用了 history 模式,而 Nginx 没有配置try_files。改成 hash 模式也能解决,但我更建议 Nginx 补上 location 配置,这样 URL 更好看。还有一个很隐蔽的问题:SpringBoot 单服务部署时,如果前端路由用的 history 模式,后端没有做 fallback,刷新时会直接抛 404。此时需要在后端加一个转发规则,让非/api的路径都到index.html。不过如果你把dist放进 static 并且用 hash 模式,就省心很多。

6. 从源码到项目:我个人的经验沉淀

这套厨艺交流平台是我近期完整走完的一条链路,从数据库设计到后端接口,再到前端页面和部署,每一步都能看到明显的成败点。我觉得最有价值的部分不是 CRUD 本身,而是以下几点:第一点是前后端接口约定要提前定死,返回格式统一用Result,字段命名统一用驼峰,联调时能少掉一半扯皮;第二点是数据库设计不用过度设计,但外键逻辑要清楚,多对多关系要学会用关联查询而不是建一堆冗余字段;第三点是一切安全相关的地方都不能偷懒,比如密码加密、Session 校验、上传文件类型校验,哪怕只是个人项目也要按规范写,因为这些习惯会带进工作里。

最后分享一个小技巧:当你拿到一个前后端分离项目源码时,不要急着改代码,先看三样东西——数据库脚本、application.yml、前端接口封装。把这三样理清,整个项目的地图就在脑子里了。遇到接口报错,先从浏览器 Network 里看请求状态码和响应体,再定位到底层 SQL 还是业务逻辑。这比盲猜变量名有效得多。厨艺交流平台这种规模的项目,非常适合做全栈理解的实践样本,跑通一遍,你对 SpringBoot、Vue3、MyBatis 的配合方式会有一个整体性的掌握。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 5:32:45

【MT32F006】MT32F006之PWM控制背光灯(RGB)

本文最后修改时间&#xff1a;2026年09月17日 一、本节简介 本文介绍如何使用MT32F006使用PWM控制RGB灯显示白光&#xff0c;再加上扩散膜、导光板、反光纸、遮光纸&#xff0c;即可作为LCD的背光。 二、实验平台 库版本&#xff1a;V1.0.0 编译软件&#xff1a;MDK5.37 硬…

作者头像 李华
网站建设 2026/10/10 5:30:37

Python shlex 完全指南:从词法原理到命令行安全解析

第一次在别人的工具源码里看到import shlex时&#xff0c;我第一反应是&#xff1a;这名字是故意的吧&#xff1f;后来翻了文档才知道&#xff0c;它全称是shell lexical analyzer&#xff0c;也就是“Shell 词法分析器”。当时我正好在写一个需要解析命令行字符串的工具&#…

作者头像 李华