1. 这个项目到底是什么一套能真正跑起来的完整餐饮业务闭环先说个很多人容易忽略的常识在 Java Web 方向的课设、毕设和简历项目里管理系统永远比纯前端展示页吃香而餐饮管理系统又是管理系统里综合度最高的那一档。原因不复杂——它同时涉及商品管理、订单流转、库存联动、会员营销、报表统计几乎把一套业务系统该有的模块全占了。你拿这一个项目去讲清楚 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这套技术栈怎么协作比单独写十个 CRUD 例子都有说服力。这套系统的定位不是一个花架子 Demo而是按真实门店使用场景设计的完整业务闭环。从顾客扫码点餐开始到后厨接单出餐再到收银台结算、老板看营业报表整条链路是通的。项目里还包含管理员端和门店操作端两个视角——管理员管菜品分类、上下架、员工账号门店端处理桌台状态、订单接单、会员储值。换句话说它不是看起来功能很多而是每个功能点都有实际业务含义。我在本地把它完整跑通过一遍前后端加数据库加起来大概半小时能启动完。前提是你把环境装好尤其是 MySQL8.0 和 Node 环境这两块最容易出幺蛾子。这篇博文我不打算只贴代码而是从技术选型、目录结构、核心表设计、关键接口实现、前后端联调、部署运行这几个维度把整个项目的设计思路和避坑点全部拆开讲清楚。适合的人群很明确做 Java Web 课设/毕设的学生、准备把管理系统写进简历的初级开发者、以及想快速落地一套 Web 全栈业务系统的朋友。2. 为什么是这套组合SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0 的选型逻辑2.1 SpringBoot2 比 SpringBoot3 更适合拿来做项目现在不少人一上来就追 SpringBoot3但实际落地上SpringBoot2.7.x 仍然是目前课设、毕设和中小型公司存量项目里的绝对主流。原因有三个第一SpringBoot2 的用户基数大你遇到任何报错几乎都能搜到现成的解决方案第二很多第三方框架生成的验证码组件、某些文件上传工具对 SpringBoot3 的 Jakarta 命名空间适配还不完整Boot2 的 javax 体系下兼容性最好第三对学习而言SpringBoot2 和 3 的自动配置原理、starter 机制完全一脉相承学通 2 再切 3 的成本很低。提示本项目用 2.7.x 版本Java 环境用 JDK8 或 JDK11 都行千万别上 JDK17 跑 Boot2.7虽然也能跑但某些老依赖会报模块访问异常没必要折腾。2.2 MyBatis-Plus比 JPA 更好上手比原生 MyBatis 省一半代码关于 Spring Data JPA 和 MyBatis-Plus 的区别我自己的体会是JPA 适合领域模型驱动、表结构不那么固定的项目但它的隐式 SQL 对新手很不友好一个方法名写错你根本不知道它在查什么MyBatis-Plus 则完全站在业务增删改查的角度单表操作连 SQL 都不用写Wrapper 条件构造器写起来像拼积木。选 MyBatis-Plus 还有一个非常现实的原因它自带的分页插件、代码生成器、逻辑删除、自动填充每一样都是做管理系统时刚需中的刚需。比如分页一个订单列表、菜品列表全都需要分页手写 limit 还得自己包 PageResultMP 直接返回 Page 对象前端拿 current、size、total 三件套就走完了。再比如逻辑删除菜品下架肯定不能物理删加一个 TableLogic 注解就解决。2.3 Vue3从 Options API 到 Composition API 的跨代优势前端选 Vue3 不只是因为新而是它和这套项目的契合度确实高。Vue3 的 Composition API 让代码复用变得特别顺——团队里公共的接口请求逻辑、登录态检查逻辑、购物车状态管理全部可以用自定义 Hook 提取出来。而且 Element Plus 组件库现在已经非常成熟做后台管理系统几乎零成本搭界面。用 Vite 做构建工具后冷启动秒开开发体验比 Webpack 时代的 Vue2 强一个量级。2.4 MySQL8.0 与不用 Oracle 也不用 SQL Server的考虑管理系统选数据库MySQL8.0 基本是标准答案。它对窗口函数、公用表表达式WITH AS的支持已经很全做营业报表、排行统计比 5.7 顺滑很多。再加上 MySQL8.0 默认字符集是 utf8mb4直接支持 emoji 和生僻字菜品名称里有个字也不会乱码。如果你用 5.7还得手动改配置文件指定字符集很多新手在数据库初始化那一步就倒下了。8.0 安装完之后默认就是 utf8mb4少操一份心。3. 先按标准目录搭骨架Java Web 项目的目录结构决定你的开发效率这套项目拿到的第一件事不是急着写业务而是把前后端两个工程的目录结构看清楚、理顺了。目录结构这事儿看着基础但很多人项目跑不起来根源就是文件放错了地方。3.1 后端标准分包controller / service / mapper / entity / common后端 Maven 工程采用的是标准的分层架构com.example.restaurant 作为根包下面按职责拆分为五个核心包controller只负责接收请求、参数校验、调用 service不写任何业务逻辑。比如 OrderController 里只有下单、查单、改状态这几个端点。service业务逻辑层接口加实现类分离。订单超时处理、库存联动扣减这些逻辑都放在这里。mapperMyBatis-Plus 的 Mapper 接口层继承 BaseMapper 后单表 CRUD 全免费。多表关联的统计查询写自定义 XML 或注解 SQL。entity数据库表对应的实体类字段和表列一一对应用 TableName 和 TableId 注解标注映射关系。common放通用返回结果Result 、异常处理器、工具类、常量类。这种结构的好处是非常容易定位问题——前端报错你能迅速判断是 controller 层参数问题还是 service 层业务问题而不是在一个几百行的类里从头翻到尾。3.2 前端目录结构src 下的模块化设计前端工程 Vue3 Vite 的标准结构是src/ api/ // 按业务模块拆分的接口请求文件 assets/ // 静态资源 components/ // 通用组件 layout/ // 后台布局框架侧边栏顶栏主内容区 router/ // 路由配置 store/ // Pinia 状态管理 utils/ // 请求封装、工具函数 views/ // 页面视图按模块建子目录需要注意的一个点api 目录要按后端 controller 的模块一一对应比如 菜品管理 对应 dish.js订单管理对应 order.js这样一个后端接口改动时你只需要去对应文件里改维护成本低很多。很多项目到后期改需求改到崩溃就是因为接口请求散落在各个页面组件里。3.3 配置文件分离application.yml 里的三个关键环境后端工程里有 application.yml 作为主配置文件再配合 application-dev.yml开发环境和 application-prod.yml生产环境通过 spring.profiles.active 来切换。数据库连接信息、Redis 地址、JWT 密钥这类敏感配置都放在不同环境里而不是写死在一个文件中。配置里最容易被忽略的就是 MyBatis-Plus 的逻辑删除配置mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这个配置配合实体类上的 TableLogic 注解所有删除操作自动变成 UPDATE有效避免误删数据。检查一个含文档项目是否靠谱我一般先看它有没有配逻辑删除没配的说明开发者根本没有实际运营思维。4. 数据库设计订单、菜品、库存这三张表是怎么连起来的餐饮管理系统看着模块多实际上数据库的核心就是几张关键表菜品表、分类表、桌台表、订单表、订单明细表、会员表、库存表。它们之间的关系捋顺了整个系统就撑起来了。4.1 菜品信息和分类SPU 与 SKU 的简化处理菜品分类表比较简单就是 id、name、sort 三件套。菜品表则用到了简约版的 SPU/SKU 思想——菜品基本信息名称、主图、描述、销量是一层菜品规格大份/小份、加辣/不辣放在 sku 表。订单明细里存的是 sku_id 而不是 dish_id这样不同规格的价格和口味才能区分开。项目思路延伸如果想锦上添花可以给菜品加推荐指数字段前端首页按指数排序展示。这个小功能涉及一个 update 操作加一个列表查询成本极低但对做展示很有帮助。4.2 订单主表和明细表为什么必须拆两张表订单表orders和订单明细表order_detail是典型的主从表结构。订单主表保存整单的维度信息订单号、桌台号、总金额、状态、顾客信息、下单时间明细表保存每一道菜品的数量、单价、小计。这里有个容易犯的错误直接在主表里用一个逗号分隔的字符串存菜品列表。这种设计查单的时候确实方便但后厨要按菜品合并出餐、老板要统计每种菜的销量时SQL 根本写不出来。拆成明细表后一个 GROUP BY 就能算出任何菜品在任意时间段的销量这才是管理系统的管理价值所在。金额字段我强烈建议用 decimal(10, 2)不要用 float 或 double。数据库里的 float 做加法会产生精度误差餐饮账单这种涉及钱的地方一分钱都不能差。4.3 库存联动从扣减到回滚的完整链路库存表的核心字段是原料 id、名称、当前库存、预警阈值、单位。它和菜品的关系是通过 recipe配方表关联的——一道菜品需要哪些原料、各需要多少份。下单时系统查出菜品的配方逐项扣减库存取消订单时回补库存。这个联动逻辑如果放在前端做那延期和超卖就避免不了。正确的做法是在 service 层用事务包裹——扣库存和创建订单放在同一个 Transactional 方法里任何一步失败整体回滚。这个事务处理是整套系统里最值得跟面试官讲的技术点之一比我会增删改查有含金量得多。4.4 时间字段设计create_time 与 update_time 的自动填充两张核心表都包含 create_time 和 update_time 字段。如果你每个插入和更新操作都手动 set 时间不仅代码冗余还容易漏。更专业的做法是用 MyBatis-Plus 的 MetaObjectHandler 实现自动填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }并在实体类字段上加 TableField(fill FieldFill.INSERT) 和 TableField(fill FieldFill.INSERT_UPDATE)。这样订单创建、菜品修改时时间自动写入全项目统一。5. 后端核心功能拆解登录认证、订单状态机、统计报表的实现套路5.1 基于 JWT 的登录认证拦截器 注解 异常处理系统采用无状态登录方案登录接口校验用户名和密码后返回一个 JWT token前端存储并在后续请求头携带。后端的实现分三步登录接口用 BCrypt 校验密码生成 token包含用户 id、角色、过期时间。注册一个拦截器拦截所有需要认证的请求路径/api/** 但排除 /api/login。在拦截器中解析 token失败则直接抛异常由全局异常处理器返回 401 状态码。管理员和门店操作员的角色区分可以在 token 里放一个 role 字段后端在特定的管理端接口上用注解校验角色。这里有个细节JWT 的密钥不能写死在代码里要放在 application.yml 里因为项目文档里其他开发者拿到源码后如果密钥泄漏他们可以任意伪造 token。5.2 订单状态机待付款、已付款、制作中、待取餐、已完成、已取消餐饮订单状态流转是有严格顺序的不能从待付款直接跳到已完成。我的实现方式是在 service 层定义一组状态变更方法每个方法内部校验当前状态是否合法public void pay(Long orderId) { Orders order getById(orderId); if (!Status.WAIT_PAY.equals(order.getStatus())) { throw new BusinessException(当前订单状态不能支付); } // 扣减库存、更新状态 }这种做法的好处是状态流转逻辑集中在一个类里不会出现前端调了个接口就胡乱改状态的情况。状态字段建议用 int 而非字符串因为 int 比较效率更高而且前端只需要几个常量就能映射。5.3 统计报表MyBatis-Plus 自定义 SQL 完成营业额分析报表模块是管理系统的加分项包括今日营收、订单数、客单价、畅销菜品 TOP10。这些统计查询用 Wrapper 不方便我直接在 Mapper 里写了一个自定义 SQLselect idselectDailyReport resultTypecom.restaurant.dto.ReportDTO SELECT DATE(create_time) AS reportDate, COUNT(*) AS orderCount, SUM(total_amount) AS totalAmount FROM orders WHERE create_time BETWEEN #{start} AND #{end} GROUP BY DATE(create_time) ORDER BY reportDate /select核心就是 GROUP BY 加聚合函数。报表查询的数据量通常不大不需要过度设计但索引一定要建好否则数据量上来后统计查询会慢。orders 表的 create_time 字段必须建索引这是这个模块性能的关键。6. 前端实战细节创建 Vue3 项目、请求封装、权限控制到打包6.1 Vite 创建项目与 Element Plus 引入创建项目环境时我建议直接使用 npm create vite而不是用 vue-cli。Vite 创建完的项目结构干净启动速度快配置相对直观。命令如下npm create vitelatest restaurant-web -- --template vue cd restaurant-web npm install npm install element-plus element-plus/icons-vue pinia vue-router axiosElement Plus 可以全量引入也可以按需引入。个人建议做管理系统时全量引入虽然体积稍大但省心。按需引入一旦漏配样式排查起来很费时间违背了先跑通再优化的原则。6.2 axios 封装与请求拦截器统一处理 401 和业务错误所有接口请求必须走统一的 axios 实例而不是每个页面单独创建。封装的核心逻辑有三块请求拦截器从 localStorage 取 token加到请求头的 Authorization 字段。响应拦截器判断 HTTP 状态码和业务状态码。HTTP 401 时清理 token 并跳转登录页业务状态码非 200 时用 Element Plus 的 ElMessage 弹出错误提示。统一 baseURL开发环境用/api通过 Vite 代理转发到后端的http://localhost:8080避免跨域问题生产环境可以通过环境变量切换真实地址。6.3 路由守卫未登录跳转登录页与页面级权限Vue Router 的路由守卫是必须写的。一套管理系统如果没有登录拦截用户直接访问 /order/list 也能看到数据那 JWT 认证就是摆设。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这个守卫还能顺便处理已登录但访问登录页的情况——如果已登录还要去 /login直接重定向到首页。细节虽小但体验差异很明显。6.4 联调阶段最容易踩的坑代理配置与端口冲突前后端联调时最常见的报错就是跨域和被拦截。跨域的解决方案是在 vite.config.js 里配置 proxyserver: { host: 0.0.0.0, port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这里有个容易忽视的细节如果后端接口路径本身带 /api 前缀前端请求就用 /api/xxx 直接代理如果后端不带则需要用 rewrite 做路径重写。不少人在这一步反复配不通根本原因是前后端路径设计没对齐。端口冲突也常见——默认 Vite 是 5173如果你电脑上 5173 被占了Vite 会自动换端口但 element 页面里的跳转地址写死的还是老端口就会莫名访问不了。我的习惯是显式指定 host 和 port避免自动切换造成环境不一致。7. MySQL8.0 的安装与连接配置从头避坑到稳定跑通7.1 Windows 下安装用安装包还是解压版MySQL8.0 在 Windows 下建议直接用官方安装包MySQL Installer它会帮你自动配置服务、环境变量和初始密码重置流程。如果你要用免安装的 zip 解压版很多人提 Navicat 免安装版但 MySQL 本身我建议装原版步骤会麻烦一些解压后在根目录新建 my.ini 配置文件。以管理员身份运行 cmd进入 bin 目录。mysqld --initialize-insecure 初始化目录生成一个无密码的 root 用户。mysqld -install 注册 Windows 服务。net start mysql 启动服务。第 3 步用 --initialize-insecure 还是建议的免去第一次登录时去找随机密码的环节。但生产环境一定不能留空密码初始化后要立刻改。7.2 连接报 2059 错误的根因与解决用 Navicat 连接 MySQL8.0 时经常会碰到 2059 错误原因是 MySQL8.0 默认的认证插件是 caching_sha2_password而旧版客户端只支持 mysql_native_password。网上很多教程让你改加密规则但更稳妥的做法是升级 Navicat 版本新版完全兼容。如果你不想升级数据库侧也可执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;但注意这种方式在 MySQL 8.4 里已经被标记为废弃。从长期角度看跟上工具版本才是正解。7.3 时区和编码设置今天 22:00 的订单别变成昨天mysql8.0 连接 URL 必须在后面带时区参数否则控制台会直接报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这个经典的乱码时区错误。正确连接串是jdbc:mysql://localhost:3306/restaurant?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue参数含义逐一说清楚useUnicode characterEncoding保证中文存储不乱码。serverTimezone指定时区。useSSLfalse本地开发没必要用 SSL减少握手时间。allowPublicKeyRetrievaltrue连接 caching_sha2_password 认证时必须要加否则某些驱动会报公钥检索失败。7.4 本地库建好后项目初始化 SQL 脚本怎么导入项目文档里一般会附带 restaurant.sql 脚本。导入前先新建数据库再导入脚本CREATE DATABASE restaurant DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后使用命令mysql -u root -p restaurant restaurant.sql导入Windows 下在 bin 目录里操作或者在 Navicat 里右键数据库运行 SQL 文件。建议用命令行方式导入速度更快也更稳定。8. 从零到一跑通全流程启动顺序、验证方法和我遇到的坑8.1 启动顺序先 MySQL再后端最后前端整个系统跑起来的顺序是个关键经验。先启动 MySQL 服务任务管理器确认 mysqld 在运行再启动后端 SpringBoot 应用它会自动连接数据库最后启动前端 Vite 服务。前端启动没问题不代表后端 OK后端控制台出现Started RestaurantApplication字样才算真正成功。后端启动后可以用接口文档工具测试登录接口拿到 token 后再测菜品列表接口。这样能快速定位问题是出在认证还是数据查询。8.2 我在复现过程中的三个记忆深刻的报错第一个是Invalid bound statement (not found)。这个报错的前因是 Mapper 接口和 XML 文件没放到同一个包路径下。如果你把 XML 放在 resources/mapper 目录就必须在 application.yml 里配置 mapper-locationsmybatis-plus: mapper-locations: classpath*:mapper/**/*.xml配置完重启问题解决。很多新手在这一步会卡很久因为报错信息并不直说XML 没找到而是抛一个 Statement not found 的异常。第二个是表名或字段名与 MySQL 关键字冲突。订单表如果命名为 order在 MySQL 里会直接报语法错误因为 order 是排序关键字。项目里用的表名是 orders规避了这个问题。同理字段名不要用 desc、rank、condition 这类词大概率是保留字。第三个是Vue3 项目在部分浏览器里异常退出的怪问题。有人反馈说页面在 Edge 浏览器下偶尔会出现右上按钮无响应最后排查是浏览器插件冲突。这类问题和项目本身关系不大但也要知道——当本地跑得好好的、换个环境就出怪事时先怀疑环境而不是代码。8.3 赶时间的朋友可以直接复制这套部署检查单每次部署前按下面的清单过一遍能省掉大半的报错时间MySQL 服务已启动且端口 3306 没被占用用netstat -ano | findstr 3306查。后端数据库账号密码与 application-dev.yml 一致。本机 JDK 版本与 pom.xml 中 java.version 匹配。前端依赖已安装node_modules 存在且代理 target 端口为后端实际端口。所有路径中的端口号没有写死成过期的值。测试时先用管理员账号登录后台创建一条菜品分类再添加几个菜品然后模拟用户下单一整单看订单状态流转和库存扣减是否正常。这套验证流程走完系统基本就是可跑的。个人经验上最容易被忽略的反而是软件版本之间细微的兼容问题。比如 JDK 版本过高导致 Lombok 失效、Node 版本过低导致 Vite 装不上、MySQL8.0 的驱动版本过旧导致认证报错。如果跑项目时遇到看似配置都对但就是不行的状态第一反应先去确认工具链版本这比反复检查代码有效得多。