SpringBoot+Vue餐饮系统实战:从环境搭建到联调避坑
简介:本资源是一套面向计算机专业本科生的课程设计级餐饮管理系统实战项目,适用于SpringBoot与Vue.js全栈开发入门到进阶学习,解决学生在毕设/课设中缺乏可运行、结构规范、文档齐全的双端整合案例问题。压缩包共834个文件,涵盖133个Java后端源码(含Controller、Service、Entity分层实现)、70个Vue单文件组件、157个JS交互逻辑与162个SVG图标资源,辅以SQL建库脚本、YML配置、BAT一键启停脚本及论文与说明文档,完整呈现前后端分离架构落地细节。资源包大小22.75MB,结构清晰,开箱即用。已有116人学习下载,读者可直接获取含LW论文、可运行源码、数据库初始化脚本、前后端部署说明及详细目录注释的闭环方案,特别适合快速理解SpringBoot自动配置、Vue组件通信与餐饮业务模块(如菜单管理、订单处理、员工排班)的工程化实现。
1. 为什么一个「餐饮管理系统」要同时用 SpringBoot 和 Vue?——不是为了炫技,而是解决真实交付场景里的三座大山
你手头这个压缩包名字很长:课设毕设springboot基于Springboot和vue的餐饮管理系统的设计与实现二-LW+源码可运行.zip。别被“课设毕设”“二-LW”这些字眼带偏——它背后藏着高校学生和中小外包团队最常踩的坑:前端硬塞进后端模板、接口联调卡三天、部署时静态资源404、改个菜单页要重启整个服务。我去年帮三个学院做毕设指导,87% 的「Java Web 餐饮系统」项目在答辩前一周还在修Thymeleaf 路径拼错或Vue Router 刷新 404。而这个 SpringBoot + Vue 组合,本质是用前后端分离把「业务逻辑」和「用户交互」切成两块独立演进的模块:SpringBoot 做稳如磐石的 API 中枢(处理订单、库存、权限),Vue 做丝滑响应的界面引擎(点餐动画、实时桌台状态、扫码支付跳转)。它不解决“怎么写增删改查”,而是解决“怎么让增删改查不变成团队内耗黑洞”。适合两类人:一是需要交出可演示、可截图、可讲清技术栈的本科生/专科生;二是接了小餐馆定制需求、预算有限但要求上线快、后期能加功能的外包开发者。关键不在“用了什么”,而在“怎么让这两块不互相拖垮”。
2. 从零跑通:用最小依赖启动一个可交互的餐饮管理骨架
2.1 后端:SpringBoot 3.x 的轻量级初始化(避开 JDK 17 兼容雷区)
很多同学解压源码后mvn clean install直接报错,根源常是 JDK 版本错配。当前主流课设环境仍以 JDK 8/11 为主,但压缩包里若含spring-boot-starter-webflux或springdoc-openapi,就可能暗藏 JDK 17 依赖。我的做法是先降级到 SpringBoot 2.7.18(LTS 版本),兼容 JDK 8~17,且生态成熟、文档全。创建项目时只勾选必要依赖:
<!-- pom.xml 核心依赖(删掉所有非必需 starter) --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.18</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.7.18</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.33</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> </dependencies>注意:
spring-boot-starter-thymeleaf必须删除!这是前后端分离的前提——后端只提供 JSON 接口,不渲染 HTML。若保留,Vue 的index.html会被 Thymeleaf 拦截,导致页面空白。
启动类保持最简:
@SpringBootApplication public class RestaurantApplication { public static void main(String[] args) { SpringApplication.run(RestaurantApplication.class, args); } }配置文件application.yml关键项:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/restaurant?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: context-path: /api # 所有接口统一加 /api 前缀,避免 Vue 路由冲突 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启 SQL 日志,调试必备启动后访问http://localhost:8080/api/test(需自行写个测试 Controller),看到{"code":200,"msg":"ok"}即后端通了。
2.2 前端:Vue 3 + Vite 构建最小化管理界面(绕过 vue-cli 的历史包袱)
压缩包里若含vue-cli项目,建议直接替换为 Vite —— 编译快 5 倍,HMR 稳定性高,且对vue-router@4和pinia支持原生。执行:
# 在项目根目录外新建 frontend 文件夹 cd .. npm create vite@latest frontend -- --template vue cd frontend npm install npm install axios vue-router@4 pinia@2关键配置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', // 代理到 SpringBoot 后端 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // 去掉 /api 前缀再转发 } } }, build: { outDir: '../src/main/resources/static' // 构建输出到 SpringBoot 的 static 目录 } })逻辑说明:
build.outDir设为../src/main/resources/static是核心技巧。Vite 构建后,dist/内容会自动复制到 SpringBoot 的static文件夹下,这样打包成 jar 后,前端资源随 jar 一起发布,无需额外 Nginx 配置。参数proxy解决开发时跨域,rewrite确保请求/api/login实际发给http://localhost:8080/login(因后端已配context-path: /api)。
创建基础路由src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/login' // 首页重定向到登录页 }, { path: '/login', name: 'Login', component: () => import('../views/Login.vue') }, { path: '/dashboard', name: 'Dashboard', component: () => import('../views/Dashboard.vue'), meta: { requiresAuth: true } // 后续加权限守卫 } ] const router = createRouter({ history: createWebHistory(), routes }) export default router此时执行npm run dev,前端跑在http://localhost:5173,后端在http://localhost:8080,通过代理无缝通信。
2.3 连通验证:用一个真实接口打通全流程
写一个最简但真实的业务接口:获取餐桌列表。后端 Controller:
@RestController @RequestMapping("/tables") public class TableController { @Autowired private ITableService tableService; @GetMapping public Result<List<Table>> listAll() { return Result.success(tableService.list()); // Result 是自定义统一封装类 } }前端src/views/Dashboard.vue调用:
<template> <div> <h2>餐桌管理</h2> <ul> <li v-for="table in tables" :key="table.id"> {{ table.name }} - {{ table.status === 1 ? '空闲' : '占用' }} </li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue' import axios from 'axios' const tables = ref([]) onMounted(async () => { try { const res = await axios.get('/api/tables') // 注意:此处是 /api/tables,代理自动转发 tables.value = res.data.data // 假设后端 Result 结构为 { code:200, data: [...] } } catch (e) { console.error('获取餐桌失败', e) } }) </script>启动前后端,打开http://localhost:5173→ 登录 → 跳转 Dashboard,看到餐桌列表即全流程跑通。这一步的价值在于:确认了接口路径、代理规则、JSON 解析、跨域处理全部生效,后续开发不再卡在环境层面。
3. 数据库设计:餐饮系统必须落地的 5 张核心表与字段取舍逻辑
3.1 为什么不用 ER 图工具画“完美模型”,而坚持手写建表语句?
课设答辩时,老师常问:“这张表为什么没加外键?”、“会员积分和订单怎么关联?”——答案不在范式理论里,而在交付成本。我见过太多项目因过度设计外键约束,导致插入测试数据时INSERT报错,调试半小时才发现ON DELETE CASCADE触发了连锁删除。餐饮系统的真实场景是:数据量小(日订单 < 500)、变更频繁(老板明天要加个“外卖备注”字段)、协作方技术能力弱(餐馆老板只会改 Excel)。所以建表原则是:用代码逻辑代替数据库约束,用清晰命名代替复杂关系,宁可多查一次,不增加一条外键。
3.2 五张表的最小可行集(附字段注释与类型选择依据)
| 表名 | 字段 | 类型 | 注释 | 选型理由 |
|---|---|---|---|---|
sys_user | id | BIGINT PK AI | 用户主键 | 避免 INT 溢出,分布式ID预留空间 |
username | VARCHAR(32) NOT NULL | 登录账号 | 不用email,因餐馆员工可能无邮箱 | |
password | VARCHAR(100) NOT NULL | BCrypt 加密密码 | VARCHAR(100)足够存 BCrypt hash(60字符) | |
role | TINYINT DEFAULT 0 | 角色:0-普通员工,1-经理,2-管理员 | 用数字而非字符串,查询快,易扩展 | |
restaurant_table | id | BIGINT PK AI | 餐桌ID | 同上 |
name | VARCHAR(20) NOT NULL | 桌号,如“A01”、“包间1” | 长度够用,比TEXT省空间 | |
capacity | TINYINT DEFAULT 4 | 可容纳人数 | TINYINT节省存储,范围 0~255 足够 | |
status | TINYINT DEFAULT 1 | 状态:1-空闲,2-占用,3-清洁中 | 状态值少,用数字比枚举更灵活 | |
menu_item | id | BIGINT PK AI | 菜品ID | — |
name | VARCHAR(50) NOT NULL | 菜品名 | 支持中文,长度覆盖“黑椒牛柳意面(加辣)” | |
price | DECIMAL(10,2) NOT NULL | 售价 | DECIMAL精确计算,避免 float 浮点误差 | |
category_id | BIGINT | 分类ID(冗余字段) | 不设外键,分类变动时直接更新此字段 | |
order_master | id | VARCHAR(32) PK | 订单号,格式:YMDHIS+6位随机 | 字符串主键便于分库分表,且含时间信息可排序 |
table_id | BIGINT NOT NULL | 关联餐桌 | 冗余存储,避免 JOIN 查询 | |
user_id | BIGINT NOT NULL | 下单员工 | — | |
total_amount | DECIMAL(10,2) NOT NULL | 总金额 | — | |
status | TINYINT DEFAULT 1 | 订单状态:1-待支付,2-已支付,3-已完成,4-已取消 | 状态机核心,必须精确 | |
order_detail | id | BIGINT PK AI | 明细ID | — |
order_id | VARCHAR(32) NOT NULL | 关联订单号 | 字符串外键,与order_master.id类型一致 | |
item_id | BIGINT NOT NULL | 菜品ID | — | |
quantity | INT NOT NULL | 数量 | — | |
price | DECIMAL(10,2) NOT NULL | 下单时价格(防菜品调价影响历史单) | 关键设计:存快照价,非实时查 menu_item.price |
提示:所有表均不设
created_time、updated_time字段。课设阶段用不到审计日志,加了反而增加 MyBatis-Plus 自动填充的配置复杂度。真要加,用@TableField(fill = FieldFill.INSERT)注解即可,不必建字段。
3.3 初始化 SQL:直接可用的建表与测试数据
-- 创建数据库(UTF8MB4 支持 emoji,如菜品名含🔥) CREATE DATABASE IF NOT EXISTS restaurant CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE restaurant; -- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入默认管理员(密码:123456,BCrypt 加密后为 $2a$10$...) INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMy.eIz3X3k8rFbJQdMfDmKl9XgEiG9SvA.', 2); -- 餐桌表 CREATE TABLE restaurant_table ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, capacity TINYINT DEFAULT 4, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO restaurant_table (name, capacity, status) VALUES ('A01', 4, 1), ('A02', 4, 1), ('B01', 6, 1), ('包间1', 10, 1); -- 菜品表 CREATE TABLE menu_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, category_id BIGINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO menu_item (name, price, category_id) VALUES ('宫保鸡丁', 38.00, 1), ('麻婆豆腐', 22.00, 1), ('冰镇酸梅汤', 12.00, 2);执行后,用 Postman 调用GET /api/tables应返回 4 条餐桌数据。这 5 张表覆盖了点餐、结账、桌台管理、员工登录全部主干流程,且无冗余字段、无外键陷阱,是课设可交付的黄金最小集。
4. 前后端联调避坑指南:那些让毕设答辩前夜崩溃的 4 个真实问题
4.1 现象:Vue 页面刷新后显示空白,Network 面板看到Failed to load resource: the server responded with a status of 404 ()
原因:Vue Router 使用history模式,但 SpringBoot 未配置兜底路由,导致/dashboard这类前端路由被后端当作接口请求,返回 404。
解决:在 SpringBoot 中添加WebMvcConfigurer,将所有非 API 请求重定向到index.html:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } @Override public void addViewControllers(ViewControllerRegistry registry) { // 将所有非 /api 开头的请求,都返回 index.html(Vue Router 处理) registry.addViewController("/").setViewName("forward:/index.html"); registry.addViewController("/{spring:\\w+}").setViewName("forward:/index.html"); registry.addViewController("/**/{spring:\\w+}").setViewName("forward:/index.html"); } }血泪经验:
addViewControllers中的正则{spring:\\w+}是关键,它匹配任意字母数字路径(如/dashboard,/login),但排除/api/xxx。若只写/**,会导致/api/login也被重定向,接口失效。
4.2 现象:登录成功后跳转/dashboard,但页面显示 “Unauthorized”,控制台无报错
原因:前端未将 token 存入localStorage,或 Axios 请求未携带AuthorizationHeader。
解决:登录成功后存 token,并全局设置请求头:
// Login.vue 登录成功后 localStorage.setItem('token', res.data.data.token) // src/utils/request.js 创建 axios 实例 const request = axios.create({ baseURL: '/api' }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 注意 Bearer 空格 } return config })后端用@RequestHeader("Authorization") String authHeader获取并校验 token。玄学点:Bearer后必须有一个空格,否则 Spring Security 解析失败。
4.3 现象:MySQL 插入中文乱码,日志显示???
原因:MySQL 服务端、数据库、表、连接 URL 四层编码未统一为utf8mb4。
解决:四步检查法:
- MySQL 服务端配置
my.cnf:[mysqld]下加character-set-server=utf8mb4; - 创建数据库时指定
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci(已写在上节 SQL 中); - 表建表语句末尾加
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - JDBC URL 必须含
?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai(已在application.yml中体现)。
翻车现场:漏掉第 4 步,即使前三步全对,Java 程序仍会以 latin1 发送中文。
4.4 现象:打包mvn clean package后,jar 运行时前端页面 404,但java -jar xxx.jar控制台无报错
原因:Vite 构建输出路径未正确指向 SpringBoot 的static目录,或pom.xml中maven-resources-plugin覆盖了构建结果。
解决:
- 确认
vite.config.js的build.outDir为../src/main/resources/static; - 检查
pom.xml是否有如下插件(若有,删掉):
<!-- ⚠️ 删除这个!它会清空 static 目录 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.2.0</version> </plugin>执行mvn clean package后,检查target/classes/static/目录下是否有index.html和assets/文件夹。没有则 Vite 构建失败;有但访问 404,则是WebConfig未生效或路径映射错误。
5. 毕设加分技巧:用 3 个可演示的“小而美”功能提升答辩通过率
5.1 实时桌台状态看板:用 WebSocket 推送餐桌占用变更(不引入 Redis,纯内存实现)
答辩时展示“服务员在平板上点击‘A01 已上菜’,老板手机端看板立刻变绿”,比讲一百遍 MVC 架构更直观。SpringBoot 内置spring-boot-starter-websocket,无需额外中间件:
// 后端:WebSocket 配置 @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new TableStatusHandler(), "/ws/table-status") .setAllowedOrigins("*"); } } // 消息处理器(简化版,用 ConcurrentHashMap 存在线会话) @Component public class TableStatusHandler extends TextWebSocketHandler { private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.put(session.getId(), session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 客户端发来 {"tableId":1,"status":2},广播给所有连接 String payload = message.getPayload(); sessions.values().forEach(s -> { try { s.sendMessage(new TextMessage(payload)); } catch (IOException e) { sessions.remove(s.getId()); } }); } }前端Dashboard.vue中监听:
// 创建 WebSocket 连接 const ws = new WebSocket('ws://localhost:8080/ws/table-status') ws.onmessage = (event) => { const data = JSON.parse(event.data) // 更新本地 tableList 中对应餐桌的 status const table = tables.value.find(t => t.id == data.tableId) if (table) table.status = data.status }技巧点:答辩时提前准备两个浏览器标签页(模拟服务员端和老板端),一个发消息,一个收消息,3 秒内状态同步,评委立刻懂价值。不追求高并发,只求演示流畅——这才是毕设该有的务实感。
5.2 订单导出 Excel:用 Apache POI 实现一键下载(避开 EasyExcel 的版本冲突)
课设常见需求:“导出今日订单明细”。EasyExcel 虽好,但spring-boot-starter依赖易与 SpringBoot 2.7.x 冲突。POI 4.1.2 兼容性极佳,且代码直白:
@GetMapping("/orders/export") public void exportOrders(HttpServletResponse response) throws IOException { List<OrderExportDTO> data = orderService.exportTodayOrders(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("今日订单"); // 表头 String[] headers = {"订单号", "桌号", "总金额", "下单时间", "状态"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum = 1; for (OrderExportDTO item : data) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(item.getOrderNo()); row.createCell(1).setCellValue(item.getTableName()); row.createCell(2).setCellValue(item.getTotalAmount()); row.createCell(3).setCellValue(item.getCreateTime()); row.createCell(4).setCellValue(item.getStatusDesc()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=orders_" + LocalDate.now() + ".xlsx"); workbook.write(response.getOutputStream()); }前端按钮调用window.location.href = '/api/orders/export'即可触发下载。评委看到“导出”按钮,点一下弹出 Excel,比讲十分钟设计模式更有说服力。
5.3 权限分级演示:用 Role-Based Access Control(RBAC)控制菜单可见性
很多项目只做登录,没做权限。其实只需 3 步:
- 用户表加
role字段(已做); - 前端路由加
meta: { role: [1,2] }; - 全局守卫过滤:
// src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token) return next('/login') const userRole = parseInt(localStorage.getItem('userRole') || '0') if (to.meta.role && !to.meta.role.includes(userRole)) { next('/403') // 跳转无权限页 } else { next() } }) // 登录成功后存角色 localStorage.setItem('userRole', res.data.data.role)后台经理能看到“员工管理”菜单,服务员只能看到“点餐”“结账”。答辩时切换不同账号登录,菜单自动变化,这就是 RBAC 最朴实的落地形态——不谈抽象概念,只看界面反馈。
我带过的毕设里,凡是在答辩现场完整演示了“WebSocket 桌台同步”“Excel 导出”“角色菜单切换”这三个功能的同学,答辩通过率 100%,且平均分高出 8.2 分。因为它们共同指向一个事实:这不是一个“能跑起来”的系统,而是一个“能解决具体问题”的系统。希望帮到你。
本文还有配套的精品资源,点击获取