ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue餐饮管理系统:前后端分离项目设计与避坑指南

SpringBoot+Vue餐饮管理系统:前后端分离项目设计与避坑指南 简介前后端分离架构已成为现代Web开发的基石SpringBoot以其自动配置和内嵌容器简化了后端服务构建Vue则通过组件化与数据驱动提升前端开发效率。两者结合不仅适用企业级应用也常被用于课程设计与毕业设计以最小成本呈现完整的增删改查链路。本文从数据库表设计、接口分层、JWT鉴权到Vue3Element Plus前端实现梳理餐饮管理系统的核心开发流程同时总结依赖版本、跨域代理、时间时区等高频问题帮助开发者快速落地一套能演示、能答辩的工程化项目。1. 为什么SpringBootVue餐饮管理系统成了课设毕设标配如果你正在做课设或毕设大概率见过这个标题基于SpringBoot和Vue的餐饮管理系统。光是被下载的次数就能说明问题因为它同时踩中了两个最稳妥的点业务场景够日常点餐、菜品、订单谁都能理解技术栈够新SpringBoot后端 Vue前端分离。这个标题背后通常是一整套可运行的源码加配套文档拿到手不是让你背代码而是让你看懂一条完整的前后端分离链路——从数据库表设计到接口请求再到页面渲染每一步都能在答辩时讲出理由。这套系统并不复杂但它能让你用最小成本学会一件事怎么把一套增删改查系统做成别人眼里“有工程感”的项目。接下来我按自己做过的方案把它拆成能照着复现的步骤也会把下载源码后最容易翻车的地方一次说清。2. 从选题到表结构餐饮管理系统的技术骨架先立住2.1 为什么不是SSH、不是JSP而是SpringBootVue很多课设模板还在用JSPServlet或者SSHStrutsSpringHibernate这些技术不是说不能用而是你在简历上写出来会显得时间停滞在2015年。SpringBoot的自动配置把大量XML配置干掉了内嵌Tomcat让部署变成一个java -jar命令Vue则把页面拆成组件数据驱动视图前端代码再也不是一堆$(#id).html()。前后端分离还有一个隐藏优势答辩时你可以强调“前后端通过JSON交互后端不关心页面渲染”这句话本身就是加分项。而且SpringBoot和Vue的生态足够大遇到问题搜索答案特别容易不会像SSH那样查半天都是十几年前的老帖子。从运行角度来看SpringBootVue的启动流程非常直观后端启动一个独立服务默认8080前端启动一个开发服务器默认5173或8081前端通过HTTP调用后端接口。你下载的源码里如果已经是前后端分离结构那么根目录下通常会有backend和frontend这样的文件夹或者一个放Java代码一个放Vue工程。2.2 用户、菜品、订单、桌台四张核心表怎么设计餐饮管理系统的表结构不需要搞得很复杂但四张表是无论如何都跑不掉的用户表、菜品表、订单表、订单明细表。再加上可选的桌台表和分类表基本就覆盖了“点餐—下单—结账”的完整流程。用户表主要用于登录认证区分管理员和普通操作员。菜品表保存菜名、价格、图片、分类和上下架状态。订单表记录是谁在哪个桌台下的单、总金额、下单时间、订单状态订单明细表则记录每道菜的数量和单价因为菜品价格可能会改明细中的价格必须冗余保存。我的建议是给每张表都加上create_time和update_time不要小看这两个字段答辩时老师问“怎么排查数据异常”你至少能说一句“我看创建时间和更新时间”。另外订单状态不要用字符串存用整数0待支付、1制作中、2已完成、3已取消前端再映射成中文这样查询和统计都方便。下面是一个简化版的订单表和明细表DDL你导入数据库后可以直接用CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) DEFAULT NULL COMMENT 订单编号, table_id bigint(20) DEFAULT NULL COMMENT 桌台id, user_id bigint(20) DEFAULT NULL COMMENT 操作员id, total_amount decimal(10,2) DEFAULT NULL COMMENT 总金额, status tinyint(4) DEFAULT 0 COMMENT 0待支付 1制作中 2已完成 3已取消, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_items ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) DEFAULT NULL, dish_id bigint(20) DEFAULT NULL, dish_name varchar(50) DEFAULT NULL COMMENT 冗余菜名, price decimal(10,2) DEFAULT NULL COMMENT 下单时价格, quantity int(11) DEFAULT 1, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表里我用table_id关联桌台而不是直接存桌台名这样后面如果要扩展“桌台状态”空桌/用餐中就有地方接。order_items冗余了dish_name和price原因是菜品表和明细表是两类生命周期的数据菜品可以被修改甚至删除但历史订单里的信息必须保持不变。这里有个常见误用很多人喜欢把所有菜品信息都塞进一张“订单详情”文本字段用逗号分隔查询时再拆分。别这么干答辩时老师一句“怎么统计销量”就会把你问住。规范的设计就是主表加明细表两张表用order_id关联。3. 后端接口怎么落SpringBoot的三层结构与点餐链路3.1 先搭骨架依赖、配置、统一返回拿到源码或者自己新建项目时pom.xml里最关键的依赖是这几个Spring Web、MyBatis或MyBatis-Plus、MySQL驱动、Lombok、JWT相关库。如果你用的是MyBatis-Plus单表CRUD连SQL都不用写这是课设项目提速的最大神器。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意MyBatis-Plus版本不要乱升3.5.x和SpringBoot 2.x完全兼容如果你用的是SpringBoot 3.x需要切换成mybatis-plus-spring-boot3-starter。很多人下载的源码跑不起来一半的原因都出在这个版本匹配上。application.yml里最需要注意的是数据库连接和MyBatis配置。密码不要直接写root的明文其实课设项目写好也无所谓但如果你要传到GitHub上建议用环境变量占位spring: datasource: url: jdbc:mysql://localhost:3306/canteen_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:root} jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必须开这样数据库的order_no能自动映射到Java实体里的orderNo省掉一堆写resultMap的时间。log-impl建议开发时开着能直接在控制台看到SQL排错时特别有用。统一返回结构是很多课设源码里没有的东西但强烈建议加。用一个Result对象包住code、message和data所有接口都返回它前端处理逻辑就能统一起来。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.setCode(500); r.setMessage(msg); return r; } }为什么推荐这个类因为前端axios拦截器只需要判断code 200就能决定走成功还是失败分支不需要每个接口单独处理异常。答辩时你还能指出“统一返回结构让前后端联调成本降低了”。3.2 从菜品到订单Service层别把业务写在Controller里很多课设源码的Controller里直接写数据库操作那是不对的。正确的做法是Controller只做参数接收和结果返回业务逻辑放在Service数据访问放在Mapper。核心原因有两条一是代码能复用比如下单和结账都要算总价你写两遍就会出漏子二是事务好控制下单必须同时写订单表和明细表这必须放在一个事务方法里。拿点餐这个核心场景举例前端传过来的是一个订单主数据加明细列表后端需要一个原子操作Service public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService { Autowired private OrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public Long createOrder(OrderDTO dto) { // 1. 计算总价 BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 插入订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setTableId(dto.getTableId()); order.setUserId(dto.getUserId()); order.setTotalAmount(total); order.setStatus(0); this.save(order); // 3. 插入明细表 for (OrderItemDTO item : dto.getItems()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(item.getDishId()); orderItem.setDishName(item.getDishName()); orderItem.setPrice(item.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } return order.getId(); } }这段代码里Transactional(rollbackFor Exception.class)是整个方法的关键。如果不加事务步骤3中间任何一条SQL失败步骤2的订单主表已经写进去了数据库里就会出现一张没有明细的“孤儿订单”。加了事务任何异常都会回滚。这里的OrderDTO建议用Data的类接收前端JSON而不是直接把Order实体暴露给前端。原因是前端传来的字段和数据库实体可能不一致比如前端会传items列表实体类并没有这个字段直接用实体收参Jackson会报未知字段或者直接丢掉而DTO能清晰地表达接口契约。3.3 JWT登录与拦截器给接口加一道门餐饮管理系统不是所有接口都能匿名访问。用户登录后才能下单管理员才能管理菜品。常见做法是登录成功时签发一个JWT前端把Token存在本地每次请求带上Authorization头后端用拦截器校验。登录接口的逻辑很简单根据用户名查出用户比对密码如果密码是MD5或BCrypt加密的话还需要额外处理。这里我建议用BCrypt因为MD5在答辩时很容易被问“彩虹表攻击怎么办”。PostMapping(/login) public ResultString login(RequestBody LoginDTO dto) { User user userService.getOne(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (user null) { return Result.error(用户不存在); } if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); return Result.ok(token); }JwtUtil是一个工具类内部用io.jsonwebtoken库生成和解析Token。生成时建议放入userId和username两个声明过期时间设置为24小时。注意密钥不要写在代码里放到配置文件里。拦截器实现HandlerInterceptor接口在preHandle里从请求头拿Token并解析解析失败就返回401。这里有一个课设源码常见的坑拦截器拦截了所有接口但登录接口本身也被拦了导致永远登不进去。解决办法是放行登录和注册相关URLregistry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /api/auth/register);excludePathPatterns里还可以放图片访问路径比如/images/**否则前端加载菜品图片时也会被鉴权拦住。4. 前端页面怎么落Vue3 Element Plus 把管理端串起来4.1 初始化Vue3项目并配置代理现在你拿到的源码多半是Vue3 Vite Element Plus的组合而不是老的Vue2。如果你要自己初始化命令是npm create vitelatest frontend -- --template vue cd frontend npm install npm install element-plus axios vue-router pinia这里的--template vue会生成Vue3单文件组件结构。安装Element Plus后建议按需引入而不是全量引入当然课设项目全量引入其实问题也不大就是首屏包体积大一点。我自己做会全量引入因为省事// main.js import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)关键配置是Vite代理。后端跑在8080前端跑在5173如果前端直接请求http://localhost:8080/api/xxx会遭遇跨域。解决办法是在vite.config.js里配置代理export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这个代理的意思是前端所有以/api开头的请求Vite开发服务器会转发到http://localhost:8080。这样前端只需要写axios.get(/api/dishes)不需要写完整地址。很多人运行起来后发现接口404八成就是这个代理没配或者后端接口路径根本不带/api前缀。4.2 登录页与路由守卫登录页是管理系统的脸面。用Element Plus的el-form做表单校验用户名和密码非空。提交成功后把Token存到localStorage然后用router.push(/home)跳转。路由守卫是必做的一环不然用户直接在地址栏输入/home就能绕过登录访问管理页。// router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这段代码放在router/index.js里每次路由跳转前都会执行。逻辑很简单目标不是登录页且没有Token就强制回到登录页。你可以在答辩时补充一句“路由守卫只能控制前端页面访问真正安全还是要靠后端的JWT拦截器”。axios请求拦截器里也要加上Token注入axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })和后端拦截器的excludePathPatterns对应登录接口不能带Token其余接口都要带。如果后端返回401前端响应拦截器里可以做统一跳转axios.interceptors.response.use( res { if (res.data.code 401) { localStorage.removeItem(token) router.push(/login) } return res }, err { return Promise.reject(err) } )4.3 菜品管理页表格、弹窗、上传一条龙菜品管理页是餐饮系统前端最典型的CRUD界面。用el-table展示菜品列表el-dialog做新增和编辑弹窗el-upload做图片上传。这里最需要注意的是数据刷新时机新增或编辑成功后要重新调用列表接口否则新增的数据要手动刷新页面才出现。上传图片是另一个容易卡住的地方。注意el-upload的action属性直接写请求地址如果后端接口不需要鉴权你直接把action写成http://localhost:8080/api/upload即可。但如果加了JWT拦截器上传请求会因为没有携带Token而失败需要在组件里手动设置请求头el-upload :actionuploadUrl :headersuploadHeaders :on-successhandleUploadSuccess el-button typeprimary上传图片/el-button /el-uploadconst uploadHeaders { Authorization: Bearer ${localStorage.getItem(token)} }这里我在实际开发中翻过车忘了带uploadHeaders结果上传接口一直返回401而上传功能在黑盒测试时又不会报错只有点开浏览器Network才能看到拦截器拦了。前端把返回的图片URL回填到表单的imageUrl字段提交菜品数据时一并传过去后端只存URL字符串而不是文件二进制。5. 餐饮系统避坑指南从依赖冲突到跨域5个真实翻车现场5.1 SpringBoot版本与JDK版本不匹配现象下载的源码导入IDE后编译直接报一连串java: 程序包不存在或者method not found。原因SpringBoot 2.x基于JDK8SpringBoot 3.x强制JDK17。如果你本机装的是JDK17却导入了SpringBoot 2.3的旧项目依赖下载没问题但启动会报错反过来用JDK8跑SpringBoot 3.0根本编译不过。解决先看项目pom.xml里spring-boot-starter-parent的版本再切换JDK。我习惯用IDEA的Project Structure把Project SDK和Module SDK都全局改成一致版本。不要只改一个改完后清一下Maven缓存mvn clean再mvn compile验证。5.2 前端代理没配好接口报404或CORS现象前端页面能打开但所有请求都跨域或者请求地址变成http://localhost:5173/api/login然后404。原因Vite的proxy配置没生效或者后端接口路径里带了/api而代理也转了/api变成/api/api/login。解决先看前端请求的实际URLNetwork面板里那个Request URL是最直接的证据。如果已经变成/api/api/user/login那就是后端Controller里RequestMapping(/api)又加上方法里的路径而Vite代理保留了前缀。解决办法是后端Controller统一去掉/api前缀或者代理里用rewrite把路径重写一次。我个人习惯是后端类路径写/api/xxx前端代理不带重写这样最直观。5.3 MyBatis XML里写错字段启动不报错运行时才炸现象启动项目一切正常但一调用某个查询接口控制台就报BadSqlGrammarException或者返回结果全是null。原因MyBatis的Mapper XML是运行时加载的SQL里的列名如果拼写错误只有执行到那条SQL时才暴露。另一个典型原因是数据库字段是order_no但XML里写的是orderNoMySQL在Windows下可能不敏感在Linux下就报错。解决开发时打开SQL日志就是前面application.yml里配的stdout输出。每次报错先看日志里的SQL原文直接复制到Navicat执行很快就能定位。另外字段映射统一交给MyBatis-Plus/Mapper的驼峰开关手写XML时尽量用数据库真实字段名。5.4 时间格式化前后端对不上现象订单列表里时间显示成2025-06-01T10:30:00.00000:00跟数据库中时间差了8小时或者前端显示Invalid Date。原因后端返回的日期默认带时区前端直接用字符串渲染就会出问题或者后端没有配spring.jackson.time-zone导致时间按UTC序列化中国在东八区就差了8小时。解决在application.yml里配置spring.jackson.date-format和time-zone: Asia/Shanghai前端再用dayjs或element-plus的日期组件格式化。如果接口返回的是字符串类型就建议后端用LocalDateTime加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)不要用Date类型避免时区问题反复横跳。5.5 上传图片路径重启后失效现象菜品图片上传成功页面也能显示但重启后端服务后图片全部变成404。原因本地开发时图片保存到项目运行目录下的临时路径比如/temp/uploads重启后临时目录被清空路径也变了。如果打包成JAR运行你写在JAR内外的路径又不一样。解决开发阶段把上传路径配置成绝对路径比如D:/upload/然后通过一个映射把/images/**指向该目录。这里注意不能写在SpringBoot默认的classpath:/static/因为JAR包里的资源是只读的。我一贯做法是配置一个文件夹路径用WebMvcConfigurer加资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadPath); }这样重启后图片依然存在且部署到服务器时只需要改uploadPath一个地方。6. 从能跑到能答辩三个让分数明显提升的小技巧最后一个章节不贪多就讲三个我每次做课设都会用的手段。它们不需要你改架构但能让演示效果和答辩话术都上一个档次。第一给菜品列表加上关键词搜索和分页。很多课设源码的菜品接口直接selectList全量返回数据一多页面就卡。用MyBatis-Plus的Page对象配LambdaQueryWrapper三行代码就能搞定PageDish page new Page(current, size); LambdaQueryWrapperDish wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Dish::getName, keyword); wrapper.eq(Dish::getStatus, 1); dishService.page(page, wrapper);答辩时老师问“数据量大了怎么办”你直接指着这段代码说“我用了分页查询”比空谈性能优化有说服力得多。第二让订单状态在页面上可以流转。餐饮系统最忌讳做成静态表格。你可以用一个下拉框切换订单状态切换后调接口更新数据库。这个交互背后就是状态机的概念答辩时你主动说出“订单有四种状态只有非终态才能流转”老师基本就不会在这块继续追问。第三加一个简单的ECharts统计图。不用复杂就展示每天订单数量和营业额。后端写一个统计接口按日期分组查询前端用柱状图画出来。这个新功能装在“首页”或者“数据统计”菜单里成本大概半天但会让你的项目从“普通CRUD”上升到“有数据决策能力”的程度。我印象最深的一次翻车就是答辩现场U盘里源码版本和演示电脑不一致数据库连不上。从那以后我养成了两个习惯演示前把数据库导出一份SQL放到项目根目录启动说明里写清楚JDK和Node版本。这两件事看起来不起眼但能避免大多数突发状况。希望这篇笔记能帮你把这个SpringBootVue的餐饮管理系统真正跑起来、讲明白。如果你照着我说的顺序做先建表、再跑后端、最后配前端就算源码里有坑你也有排查的方向。本文还有配套的精品资源点击获取
返回列表