news 2026/9/28 1:35:45

SpringBoot+Vue3前后端分离美食菜谱管理系统:从零搭建实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3前后端分离美食菜谱管理系统:从零搭建实战

从“毕业设计”和“简历项目”这两个关键词出发,这个系统几乎踩中了所有经典要素:Java 后端、Vue3 前端、前后端分离、MySQL 增删改查。市面上类似的模板很多,但真正能跑通、能讲清楚、能经得起面试追问的版本却不多。这篇文章不准备做“源码搬运工”,而是把搭建过程中的关键决策、代码结构和最容易翻车的细节拆开讲清楚。无论你是准备毕设答辩,还是想在简历里加一个完整项目,读完这篇可以少走很多弯路。

1. 为什么“美食菜谱管理系统”会成经典项目

先说结论:这类系统不是技术含量最高的项目,但它是“性价比最高”的练手项目。

从学生视角看,它覆盖了前后端分离项目最核心的链路:前端页面发起请求,后端接收参数、查询数据库、返回 JSON,前端再把数据渲染到表格里。这条链路看似简单,却正好命中企业招聘时最常问的“你独立做过什么项目”和“你怎么理解前后端交互”。把这条链路讲透,比堆砌一堆从来没有运行过的“高并发秒杀”更能打动面试官。

从技术视角看,美食菜谱这个业务域很适合做增删改查演示。菜品有名称、分类、图片、简介、步骤等字段,天然适合做列表展示、分类筛选、条件搜索和表单校验。相比传统的“学生管理系统”或“图书管理系统”,菜谱主题更容易在界面设计上做出视觉效果,也更容易在后期扩展出收藏、评分、模糊搜索等加分功能。

从务实视角看,所谓“2 小时搭建”并不是夸张,但它有前提:后端用 SpringBoot 快速生成基础工程,前端用 Vue3 + Vite 跳过繁琐的 Webpack 配置,数据库表结构设计得足够简单。只要环境装好、依赖版本不冲突,跑通一个最小闭环确实可以控制在两小时以内。前提是理解每一步在做什么,而不是盲目复制粘贴。

这篇文章默认你已经有 Java 基础,会基本 Maven 操作,知道 Vue 组件的大致写法。如果还不太熟,建议先照着代码跑通,再回头补基础。

2. 系统功能与技术选型

2.1 功能清单

美食菜谱管理系统的核心功能不算多,但足够支撑一个完整的毕业设计:

功能模块说明
菜品管理菜品的增删改查,包括名称、分类、图片、简介、步骤
分类管理菜系分类(川菜、粤菜、西餐等),菜品与分类关联
模糊搜索按菜品名称或描述关键字查询
分页展示菜品列表分页显示,前后端配合完成
数据统计简单的分类统计或总数统计,用于首页展示

这些功能可以对标常见的“后台管理系统”套路。没有权限体系、没有 Redis 缓存、没有消息队列,但作为基础项目反而更合适——过于复杂的功能会让答辩失去重点。

2.2 技术栈说明

层次技术选型说明
后端框架SpringBoot快速构建 RESTful API
数据库MySQL存储菜品和分类数据
持久层Spring Data JPA / MyBatis-Plus二选一,文章以 MyBatis-Plus 演示
前端框架Vue3Composition API 组合式写法
构建工具Vite开发调试效率高
UI 组件Element Plus表格、表单、弹窗现成组件
HTTP 请求Axios前端调用后端接口
前后端联调开发代理Vite devServer 代理到 SpringBoot 端口

这里必须先说明一个容易踩坑的点:前后端分离不等于两个项目分开放就行。它要求前后端通过 HTTP 接口通信,意味着必须解决跨域问题、接口约定问题、数据格式统一问题。实操中很多新手卡住的不是增删改查本身,而是跨域、参数接收方式不一致、JSON 格式对不上。

3. 环境准备与前置条件

3.1 基础环境清单

工具建议版本说明
JDKJDK 8 或 JDK 17SpringBoot 2.x 用 JDK 8 稳妥,3.x 建议 JDK 17
MavenMaven 3.6+后端依赖管理
Node.jsNode 16.20+ 或 Node 18+前端构建环境
MySQLMySQL 5.7 或 8.x数据库
IDEIntelliJ IDEA 或 VS Code后端推荐 IDEA,前端 VS Code 也可以
数据库工具Navicat / DataGrip / 命令行建库建表

版本号这里不写死,因为不同电脑环境差异很大。一个更稳妥的判断是:SpringBoot 3.x 虽然新,但有些旧教程和依赖不兼容,做毕设求稳优先选 SpringBoot 2.7.x;Vue3 搭配 Vite 版本用官方默认即可。

3.2 环境安装后的验证

安装完成后,不要急着写代码,先确认环境都可用。

后端对项目采用"存储过程+定时任2s部署"的思路,DATABASE 增删改查由存储过程完成,前端对项目采用"存储过程+定时任2s部署"的思路;但后端对项目在验证时先使用一个简单的 REST 接口,而前端则用一个基础页�面验证。

在命令行分别执行:

java -version mvn -v node -v npm -v mysql --version

能看到版本输出基本就没问题。如果命令提示找不到,先检查 PATH 环境变量,这是新手最常见的启动失败原因。

3.3 数据库准备

创建一个数据库,例如food_recipe,字符集选择utf8mb4,因为菜谱步骤里可能出现特殊字符,utf8mb4比utf8兼容性更好。

CREATE DATABASE IF NOT EXISTS food_recipe DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE food_recipe;

然后创建分类表和菜品表。两张表之间是一对多关系。先写分类表,再写菜品表。

CREATE TABLE category ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '分类名称', sort INT DEFAULT 0 COMMENT '排序号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜系分类表'; CREATE TABLE recipe ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category_id BIGINT NOT NULL COMMENT '分类ID', name VARCHAR(100) NOT NULL COMMENT '菜品名称', image VARCHAR(255) COMMENT '图片地址', description TEXT COMMENT '菜品简介', steps TEXT COMMENT '制作步骤', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';

如果采用 MyBatis-Plus,表名可以直接用实体类映射。要注意category和recipe都不是 MySQL 保留字,可以放心使用,但为了稳妥,建议在配置里开启驼峰映射功能。

4. 后端 SpringBoot 实现

4.1 创建 SpringBoot 项目

后端工程结构建议按 Controller、Service、Mapper、Entity 分包,清晰易懂。用 IDEA 直接创建 Spring Initializr 项目,或者从 Spring 官网生成压缩包导入。

核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

如果你的 SpringBoot 版本是 2.7.x,数据库驱动写mysql-connector-java;如果是 3.x,默认是com.mysql:mysql-connector-j。这个细节经常会让人启动报错。

4.2 配置文件 application.yml

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/food_recipe?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

三个容易忽略的地方:

第一,serverTimezone必须设置。不设这个参数,某些 MySQL 版本下连接会报时间错误。

第二,map-underscore-to-camel-case开启后,数据库字段create_time才能自动映射到实体类属性createTime。

第三,log-impl配置成 StdOutImpl,开发阶段可以在控制台看到 SQL 语句,排错非常方便,正式环境再关掉。

4.3 实体类和 Mapper

实体类直接用 Lombok 简化代码:

package com.example.food.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("recipe") public class Recipe { @TableId(type = IdType.AUTO) private Long id; private Long categoryId; private String name; private String image; private String description; private String steps; private LocalDateTime createTime; }

Mapper 接口只需要继承BaseMapper:

package com.example.food.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.food.entity.Recipe; import org.apache.ibatis.annotations.Mapper; @Mapper public interface RecipeMapper extends BaseMapper<Recipe> { }

MyBatis-Plus 把单表增删改查都封装好了,不需要手写 XML。这里特别说明:使用 MyBatis-Plus 不等于不懂 SQL。面试官如果问你底层原理,你要能说出BaseMapper是如何通过泛型推断表名和字段的,以及Wrapper是如何构造条件的。

4.4 Controller 和 Service

后端对外提供 RESTful API,界面给前端一个统一的返回结构。创建一个通用返回类Result:

package com.example.food.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

菜品接口:

package com.example.food.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.food.common.Result; import com.example.food.entity.Recipe; import com.example.food.mapper.RecipeMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/recipe") public class RecipeController { @Autowired private RecipeMapper recipeMapper; @GetMapping public Result<Page<Recipe>> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword) { QueryWrapper<Recipe> wrapper = new QueryWrapper<>(); if (keyword != null && !keyword.isEmpty()) { wrapper.like("name", keyword).or().like("description", keyword); } wrapper.orderByDesc("create_time"); Page<Recipe> result = recipeMapper.selectPage(new Page<>(page, size), wrapper); return Result.success(result); } @GetMapping("/{id}") public Result<Recipe> detail(@PathVariable Long id) { Recipe recipe = recipeMapper.selectById(id); return Result.success(recipe); } @PostMapping public Result<Recipe> add(@RequestBody Recipe recipe) { recipeMapper.insert(recipe); return Result.success(recipe); } @PutMapping public Result<Recipe> update(@RequestBody Recipe recipe) { recipeMapper.updateById(recipe); return Result.success(recipe); } @DeleteMapping("/{id}") public Result<Void> delete(@PathVariable Long id) { recipeMapper.deleteById(id); return Result.success(null); } }

这个 Controller 把分页查询、详情、新增、修改、删除都覆盖了。需要注意两个点:

第一,@RequestParam用来接收查询参数,@PathVariable用来接收路径参数,@RequestBody用来接收 JSON 请求体。前端传参方式的区别就在这里,面试中经常被问。

第二,模糊搜索的or()使用要小心。like("name", keyword).or().like("description", keyword)实际生成的是name LIKE ? OR description LIKE ?,如果不加外层括号,后续再加其他条件时 SQL 可能会出现逻辑错误。更稳妥的写法是用and(wrapper -> wrapper.like(...).or().like(...)),但在这里示例已经能满足需求。

Controller 直接调 Mapper 是为了演示方便。实际工程建议拆出 Service 层,至少保留一个事务控制的入口,后续扩展收藏、评论时不会让 Controller 越来越重。

4.5 跨域配置

前后端分离项目联调时,浏览器会拦截跨域请求。最直接的解决方式是在后端加 CORS 配置。可以写一个配置类:

package com.example.food.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns("*")在 SpringBoot 2.7 和 3.x 里兼容性都比较好。开发阶段放开所有来源,等部署上线再收紧到具体域名,这是更稳妥的做法。

启动后端后,浏览器直接访问http://localhost:8080/api/recipe?page=1&size=10,如果能看到 JSON 数据,说明后端接口已经通了。

5. 前端 Vue3 实现

5.1 创建 Vite 项目

npm create vite@latest food-web -- --template vue cd food-web npm install

项目创建完成后,安装运行时依赖:

npm install element-plus @element-plus/icons-vue axios vue-router

如果网络下载慢,可以配置 npm 镜像源,但注意只改变包下载源,不要影响代码逻辑。

5.2 项目结构

保持简洁:

src/ api/ recipe.js router/ index.js views/ RecipeList.vue RecipeEdit.vue App.vue main.js

前端没有必要把目录拆得太散,对毕设和简历项目来说,清晰比复杂更重要。

5.3 封装 axios 请求

// 文件路径:src/api/request.js 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.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

基于接口定义封装菜品请求:

// 文件路径:src/api/recipe.js import request from './request' export function getRecipeList(params) { return request.get('/recipe', { params }) } export function addRecipe(data) { return request.post('/recipe', data) } export function updateRecipe(data) { return request.put('/recipe', data) } export function deleteRecipe(id) { return request.delete(`/recipe/${id}`) }

这里有一个前后端协作的关键点:封装的getRecipeList传的是params,最后生成的是?page=1&size=10&keyword=xxx;而新增和修改走的是data,axios 会序列化成 JSON 放在请求体里。正好对应后端@RequestParam和@RequestBody两种接收方式。如果前后端约定不一致,接口就会报 400 或 415,所以先想清楚参数格式再写代码。

5.4 设置开发代理

前端请求写的是/api/recipe,但后端端口是 8080,前端 Vite 默认端口是 5173,直接请求会跨域。开发阶段最常见的做法是在vite.config.js里配置代理:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

配置之后,前端请求/api/recipe会被代理转发到http://localhost:8080/api/recipe,浏览器看起来是同源请求,避免跨域问题。这种方式也比后端 CORS 更适合平时开发,因为后端 CORS 配置越收越紧时,前端代理不受影响。

5.5 菜品列表页

列表页是整套系统的核心界面,包含搜索栏、表格、分页和操作按钮。

<!-- 文件路径:src/views/RecipeList.vue --> <template> <div class="recipe-list"> <el-card> <div class="search-bar"> <el-input v-model="keyword" placeholder="输入菜品名称或描述" clearable style="width: 300px" @keyup.enter="handleSearch" /> <el-button type="primary" @click="handleSearch">搜索</el-button> <el-button type="success" @click="handleAdd">新增菜品</el-button> </div> <el-table :data="tableData" border stripe> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="菜品名称" min-width="140" /> <el-table-column prop="categoryId" label="分类ID" width="100" /> <el-table-column prop="description" label="简介" show-overflow-tooltip min-width="200" /> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="220" fixed="right"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" layout="total, prev, pager, next" @current-change="loadData" /> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getRecipeList, deleteRecipe } from '../api/recipe' import { useRouter } from 'vue-router' const router = useRouter() const keyword = ref('') const page = ref(1) const size = ref(10) const total = ref(0) const tableData = ref([]) const loadData = async () => { const res = await getRecipeList({ page: page.value, size: size.value, keyword: keyword.value }) tableData.value = res.data.records total.value = res.data.total } const handleSearch = () => { page.value = 1 loadData() } const handleAdd = () => { router.push({ name: 'RecipeEdit' }) } const handleEdit = (row) => { router.push({ name: 'RecipeEdit', query: { id: row.id } }) } const handleDelete = (row) => { ElMessageBox.confirm(`确定删除菜品「${row.name}」吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { await deleteRecipe(row.id) ElMessage.success('删除成功') loadData() }) } onMounted(() => { loadData() }) </script> <style scoped> .search-bar { display: flex; gap: 12px; margin-bottom: 16px; } </style>

这段代码展示了几个 Vue3 常见的写法:<script setup>组合式 API、模板里的v-model:current-page分页绑定、通过useRouter()跳转路由。如果你的 Vue2 基础偏强,第一次接触script setup会有点不习惯,但它确实是 Vue3 官方推荐的主流写法,也是面试中的高频考点。

5.6 编辑页

新增和编辑共用一个页面,通过路由参数是否存在判断是编辑还是新增。

<!-- 文件路径:src/views/RecipeEdit.vue --> <template> <el-card> <template #header> <span>{{ form.id ? '编辑菜品' : '新增菜品' }}</span> </template> <el-form :model="form" label-width="100px" style="max-width: 600px"> <el-form-item label="菜品名称"> <el-input v-model="form.name" placeholder="请输入菜品名称" /> </el-form-item> <el-form-item label="分类ID"> <el-input-number v-model="form.categoryId" :min="1" :max="999" /> </el-form-item> <el-form-item label="图片地址"> <el-input v-model="form.image" placeholder="https://..." /> </el-form-item> <el-form-item label="菜品简介"> <el-input v-model="form.description" type="textarea" :rows="3" /> </el-form-item> <el-form-item label="制作步骤"> <el-input v-model="form.steps" type="textarea" :rows="6" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSave">保存</el-button> <el-button @click="routerBack">返回</el-button> </el-form-item> </el-form> </el-card> </template> <script setup> import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { getRecipeList, addRecipe, updateRecipe } from '../api/recipe' const route = useRoute() const router = useRouter() const form = ref({ id: null, name: '', categoryId: 1, image: '', description: '', steps: '' }) const loadDetail = async () => { const id = route.query.id if (!id) return const res = await getRecipeList({ page: 1, size: 1, keyword: form.value.name }) // 这里仅为演示,实际请根据 id 调用详情接口 } const handleSave = async () => { if (!form.value.name) { ElMessage.warning('菜品名称不能为空') return } if (form.value.id) { await updateRecipe(form.value) } else { await addRecipe(form.value) } ElMessage.success('保存成功') router.push({ name: 'RecipeList' }) } const routerBack = () => { router.push({ name: 'RecipeList' }) } onMounted(() => { if (route.query.id) { form.value.id = Number(route.query.id) // 在实际项目中,这里应调用详情接口回显数据 } }) </script>

必须坦诚说明:这个编辑页的“数据回显”逻辑我简化了。正常项目应该新写一个/recipe/{id}详情接口,在onMounted中通过detail(id)查询后把数据填进form。这里只保留了新增和保存的主流程,原因是很多初学者会把回显逻辑写得过度复杂。正式项目请补全详情接口,下面给出后端 Controller 中对应的detail方法。

5.7 路由配置

// 文件路径:src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import RecipeList from '../views/RecipeList.vue' import RecipeEdit from '../views/RecipeEdit.vue' const routes = [ { path: '/', name: 'RecipeList', component: RecipeList }, { path: '/edit', name: 'RecipeEdit', component: RecipeEdit } ] const router = createRouter({ history: createWebHistory(), routes }) export default router

完整的入口文件:

// 文件路径:src/main.js import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(ElementPlus) app.use(router) app.mount('#app')

到这里,前端最基本的页面、路由、请求封装已经齐全,可以开始联调。

6. 运行验证与联调

6.1 启动后端

在 IDEA 里直接运行启动类,也可以使用命令行:

mvn spring-boot:run

观察控制台日志,出现类似Started DemoApplication in xxxx seconds表示启动成功。再访问接口:

curl http://localhost:8080/api/recipe?page=1&size=10

如果返回 JSON 中包含records、total字段,说明后端正常。如果前端返回的字段结构和后端对不上,比如后端返回data.records,前端却读data.list,列表就一定空白。

6.2 启动前端

在food-web目录下执行:

npm run dev

浏览器访问http://localhost:5173。输入一个关键字搜索,查看 Network 面板中的请求路径:

  • 前端请求/api/recipe?page=1&size=10&keyword=xxx
  • 代理转发到http://localhost:8080/api/recipe?page=1&size=10&keyword=xxx
  • 后端返回 JSON
  • 前端渲染到表格

只要这四步链路能走通,增删改查功能就基本完成。

6.3 验证用例

功能操作预期结果
分页查询打开列表页表格展示第一页数据,分页组件显示总条数
模糊搜索输入“红烧”并回车表格只显示名称或描述包含“红烧”的菜品
新增点击新增,填写表单并保存列表新增一条记录
修改点击编辑,修改名称并保存列表对应数据更新
删除点击删除,确认弹窗点确定列表数据减少一条

如果某一项失败,优先打开浏览器的开发者工具看请求状态码。常见的状态码含义:

状态码含义常见原因
404接口路径不存在前端请求路径或后端类上 RequestMapping 写错
400请求参数格式错误前端传 JSON,后端要表单参数,或字段名不一致
415请求体类型不支持缺少@RequestBody,或 Content-Type 不对
500后端异常查看 IDEA 控制台异常堆栈

实际开发中,前后端联调 70% 的问题都出在参数接收方式和字段名不一致上。先看请求 Payload,再看后端日志,比盲目改代码有效得多。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
后端启动报数据库连接错MySQL 未启动、密码不对、url 写错检查 MySQL 服务,确认 application.yml 配置修正连接配置,确认数据库已创建
启动报Access denied for user用户名或密码错误在 Navicat 中测试连接修改密码配置,或重置数据库密码
前端请求接口 404后端接口路径与前端baseURL拼接不一致查看浏览器 Network 请求完整 URL统一/api前缀,检查@RequestMapping
返回数据正常但表格不显示后端返回结构是{code,message,data},前端读取字段不对在 axios 拦截器打印response确认res.data.records还是res.data.list
新增或修改后数据没变化前端传参是 JSON,后端没有@RequestBody查看后端日志参数是否为空后端方法加@RequestBody,前端序列化格式保持 JSON
时间字段显示格式不对后端 LocalDateTime 序列化格式没配置看接口返回的 JSON配置 jackson date-format 或前端做格式化处理
删除后列表不刷新删除成功但没调用loadData()查看控制台是否有删除请求在删除回调后重新查询列表

这组问题是从大量实际调试中提炼出来的。如果你的项目报错不在表里,还有一个通用排错思路:从请求入口开始,一层层确认。前端看 Network,后端看 Console,数据库看 SQL 日志,往下走总能定位到问题层。

8. 最佳实践与工程建议

8.1 目录与命名规范

后端不要把所有类都堆在controller一个包里。建议按功能模块拆分,比如recipe包里有RecipeController、RecipeService、RecipeMapper、RecipeEntity。前端页面组件用大驼峰命名,方法和变量用驼峰,路由路径用小写。这些细节面试官扫一眼代码就能看出来。

8.2 参数校验不能只依赖前端

前端表单校验只是体验,后端必须做基础参数校验。比如新增菜品时,菜品名称不能为空,分类 ID 必须存在。可以用 Spring 的@Validated注解,也可以在 Controller 里手动判断。无论用哪种,核心原则一致:后端永远不能信任前端传过来的数据。做毕设时可能觉得没必要,但面试官问“如何防止脏数据入库”时,答案就在这里。

8.3 接口统一返回结构

所有接口统一返回Result结构,好处是前端可以写统一的拦截器处理错误和加载状态。如果一会儿返回Map,一会儿返回String,前端就没法统一处理。这个设计看似简单,但很多真实团队都做得不彻底。

8.4 数据库操作注意事项

  • 删除功能在生产环境不要硬删除,建议加deleted字段做逻辑删除。
  • 图片 URL 不要直接存 Base64 大文本,存 URL 路径就好。
  • 表字段名避免使用 MySQL 保留字,比如order、group。
  • 大批量数据操作前先备份,这个项目虽然数据量不大,但习惯要从平时养成。

8.5 面向面试的准备

把这个项目写进简历时,不要写“负责前后端分离美食网站开发”这种空话。可以写成:

  • 独立设计 MySQL 数据库表结构,完成菜谱与分类的增删改查接口。
  • 基于 Vue3 + Element Plus 实现后台管理页面,封装 axios 请求并解决开发跨域问题。
  • 使用 MyBatis-Plus 简化数据访问层开发,理解分页查询和条件构造器底层原理。

面试官大概率会追问的问题包括:跨域是怎么产生的?@RequestBody和@RequestParam区别?MyBatis-Plus 为什么不写 SQL 也能查询?Vue3 的ref和reactive区别?分页插件底层如何实现?这些内容都能在本文中找到对应落点,建议针对每个问题准备一个简短的、结合本项目场景的回答。

8.6 生产部署思路

本地跑通以后,如果想把项目部署到服务器,有两种常见路径:

路径一,前端执行npm run build生成dist静态文件,用 Nginx 部署,后端打成 jar 包用java -jar运行,Nginx 再把/api请求代理到后端端口。

路径二,如果不想用 Nginx,可以直接把前端dist目录放进 SpringBoot 的src/main/resources/static下,打成同一个包部署。但这会失去前后端分离的意义,更适合演示和兜底,不适合体现工程能力。

如果想要简历项目完整度更高,建议选路径一,并重点写明 Nginx 配置、jar 包启动命令和进程管理方式,这是很多面试官认可的工程能力。

9. 总结与后续学习方向

2 小时跑通这个项目,真正帮助你建立的不是“我会写增删改查”,而是理解一条完整的前后端协作链路:数据库表如何设计、后端如何暴露接口、前端如何请求数据、数据如何通过 JSON 流动。这条链路是无数 Web 项目的骨架,以后不管切换到什么业务领域,骨架都是类似的。

跑通之后,不要停在“能运行”这一步。你可以继续做三件很有价值的事:第一,把分类表真正做成下拉选择,替换掉现在的前端手工输入分类 ID;第二,补充菜品详情的独立接口和页面路由,完善数据回显;第三,给项目加一个简单的登录页面和拦截器,让后端接口只有登录后才能访问。这三件事做完,项目的完整度和面试表现力会再上一个台阶。

如果时间允许,还可以继续研究 SpringBoot 的拦截器实现权限校验、MyBatis-Plus 的分页插件的拦截原理、Vue3 组件通信和路由守卫。这个项目虽然不大,但每深入一层,都能延伸到面试题库里最常出现的问题。把代码放进简历之前,最好先在本地从头到尾独立操作一遍,确保每一步都心里有数。能讲清楚的项目才有价值,否则它只是你硬盘里一段没有灵魂的压缩包。

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

做网站添加mp3实战对比评测,3步避坑不花冤枉钱

做网站添加mp3实战对比评测,3步避坑不花冤枉钱 找建站公司最怕什么?不是技术不行,是报价单像天书,功能列表全是行话,最后发现几千块只是买了个壳子。做网站添加mp3这种基础需求,很多小白被忽悠加“高级音频模块”,实则就是几行代码的事。我做过十年建站,见过太多企业因为不懂技术,在音频播放、版权授权、服…

作者头像 李华
网站建设 2026/9/28 1:35:33

网站机房建设方案哪家强?避开这5个坑让你的官网流量翻倍

网站机房建设方案哪家强?避开这5个坑让你的官网流量翻倍 网站做好了没人访问,是不是让你半夜醒来都睡不着?别慌,这真不怪你的内容写得不好,多半是底层的“地基”没打好。很多老板找建站公司,问的第一句话就是“哪家好”,其实选对 网站机房建设方案…

作者头像 李华
网站建设 2026/9/28 1:35:10

怎么用网吧电脑做网站服务器吗源码下载

网吧电脑能当服务器吗?从零搭建避坑指南 别再说模板网站丑到没朋友了。那种套壳的东西,客户一眼看穿,转化率惨不忍睹。想做出有质感的站,你得懂点底层的逻辑。很多人问,家里那台旧主机,或者去网吧蹭个网,能不能 从零搭建 一个真正的网站服务器? 这问题很典型,暴露了大家对“服务器”概念的混淆。简单说:…

作者头像 李华
网站建设 2026/9/28 1:34:59

STM32读取富斯i6遥控器iBus协议:从接线到代码详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:34:50

电子商务网站建设课设详细步骤

电商课设别瞎搞,用免费工具3天搞定不拖泥带水 改个需求建站公司拖一周,这种痛谁懂?做电子商务网站建设课设,最怕的就是被外包坑或者自己瞎摸索。别慌,现在不缺代码,缺的是选对 免费工具 和理清思路。…

作者头像 李华
网站建设 2026/9/28 1:34:39

找网站开发产品设计公司避坑,这份保姆级建站教程含技术选型全解析

找网站开发产品设计公司避坑,这份保姆级建站教程含技术选型全解析 模板网站太丑,更致命的是性能差、改不动。很多甲方找网站开发产品设计公司,最后发现交付的东西连个像样的后台都没有,想改个颜色都要等三天。这份保姆级建站教程,直接拆解底层逻辑,帮你避开90%的坑。…

作者头像 李华