SpringBoot + Vue3 美食菜谱管理系统最适合拿来练什么?不是把页面堆得多漂亮,而是用前后端分离的方式,把一条菜谱数据从表单提交、接口接收、数据库落库、列表回显完整跑通。这个项目对课程设计、毕设和简历都友好,因为它覆盖了 Java 后端分层、MySQL 表设计、Vue3 组件化、Axios 联调、跨域处理、分页搜索和基础异常处理。标题里说的“2小时搭建”,我的理解是:在环境已经装好、依赖能正常下载、照着最小闭环走的前提下,两小时可以完成一个能启动、能增删改查的版本;如果是第一次接触前后端分离,时间主要会花在版本、端口、数据库连接和跨域排查上。下面按实际落地顺序拆一遍,先跑单表 CRUD,再补分页、搜索、校验和交付细节,不要一上来就加登录、权限、图片上传和复杂统计。
1. 先看这套美食菜谱系统真正练的是什么能力
1.1 不是堆功能,而是跑通一条完整数据链路
很多同学做美食菜谱管理系统,容易把注意力放在页面数量上:首页、分类页、详情页、后台管理页、个人中心,看起来功能很多。真正放到简历和答辩里,面试官更关心的是一条数据链路是否闭环:前端表单里的菜谱名称、分类、食材、步骤能不能被后端正确接收;后端有没有做参数校验;数据库字段类型是否匹配;列表查询能不能分页;删除之后前端有没有刷新;接口报错时页面能不能给出可读提示。
这条链路跑通之后,再加功能才有意义。比如加图片上传,前提是文件存储路径、静态资源映射、表单提交方式都已经稳定;加登录,前提是接口返回结构、异常处理、前端请求拦截已经统一。否则功能越多,排查越乱。
我一般建议先把范围压到最小:一张菜谱表,五个接口,两个页面。五个接口是分页查询、新增、按 ID 查询、更新、删除;两个页面是菜谱列表页和新增编辑页。这个范围足够体现前后端分离,也足够在答辩时讲清楚 MVC 分层、RESTful 接口、跨域和数据库操作。等功能稳定,再把分类、搜索、图片、登录作为扩展点,而不是第一天全部铺开。
1.2 哪些人适合照着做,哪些人容易卡住
这套项目适合三类人:第一类是刚学完 SpringBoot 和 Vue3,需要一个小而完整的项目把知识点串起来;第二类是要做课程设计或毕设,需要一个结构清楚、能演示、能讲技术点的题目;第三类是想往简历里放一个前后端分离项目,但之前只写过单体页面或纯后端接口的人。
容易卡住的地方也很集中。第一类是环境不统一:JDK 版本、Maven 依赖、Node 版本、MySQL 驱动版本互相不匹配。第二类是端口和路径没约定:后端跑 8080,前端跑 5173,接口前缀一会儿写 /recipe,一会儿写 /api/recipe,最后 404。第三类是数据库连接失败:用户名、密码、库名、时区、驱动类名有一个不对,启动就报错。第四类是把“能启动”当成“能交付”:页面能打开,但新增失败、删除不刷新、分页参数传错、时间格式显示异常。
如果你之前没做过前后端分离,不要被“2小时”吓到,也不要把它理解成无脑复制。更稳妥的做法是把目标拆成四段:先让后端启动并连上数据库;再用接口工具验证五个接口;然后让前端列表页拿到数据;最后补新增、编辑、删除和表单校验。每一段都有明确的成功标准,卡住时也容易定位。
1.3 两小时目标的合理拆法:启动、单表CRUD、联调、收尾
如果环境已经就绪,两小时可以这样分配:前 20 分钟确认 JDK、Maven、Node、MySQL 和端口;接下来 30 分钟建表、写实体和 Mapper、把分页查询接口跑通;再用 30 分钟补新增、更新、删除和统一返回结构;然后 30 分钟做前端列表、表单和 Axios 封装;最后 10 分钟做联调检查,包括跨域、空数据、删除确认和错误提示。
这个拆法的关键是“先接口后页面”。后端接口没有用接口工具验证之前,不要急着写前端页面。因为页面报错时,你很难判断是请求没发出去、跨域被拦、参数格式不对,还是后端本身没返回数据。先用 Postman 或 Apifox 调通接口,前端只负责发请求和渲染结果,排查范围会小很多。
注意:不要一上来就把并发、图片上传、富文本编辑器和权限管理全加上。先让一条菜谱数据完成新增、列表展示、编辑、删除的完整循环,再考虑扩展。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:把版本和端口先固定下来
2.1 后端环境与数据库的检查顺序
后端建议使用 JDK 17 或你项目里已经验证过的 LTS 版本,SpringBoot 3.x 对 JDK 17 支持更稳。Maven 要确认能正常拉依赖,不要等到写代码时才发现仓库配置有问题。MySQL 建议用 8.x,字符集用 utf8mb4,这样菜谱名称、食材描述里的中文和特殊符号不容易出问题。
检查顺序我一般是这样:先确认 java -version 和 mvn -v 能正常输出;再确认 MySQL 服务已经启动,能用命令行或客户端登录;然后建库建表;最后启动一个最小 SpringBoot 工程,确认能连上数据库。不要跳过数据库直连检查,很多“后端启动失败”其实是账号密码、库名、端口或时区不对。
application.yml 里至少要明确数据库地址、端口、库名、用户名、密码和驱动。下面是一个通用示例,实际以你的环境为准:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/food_recipe?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
这里 map-underscore-to-camel-case 很关键。数据库字段是 create_time,实体里是 createTime,没有这个配置时,查询结果可能某些字段为空。看起来像“接口没返回数据”,实际是字段映射没对上。
2.2 前端环境和包管理器的选择
前端用 Vue3 + Vite 比较轻,启动快,目录也清楚。Node 版本建议用 18 或更高的 LTS 版本,和 Vite 的兼容性更稳。包管理器用 npm、pnpm 或 yarn 都可以,但一个项目里最好只固定一种,不要混用。混用之后锁文件不一致,依赖版本容易漂移,今天能跑,明天换台机器就报错。
前端依赖不需要一开始装太多。最小组合是 Vue3、Vue Router、Axios、Element Plus。Element Plus 用来做表格、表单、分页和弹窗,能省很多样式时间。如果你不想引入组件库,也可以手写,但表单校验、分页器和删除确认都要自己处理,时间会更长。
创建项目后先跑一次空页面,确认 npm run dev 能启动,浏览器能访问。再做路由和页面。不要等页面写完才第一次启动,否则启动错误和业务错误混在一起,排查成本很高。前端启动成功后,记住实际访问地址,常见是 http://localhost:5173,后面配置代理和跨域都要用。
2.3 目录结构和启动端口先约定清楚
建议把前后端放在同一个项目根目录下的两个子目录,例如 backend 和 frontend。后端按 controller、service、mapper、entity、common 分层;前端按 api、views、router、components 组织。目录不需要多复杂,但要让别人一眼能看出请求从哪进、业务在哪层、数据库操作在哪层。
端口也要先约定:后端 8080,前端 5173,接口统一以 /api 开头。前端开发环境通过 Vite 代理把 /api 转发到后端,减少浏览器跨域问题。这个约定看起来小,但能避免很多“接口地址到底写哪个”的返工。
js复制// 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
}
}
}
})
开发阶段用代理,生产部署时再按实际域名和网关配置。不要把开发环境的 localhost 地址硬编码到所有请求里,后面换环境会很麻烦。
3. 数据库和接口先定,后面写代码才不返工
3.1 菜谱表字段怎么设计才够用
美食菜谱管理系统的最小表不需要太多字段。菜谱名称、分类、食材、步骤、封面图、创建时间、更新时间就够跑通 CRUD。食材和步骤建议用 TEXT,因为内容可能比较长;菜谱名称用 VARCHAR(100),分类用 VARCHAR(50);封面图如果暂时不做上传,可以先留空或存一个图片地址。
sql复制CREATE TABLE recipe (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '菜谱名称',
category VARCHAR(50) DEFAULT NULL COMMENT '分类',
ingredients TEXT COMMENT '食材',
steps TEXT COMMENT '步骤',
cover_url VARCHAR(255) DEFAULT NULL COMMENT '封面图地址',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段设计要跟页面表单对应。页面上有菜谱名称、分类、食材、步骤,表里就要有对应字段;页面上没有的字段,不要为了“看起来完整”硬加。分类如果后面要做筛选,可以加索引;菜谱名称如果要做模糊搜索,可以先不建索引,数据量小的时候差别不大,等数据量上来再根据查询条件优化。
3.2 RESTful 接口和统一返回结构
接口建议统一以 /api/recipes 开头。查询列表用 GET /api/recipes,新增用 POST /api/recipes,按 ID 查询用 GET /api/recipes/{id},更新用 PUT /api/recipes/{id},删除用 DELETE /api/recipes/{id}。这种写法清楚,也方便前端封装。
返回结构要统一,至少包含 code、message、data。成功时 code 为 0,失败时给出可读消息。不要有的接口返回数组,有的接口返回对象,有的接口直接返回字符串。前端处理多种结构会让代码很乱。
json复制{
"code": 0,
"message": "ok",
"data": {
"records": [],
"total": 0,
"current": 1,
"size": 10
}
}
分页返回可以用 MyBatis-Plus 的 Page 结构,也可以自己封装。关键是前端要知道 records 是列表,total 是总数。新增和更新成功可以返回影响行数或 ID,删除成功返回布尔值。接口结构一旦定下来,前端 API 文件就按这个结构写,后面改字段时集中改一处。
3.3 分页、搜索和参数校验的边界
分页参数建议用 page 和 size,默认第 1 页、每页 10 条。size 不要不限制,前端传 1000 也照查,数据量大时会影响性能。可以在后端加一个上限,比如最大 100。搜索参数用 keyword,对菜谱名称做模糊查询即可。分类筛选可以后面再加,不要一开始把查询条件堆得太复杂。
参数校验要放在后端做,不能只靠前端。菜谱名称不能为空,长度不要超过数据库字段限制;分类可以为空,但长度要控制;食材和步骤如果允许为空,就要在页面上给出提示。后端校验失败时返回明确消息,前端把消息展示出来。不要所有错误都返回“系统异常”,那样排查时只能靠猜。
注意:前端校验是体验,后端校验是底线。只做前端校验,接口被直接调用时仍然会写入脏数据。
4. 后端用 SpringBoot 把增删改查跑通
4.1 实体、Mapper、Service、Controller 的分层
后端分层不需要过度设计,但每一层的职责要清楚。entity 对应数据库表,mapper 负责数据库访问,service 放业务逻辑,controller 接收请求和返回结果。菜谱系统业务简单,Service 层可能只是转调 Mapper,但保留这一层有利于后面加校验、日志和事务。
实体类字段要和表字段对应,使用 Lombok 可以减少 getter/setter。Mapper 继承 MyBatis-Plus 的 BaseMapper<Recipe> 后,基础 CRUD 可以直接用。Service 里可以封装分页查询和保存逻辑。Controller 只做参数接收、调用 Service、包装返回结果,不要把数据库操作写进 Controller。
这种分层的价值在排查时很明显:接口 404 看 Controller 映射;参数没收到看请求体和方法签名;数据库没查到看 Mapper 和 SQL;业务校验失败看 Service。层次清楚,问题就不会全挤在一个文件里。
4.2 一个可运行的菜谱接口示例
下面是一个简化示例,重点是接口结构和调用关系,实际项目按你的包名和依赖调整。分页查询用 MyBatis-Plus 的 Page,新增时做基础校验。
java复制@RestController
@RequestMapping("/api/recipes")
public class RecipeController {
private final RecipeService recipeService;
public RecipeController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@GetMapping
public Result<Page<Recipe>> page(
@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@RequestParam(required = false) String keyword) {
return Result.ok(recipeService.pageRecipes(page, size, keyword));
}
@PostMapping
public Result<Long> create(@RequestBody @Valid Recipe recipe) {
recipeService.save(recipe);
return Result.ok(recipe.getId());
}
@PutMapping("/{id}")
public Result<Boolean> update(@PathVariable Long id, @RequestBody @Valid Recipe recipe) {
recipe.setId(id);
return Result.ok(recipeService.updateById(recipe));
}
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.ok(recipeService.removeById(id));
}
}
Result 是统一返回包装类,@Valid 需要配合实体上的校验注解。示例里没有展开所有依赖,落地时确认 spring-boot-starter-validation 是否引入。分页查询里如果 keyword 不为空,就在 Service 里加 like 条件;为空就查全部。不要把模糊查询拼成字符串 SQL,用条件构造器更稳。
4.3 异常处理、跨域和日志不要留到最后
异常处理建议加一个全局异常处理器,把参数校验失败、数据库异常和业务异常分开返回。至少让前端能拿到可读的 message。跨域在开发阶段可以通过 Vite 代理规避,但如果前端直接请求后端地址,就需要后端配置 CORS。允许的来源不要长期写成任意来源,开发时可以临时放开,交付时改成实际前端地址。
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
}
日志也要在联调阶段打开。至少能看到请求进来、参数是什么、返回什么、异常堆栈在哪里。不要只靠前端控制台判断问题。前端报 500,后端日志里往往已经有明确原因,比如字段长度超限、SQL 语法错误、空指针或时间格式转换失败。
5. 前端用 Vue3 把页面和接口接起来
5.1 页面拆分和路由
前端最小可以拆成两个页面:菜谱列表页和菜谱编辑页。列表页负责展示表格、分页、搜索、删除;编辑页负责新增和编辑共用表单。路由用 /recipes 和 /recipes/edit/:id?,编辑时带 ID,新增时不带 ID。这样页面结构简单,也方便演示。
列表页的表格列建议包括菜谱名称、分类、更新时间、操作。食材和步骤内容长,不要全塞进表格,可以在详情弹窗或编辑页展示。操作列放编辑和删除按钮。删除要有确认弹窗,避免误点。分页器绑定当前页和每页条数,切换时重新请求接口。
页面拆分不要按“首页、分类、详情、后台、个人中心”一口气铺开。先把列表和编辑两个页面做稳,后面再加分类筛选和详情页。页面越少,联调路径越短,越容易在有限时间里跑通。
5.2 Axios 封装和接口联调
Axios 建议统一封装一个实例,设置基础路径、超时时间和响应拦截。开发阶段基础路径可以写 /api,由 Vite 代理转发。响应拦截里判断 code,成功返回 data,失败弹出消息。这样页面里不用每个请求都写一遍错误处理。
js复制// 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 !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
接口文件按模块放,例如 src/api/recipe.js 里写列表、新增、更新、删除四个方法。页面只调用这些方法,不直接写 URL。后面接口地址或参数变化时,只改一个文件。
5.3 表单校验、删除确认和列表刷新
新增和编辑共用一个表单时,要注意区分模式。有 ID 是编辑,需要先请求详情回填;没有 ID 是新增,表单初始化为空。提交前做前端校验:菜谱名称必填,长度不超过 100;分类长度不超过 50;食材和步骤可以选填,但如果页面要求必填,就要给明确提示。提交成功后返回列表页,并重新请求列表数据。
删除成功后也要刷新列表。如果当前页删空了一条,页码可能要往前退一页。这个细节不做,用户会看到空表格但页码还停在最后一页,以为删除失败。列表刷新时保留搜索关键词和分页参数,不要一刷新就回到第一页。
vue复制<script setup>
import { ref, onMounted } from 'vue'
import { listRecipes, deleteRecipe } from '@/api/recipe'
const rows = ref([])
const total = ref(0)
const page = ref(1)
const size = ref(10)
const keyword = ref('')
async function load() {
const data = await listRecipes({ page: page.value, size: size.value, keyword: keyword.value })
rows.value = data.records
total.value = data.total
}
async function handleDelete(id) {
await deleteRecipe(id)
await load()
}
onMounted(load)
</script>
示例只保留核心逻辑,实际页面还要加加载状态、空数据提示和删除确认。加载状态能避免用户重复点击;空数据提示能让用户知道不是页面坏了;删除确认能减少误操作。
6. 联调时最容易误判的几个问题
6.1 404、跨域和空数据分别怎么查
404 先看请求地址和后端映射是否一致。前端写 /api/recipes,后端 Controller 是 /api/recipe,就会 404。还要看代理有没有生效,Vite 代理的 target 是否是后端实际端口。跨域问题在浏览器控制台通常有明确提示,先看请求有没有到达后端;开发阶段优先用代理,少直接跨域请求。
空数据不一定是接口坏了。先看接口工具请求是否返回 records: [],如果是,说明查询条件或数据库本身没数据;如果接口工具有数据、页面没有,检查前端取值字段是否正确,比如后端返回 records,前端读成了 list。这类问题很常见,不要先改后端 SQL。
6.2 数据库连接失败和端口占用
数据库连接失败先看四件事:MySQL 服务是否启动、库是否存在、用户名密码是否正确、驱动和时区参数是否匹配。报错信息里通常会带 Access denied、Unknown database 或 Communications link failure,按关键词查比盲目改配置快。
端口占用看启动日志。后端 8080 被占用时,要么换端口,要么结束占用进程。前端 5173 被占用时,Vite 可能会自动换端口,但代理和跨域配置里写的还是 5173,就会出现请求异常。所以端口变化后要同步检查代理和 CORS 配置。
6.3 时间格式、分页参数和中文乱码
时间格式问题通常出现在前端展示和后端序列化之间。数据库是 DATETIME,实体是 LocalDateTime,前端拿到的是字符串或数组。可以在后端统一配置时间格式,也可以在前端格式化。不要一会儿用时间戳,一会儿用字符串,接口结构要稳定。
分页参数要确认类型和默认值。前端传 page=1&size=10,后端用 long 接收;如果前端传了空字符串,后端可能转换失败。中文乱码先看数据库字符集和连接串 characterEncoding=utf8,再看请求头和服务端编码。乱码问题不要靠前端转码硬补,优先从数据库和连接配置查。
| 现象 | 优先排查 | 常见原因 | 处理方向 |
|---|---|---|---|
| 接口 404 | 请求地址、代理、Controller 映射 | 前缀不一致、代理未生效 | 对齐 /api 前缀,检查 Vite 代理 |
| 浏览器跨域报错 | 请求是否直达后端 | 未配置 CORS 或来源不一致 | 开发用代理,后端按实际来源配置 |
| 列表为空 | 接口工具返回、数据库数据 | 查询条件、字段名、分页参数 | 先验证接口,再检查前端取值 |
| 新增失败 | 请求体、校验注解、字段长度 | 必填项为空、超长、类型不匹配 | 看后端日志和返回消息 |
| 删除后不刷新 | 删除接口返回值、列表刷新逻辑 | 未重新请求或页码未处理 | 删除成功后重新加载列表 |
7. 从能跑到能交付:毕设和简历里怎么写才不虚
7.1 验收清单:启动、新增、编辑、删除、搜索
交付前按清单走一遍:后端能启动且数据库连接正常;前端能启动且页面能访问;列表能显示初始化数据;新增一条菜谱后列表能看到;编辑后详情和列表都更新;删除后列表刷新且总数变化;搜索关键词能过滤;分页切换正常;接口异常时页面有提示。每一步都要实际点一遍,不要只看代码。
还要检查空数据和异常输入。比如菜谱名称为空提交、名称超长、ID 不存在时编辑、重复点击删除。这些场景能暴露校验和异常处理是否完整。毕设演示时,老师或面试官很可能随手点这些边界,而不是只看正常流程。
7.2 简历表述和答辩问题准备
简历里不要写“独立完成美食菜谱管理系统”就结束,要写清楚技术点和你的处理方式。可以这样表述:基于 SpringBoot + Vue3 实现前后端分离的菜谱管理模块,完成菜谱分页查询、新增、编辑、删除和关键词搜索;使用 MyBatis-Plus 简化数据访问,统一接口返回结构和全局异常处理;前端使用 Vue3 + Element Plus 实现表单校验、列表分页和删除确认;联调阶段通过 Vite 代理和 CORS 配置解决跨域问题。
答辩时常见问题包括:前后端如何分离、接口如何约定、跨域怎么处理、分页怎么实现、参数校验放在哪、删除为什么要确认、数据库字段为什么这样设计。回答时结合具体文件和排查过程,比背概念更有说服力。比如跨域问题,不要只说“配置了 CORS”,要能说清楚开发阶段用代理、请求经过 Vite 转发、生产环境按域名配置。
7.3 后续扩展的优先级
扩展功能要有优先级。第一优先级是把搜索、分页、表单校验和异常提示做稳;第二优先级是加分类筛选和详情页;第三优先级才是图片上传、登录和权限。图片上传涉及文件存储、静态资源映射和表单格式,登录涉及会话或令牌、拦截器和前端路由守卫,都会明显增加联调成本。先把单表 CRUD 做扎实,再按时间决定加什么。
如果只是课程作业,默认配置和最小功能通常够用;如果要放简历或参加答辩,就要把日志、统一返回、异常处理、README 和初始化 SQL 补上。README 里写清楚启动顺序、端口、数据库初始化方式和默认访问地址,别人拿到项目才能复现。两小时能完成的是最小闭环,真正拉开差距的是这些工程细节:输入是否校验、失败是否可读、数据是否一致、页面刷新是否正确。把这些点处理干净,这个美食菜谱管理系统才不只是一个能打开的页面,而是一个能讲清楚、能演示、能继续扩展的前后端分离项目。
