如果你是一名Java或前端开发者,正在为毕业设计、课程作业、个人简历项目,或者只是想快速搭建一个具备完整增删改查功能的管理系统而发愁,那么这篇文章就是为你准备的。
你很可能遇到过这样的困境:想做一个“美食网站”或类似的管理系统,却卡在了技术选型、环境搭建、前后端联调这些繁琐的环节上。网上教程要么过于简单,只讲概念;要么过于复杂,动辄引入微服务、分布式,让人望而却步。最终,宝贵的时间都花在了环境配置和Bug调试上,核心的业务逻辑和项目展示价值反而没体现出来。
这篇文章要解决的,正是这个痛点。我们将基于当前企业级开发中最主流、最受认可的技术栈——SpringBoot 3.x + Vue 3 + Element Plus,带你在一小时内,从零开始搭建一个功能完整、界面美观、前后端分离的“美食网站管理系统”。这不仅仅是一个演示,而是一个可直接运行、可二次开发、能写进简历的真实项目。
我的核心判断是:对于大多数以学习和展示为目的的Java/Vue项目,“快速跑通一个五脏俱全的Demo”远比“深入研究某个晦涩原理”更重要。本文将聚焦于“如何做”,并提供清晰的步骤、可复现的代码和完整的源码,确保你跟着做就能成功。我们将涵盖从环境准备、数据库设计、后端接口开发、前端页面构建到最终打包部署的全流程,并重点解释那些新手最容易踩坑的环节。
1. 为什么选择SpringBoot+Vue3作为你的技术栈?
在开始动手之前,我们需要理解为什么这个组合是当前构建Web应用,尤其是管理系统类项目的“黄金标准”。
SpringBoot是Java后端开发的“瑞士军刀”。它通过“约定大于配置”的理念,极大地简化了Spring应用的初始搭建和开发过程。你不再需要编写繁琐的XML配置,内嵌的Tomcat服务器让你能一键启动应用。对于美食网站这样的系统,我们需要处理用户认证、菜品数据管理、订单处理等,SpringBoot能快速集成MyBatis(数据库操作)、Spring Security(安全控制)等关键组件,让开发者专注于业务逻辑。
Vue 3是前端框架中的后起之秀,以其响应式系统和组合式API著称。相比于Vue 2,它在性能、TypeScript支持以及逻辑复用方面有显著提升。对于管理系统这种以表单、表格、弹窗为主要交互形式的页面,Vue 3配合Element Plus(基于Vue 3的UI组件库)可以让你像搭积木一样快速构建出专业且美观的界面。
前后端分离是现代Web开发的标配。在这种架构下,后端(SpringBoot)只负责提供数据接口(API),前端(Vue)负责渲染页面和用户交互。两者通过HTTP协议(通常是JSON格式)通信。这样做的好处是:
- 职责清晰:后端专注数据和业务安全,前端专注用户体验。
- 并行开发:前后端工程师可以同时工作,只需约定好API接口。
- 易于维护和扩展:技术栈可以独立升级,也方便未来开发移动端App(共用同一套后端API)。
对于你的毕业设计或简历项目,这个组合能清晰地向面试官展示你掌握了当前业界的主流开发模式和全栈技能。
2. 项目核心功能与效果预览
在编码之前,我们先明确要构建的系统具体包含哪些功能,这有助于理解后续的代码结构。
我们的“美食网站管理系统”主要包含以下模块:
- 用户登录/注册:实现基本的身份认证。
- 菜品管理:
- 增:添加新菜品(名称、分类、价格、图片、描述)。
- 删:删除菜品。
- 改:修改菜品信息。
- 查:分页列表查询、按条件(名称、分类)搜索菜品。
- 分类管理:对菜品进行分类(如:川菜、湘菜、甜品等),支持增删改查。
- 订单管理(简化版):查看用户下单记录。
技术栈明细:
- 后端:SpringBoot 3.x, JDK 17+, Maven, MyBatis-Plus, MySQL 8.0
- 前端:Vue 3, Vite, Element Plus, Axios, Vue Router
- 开发工具:IntelliJ IDEA (后端), Visual Studio Code (前端)
3. 环境准备与项目初始化
这是最容易出错的环节,请严格按照步骤操作。
3.1 后端环境搭建
- 安装JDK:确保已安装JDK 17或更高版本。在命令行输入
java -version验证。 - 安装Maven:用于管理项目依赖。安装后配置环境变量,执行
mvn -v验证。 - 安装MySQL:安装MySQL 8.0,并启动服务。使用命令行或图形化工具(如Navicat)创建一个名为
food_website的数据库,字符集设为utf8mb4。 - 使用Spring Initializr创建项目:
- 访问 start.spring.io 。
- 选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.x.x (最新稳定版)
- Project Metadata: 填写你的Group(如
com.example)和Artifact(如food-backend)。 - Dependencies: 添加Spring Web,MySQL Driver,MyBatis Framework,Lombok。
- 点击“GENERATE”下载项目压缩包,解压后用IDEA打开。
3.2 前端环境搭建
- 安装Node.js:访问Node.js官网,安装LTS版本(如18.x)。安装后执行
node -v和npm -v验证。 - 创建Vue项目:打开终端,执行以下命令。我们使用Vite作为构建工具,它比传统的Vue CLI更快。
# 使用 npm 创建项目 npm create vue@latest food-frontend # 创建过程中,命令行会交互式询问,请按如下选择: # ✔ Project name: food-frontend (直接回车) # ✔ Add TypeScript? … No # ✔ Add JSX Support? … No # ✔ Add Vue Router for Single Page Application development? … Yes # ✔ Add Pinia for state management? … Yes # ✔ Add Vitest for Unit Testing? … No # ✔ Add an End-to-End Testing Solution? » No # ✔ Add ESLint for code quality? … Yes # ✔ Add Prettier for code formatting? … Yes # 创建完成后,进入项目目录并安装依赖 cd food-frontend npm install # 额外安装 Element Plus 和 Axios npm install element-plus axios4. 数据库设计与后端核心开发
4.1 数据库表设计
在food_website数据库中执行以下SQL语句,创建核心表。
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密后)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 菜品分类表 CREATE TABLE `food_category` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类ID', `name` varchar(50) NOT NULL COMMENT '分类名称', `sort` int DEFAULT '0' COMMENT '排序', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品分类表'; -- 菜品表 CREATE TABLE `food_dish` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜品ID', `name` varchar(100) NOT NULL COMMENT '菜品名称', `category_id` bigint NOT NULL COMMENT '分类ID', `price` decimal(10,2) NOT NULL COMMENT '价格', `image` varchar(255) DEFAULT NULL COMMENT '图片URL', `description` text COMMENT '描述', `status` tinyint DEFAULT '1' COMMENT '状态:1-上架,0-下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), CONSTRAINT `fk_dish_category` FOREIGN KEY (`category_id`) REFERENCES `food_category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';4.2 后端项目配置与实体类编写
- 配置数据库连接:打开后端项目的
src/main/resources/application.properties文件,修改为以下内容。
# 应用端口 server.port=8080 # 数据库配置 (请替换为你自己的数据库密码) spring.datasource.url=jdbc:mysql://localhost:3306/food_website?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis 配置 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.configuration.map-underscore-to-camel-case=true # 打印SQL日志,方便调试 logging.level.com.example.foodbackend.mapper=debug- 创建实体类:在
src/main/java/com/example/foodbackend/entity包下创建对应的Java类。这里以FoodDish为例,使用Lombok简化代码。
// 文件路径:src/main/java/com/example/foodbackend/entity/FoodDish.java package com.example.foodbackend.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("food_dish") // 指定对应表名 public class FoodDish { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private String description; private Integer status; // 1-上架,0-下架 @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }4.3 使用MyBatis-Plus实现增删改查
MyBatis-Plus(MP)是对MyBatis的增强,可以极大减少单表操作的SQL编写。
- 添加MP依赖:在
pom.xml中添加依赖。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> <!-- 请使用与SpringBoot 3兼容的最新版本 --> </dependency>- 创建Mapper接口:在
mapper包下创建接口。
// 文件路径:src/main/java/com/example/foodbackend/mapper/FoodDishMapper.java package com.example.foodbackend.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.foodbackend.entity.FoodDish; import org.apache.ibatis.annotations.Mapper; @Mapper public interface FoodDishMapper extends BaseMapper<FoodDish> { // 继承了BaseMapper,就拥有了基本的CRUD方法,无需编写XML }- 创建Service层:在
service包下创建接口和实现类。
// 文件路径:src/main/java/com/example/foodbackend/service/FoodDishService.java package com.example.foodbackend.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.foodbackend.entity.FoodDish; import java.util.List; public interface FoodDishService extends IService<FoodDish> { // 可以在此定义复杂的业务方法 List<FoodDish> listByCategoryId(Long categoryId); }// 文件路径:src/main/java/com/example/foodbackend/service/impl/FoodDishServiceImpl.java package com.example.foodbackend.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.foodbackend.entity.FoodDish; import com.example.foodbackend.mapper.FoodDishMapper; import com.example.foodbackend.service.FoodDishService; import org.springframework.stereotype.Service; import java.util.List; @Service public class FoodDishServiceImpl extends ServiceImpl<FoodDishMapper, FoodDish> implements FoodDishService { @Override public List<FoodDish> listByCategoryId(Long categoryId) { LambdaQueryWrapper<FoodDish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(FoodDish::getCategoryId, categoryId) .eq(FoodDish::getStatus, 1) // 只查询上架的菜品 .orderByAsc(FoodDish::getCreateTime); return this.list(queryWrapper); } }- 创建Controller层:提供RESTful API。
// 文件路径:src/main/java/com/example/foodbackend/controller/FoodDishController.java package com.example.foodbackend.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.foodbackend.common.R; import com.example.foodbackend.entity.FoodDish; import com.example.foodbackend.service.FoodDishService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @Slf4j @RestController @RequestMapping("/dish") public class FoodDishController { @Autowired private FoodDishService dishService; // 新增菜品 @PostMapping public R<String> save(@RequestBody FoodDish dish) { log.info("新增菜品:{}", dish); dishService.save(dish); return R.success("新增菜品成功"); } // 分页查询菜品 @GetMapping("/page") public R<Page<FoodDish>> page(int page, int pageSize, String name) { log.info("分页查询,页码={}, 页大小={}, 菜品名={}", page, pageSize, name); Page<FoodDish> pageInfo = new Page<>(page, pageSize); LambdaQueryWrapper<FoodDish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(name != null, FoodDish::getName, name) .orderByDesc(FoodDish::getUpdateTime); dishService.page(pageInfo, queryWrapper); return R.success(pageInfo); } // 根据ID查询菜品(用于回显修改表单) @GetMapping("/{id}") public R<FoodDish> getById(@PathVariable Long id) { FoodDish dish = dishService.getById(id); return dish != null ? R.success(dish) : R.error("未找到该菜品"); } // 修改菜品 @PutMapping public R<String> update(@RequestBody FoodDish dish) { log.info("修改菜品:{}", dish); dishService.updateById(dish); return R.success("修改菜品成功"); } // 删除菜品(逻辑删除或物理删除,此处演示物理删除) @DeleteMapping public R<String> delete(@RequestParam List<Long> ids) { log.info("删除菜品,id={}", ids); dishService.removeByIds(ids); return R.success("删除菜品成功"); } }- 统一返回结果类:在
common包下创建R.java,用于规范API返回格式。
// 文件路径:src/main/java/com/example/foodbackend/common/R.java package com.example.foodbackend.common; import lombok.Data; import java.io.Serializable; @Data public class R<T> implements Serializable { private Integer code; // 编码:1成功,0和其他数字为失败 private String msg; // 错误信息 private T data; // 数据 public static <T> R<T> success(T object) { R<T> r = new R<T>(); r.data = object; r.code = 1; return r; } public static <T> R<T> success(String msg) { R<T> r = new R<T>(); r.msg = msg; r.code = 1; return r; } public static <T> R<T> error(String msg) { R<T> r = new R<T>(); r.msg = msg; r.code = 0; return r; } }5. 前端页面构建与接口调用
5.1 配置前端项目
- 引入Element Plus:修改
src/main.js。
// 文件路径:src/main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' // 引入Element Plus import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' // 引入Axios并全局挂载 import axios from 'axios' const app = createApp(App) // 配置Axios基础URL axios.defaults.baseURL = 'http://localhost:8080' app.config.globalProperties.$axios = axios app.use(router) app.use(ElementPlus) app.mount('#app')- 配置Vue Router:修改
src/router/index.js,添加菜品管理路由。
// 文件路径:src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import DishManagement from '../views/DishManagement.vue' // 待创建 const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: () => import('../views/HomeView.vue') }, { path: '/dish', name: 'dish', component: DishManagement // 菜品管理页面 } // ... 其他路由 ] }) export default router5.2 实现菜品管理页面
创建src/views/DishManagement.vue文件,这是前端核心页面。
<!-- 文件路径:src/views/DishManagement.vue --> <template> <div class="dish-management"> <!-- 搜索和新增区域 --> <el-card class="filter-card"> <el-form :inline="true" :model="queryParams"> <el-form-item label="菜品名称"> <el-input v-model="queryParams.name" placeholder="请输入菜品名称" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">搜索</el-button> <el-button @click="resetQuery">重置</el-button> <el-button type="success" @click="handleAdd">新增菜品</el-button> </el-form-item> </el-form> </el-card> <!-- 菜品表格 --> <el-card class="table-card"> <el-table :data="dishList" border style="width: 100%"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="菜品名称" /> <el-table-column prop="categoryId" label="分类ID" width="100" /> <el-table-column prop="price" label="价格(元)" width="120"> <template #default="scope"> {{ scope.row.price.toFixed(2) }} </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="scope"> <el-tag :type="scope.row.status === 1 ? 'success' : 'info'"> {{ scope.row.status === 1 ? '上架' : '下架' }} </el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="200" fixed="right"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <div class="pagination-container"> <el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :page-sizes="[5, 10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </el-card> <!-- 新增/编辑菜品对话框 --> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px"> <el-form ref="dishFormRef" :model="dishForm" :rules="rules" label-width="80px"> <el-form-item label="菜品名称" prop="name"> <el-input v-model="dishForm.name" placeholder="请输入菜品名称" /> </el-form-item> <el-form-item label="分类" prop="categoryId"> <el-select v-model="dishForm.categoryId" placeholder="请选择分类" style="width: 100%"> <el-option v-for="cat in categoryOptions" :key="cat.id" :label="cat.name" :value="cat.id" /> </el-select> </el-form-item> <el-form-item label="价格" prop="price"> <el-input-number v-model="dishForm.price" :min="0" :precision="2" :step="0.1" style="width: 100%" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="dishForm.status"> <el-radio :label="1">上架</el-radio> <el-radio :label="0">下架</el-radio> </el-radio-group> </el-form-item> <el-form-item label="描述" prop="description"> <el-input v-model="dishForm.description" type="textarea" :rows="3" placeholder="请输入菜品描述" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确定</el-button> </span> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' // 响应式数据 const dishList = ref([]) const total = ref(0) const dialogVisible = ref(false) const dialogTitle = ref('') const categoryOptions = ref([{ id: 1, name: '川菜' }, { id: 2, name: '湘菜' }]) // 模拟分类数据 // 查询参数 const queryParams = reactive({ page: 1, pageSize: 10, name: '' }) // 表单数据 const dishForm = reactive({ id: null, name: '', categoryId: null, price: 0, status: 1, description: '' }) // 表单验证规则 const rules = { name: [{ required: true, message: '请输入菜品名称', trigger: 'blur' }], categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }], price: [{ required: true, message: '请输入价格', trigger: 'blur' }] } // 获取菜品列表 const getList = async () => { try { const response = await $axios.get('/dish/page', { params: queryParams }) if (response.data.code === 1) { dishList.value = response.data.data.records total.value = response.data.data.total } else { ElMessage.error(response.data.msg || '获取列表失败') } } catch (error) { console.error('获取菜品列表失败:', error) ElMessage.error('网络请求失败') } } // 搜索 const handleQuery = () => { queryParams.page = 1 getList() } // 重置搜索 const resetQuery = () => { queryParams.name = '' handleQuery() } // 分页大小变化 const handleSizeChange = (val) => { queryParams.pageSize = val getList() } // 当前页变化 const handleCurrentChange = (val) => { queryParams.page = val getList() } // 新增菜品 const handleAdd = () => { dialogTitle.value = '新增菜品' Object.keys(dishForm).forEach(key => { if (key === 'status') { dishForm[key] = 1 } else if (key === 'price') { dishForm[key] = 0 } else { dishForm[key] = null } }) dialogVisible.value = true } // 编辑菜品 const handleEdit = async (row) => { dialogTitle.value = '编辑菜品' try { const response = await $axios.get(`/dish/${row.id}`) if (response.data.code === 1) { Object.assign(dishForm, response.data.data) dialogVisible.value = true } else { ElMessage.error('获取菜品信息失败') } } catch (error) { ElMessage.error('网络请求失败') } } // 提交表单(新增/编辑) const submitForm = async () => { try { const url = dishForm.id ? '/dish' : '/dish' const method = dishForm.id ? 'put' : 'post' const response = await $axios({ url, method, data: dishForm }) if (response.data.code === 1) { ElMessage.success(response.data.msg) dialogVisible.value = false getList() // 刷新列表 } else { ElMessage.error(response.data.msg) } } catch (error) { ElMessage.error('操作失败') } } // 删除菜品 const handleDelete = (row) => { ElMessageBox.confirm(`确认删除菜品 "${row.name}" 吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { try { const response = await $axios.delete('/dish', { params: { ids: [row.id] } }) if (response.data.code === 1) { ElMessage.success('删除成功') getList() } else { ElMessage.error(response.data.msg) } } catch (error) { ElMessage.error('删除失败') } }).catch(() => {}) } // 页面加载时获取数据 onMounted(() => { getList() }) </script> <style scoped> .dish-management { padding: 20px; } .filter-card { margin-bottom: 20px; } .pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; } </style>6. 运行与效果验证
6.1 启动后端服务
- 在IDEA中找到主启动类(通常名为
FoodBackendApplication),右键运行。 - 观察控制台日志,看到类似
Tomcat started on port(s): 8080的日志,表示启动成功。 - 打开浏览器,访问
http://localhost:8080,如果看到SpringBoot的默认错误页面(Whitelabel Error Page),这是正常的,因为根路径没有映射控制器。可以访问http://localhost:8080/dish/page?page=1&pageSize=10测试接口,应该返回JSON格式的分页数据(初始为空)。
6.2 启动前端服务
- 在
food-frontend目录下打开终端,执行命令:npm run dev - 终端会输出本地访问地址,通常是
http://localhost:5173。 - 在浏览器中访问该地址,你应该能看到Vue的默认页面。在地址栏手动输入
http://localhost:5173/dish跳转到我们刚创建的菜品管理页面。
6.3 功能测试
现在,你可以进行完整的增删改查测试:
- 新增:点击“新增菜品”,填写表单后提交。观察浏览器Network面板,应看到向
http://localhost:8080/dish发送的POST请求成功。 - 查询:新增后页面自动刷新,列表中应出现刚添加的菜品。尝试使用搜索框。
- 编辑:点击某行数据的“编辑”按钮,修改信息后提交。应看到PUT请求。
- 删除:点击“删除”按钮,确认后数据从列表消失。应看到DELETE请求。
- 分页:添加足够多的数据(超过10条),测试分页组件是否正常工作。
如果一切顺利,恭喜你!一个基于SpringBoot和Vue3的前后端分离美食网站管理系统核心模块已经成功运行。
7. 常见问题与排查思路
在搭建过程中,你可能会遇到以下问题。这里提供快速排查路径。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 后端启动失败,端口被占用 | 8080端口已被其他程序(如旧Tomcat)使用 | 查看启动日志中的错误信息 | 1. 在application.properties中修改server.port。2. 命令行执行 netstat -ano | findstr :8080找到占用进程并结束。 |
| 前端无法访问后端接口,出现CORS错误 | 浏览器因同源策略阻止跨域请求 | 打开浏览器开发者工具,查看Console或Network标签下的CORS错误 | 在后端添加CORS配置。创建一个配置类:@Configurationpublic class CorsConfig implements WebMvcConfigurer {@Override public void addCorsMappings(...) { ... }} |
前端页面空白,控制台报错Failed to resolve component | Element Plus组件未正确引入或Vue版本不兼容 | 检查浏览器Console具体错误,检查main.js中Element Plus的引入语句 | 确保安装的element-plus版本与Vue 3兼容。检查导入路径是否正确。 |
| 数据库连接失败 | MySQL服务未启动、密码错误、数据库名不对、时区配置问题 | 查看后端启动日志中的SQL异常堆栈 | 1. 确认MySQL服务已启动。 2. 检查 application.properties中的URL、用户名、密码。3. 在JDBC URL中添加 &serverTimezone=Asia/Shanghai。 |
插入数据时,create_time字段为null | MyBatis-Plus的自动填充功能未生效 | 检查实体类字段注解,检查是否配置了元对象处理器 | 创建一个MetaObjectHandler的实现类,并标注@Component。在insertFill和updateFill方法中设置时间。 |
前端发送请求后,后端接收不到参数(如@RequestBody对象属性为null) | 前后端字段名不匹配(下划线 vs 驼峰)、缺少无参构造器、JSON格式错误 | 查看后端Controller方法接收到的对象,打印日志;检查前端发送的JSON格式 | 1. 确保实体类有@Data注解(Lombok)或显式的getter/setter。2. 使用 @JsonProperty注解指定JSON字段映射。3. 检查前端Axios发送的 data格式。 |
| 打包后运行出错 | 前端资源路径错误、后端依赖缺失 | 分别检查前端vite.config.js中的base配置,后端打包的jar是否包含所有依赖 | 1. 前端:配置base: './'或正确的子路径。2. 后端:使用 mvn clean package打包,用java -jar target/xxx.jar运行。 |
8. 项目扩展与最佳实践建议
完成基础功能后,你可以从这个“骨架”项目出发,进行深度扩展,使其更接近一个真正的企业级项目。
用户认证与权限控制:
- 后端:集成Spring Security + JWT。用户登录后,服务器签发一个Token,后续请求需在Header中携带此Token进行鉴权。
- 前端:在Axios拦截器中统一添加Token,并处理401(未认证)错误,跳转到登录页。
- 最佳实践:将权限细化为菜单权限和按钮权限,实现动态路由。
文件上传(菜品图片):
- 后端:使用SpringBoot的
MultipartFile接收文件,保存到本地目录或云存储(如OSS),将文件访问URL存入数据库。 - 前端:使用Element Plus的
<el-upload>组件。 - 安全提醒:务必校验文件类型、大小,防止恶意上传。
- 后端:使用SpringBoot的
更复杂的业务逻辑:
- 事务管理:在Service方法上添加
@Transactional注解,确保涉及多表修改的操作要么全部成功,要么全部回滚。 - 数据校验:在Controller的入参对象上使用
@Validated注解,并结合JSR-303注解(如@NotBlank,@Min)进行校验。 - 全局异常处理:使用
@ControllerAdvice和@ExceptionHandler统一处理异常,返回友好的错误信息给前端。
- 事务管理:在Service方法上添加
前端工程化:
- API统一管理:将所有的Axios请求封装到单独的
src/api目录下的模块中,便于维护。 - 状态管理:使用Pinia(已在项目创建时安装)来管理全局状态,如用户信息、侧边栏折叠状态等。
- 环境变量:使用
.env.development和.env.production文件管理不同环境的后端API地址。
- API统一管理:将所有的Axios请求封装到单独的
部署上线:
- 前端:执行
npm run build生成静态文件(在dist目录),将其部署到Nginx或对象存储。 - 后端:使用
mvn clean package生成可执行的Jar包,在服务器上通过nohup java -jar xxx.jar &后台运行。更推荐使用Docker容器化部署。
- 前端:执行
这个项目麻雀虽小,五脏俱全。它为你清晰地演示了从数据库设计、后端API开发到前端页面交互的完整闭环。你可以基于此,继续添加用户管理、订单管理、数据统计图表、移动端适配等功能,将其打造成一个丰满的毕业设计或个人作品。
掌握这个流程,意味着你不仅学会了SpringBoot和Vue3的用法,更理解了前后端分离架构下,数据是如何从数据库流动到用户界面的。这才是面试官最看重的“项目经验”背后的真实能力。建议你将这个项目作为模板收藏,在需要快速启动新项目时,它能为你节省大量前期搭建的时间。