Vue+SpringBoot在线点餐系统实战:从架构设计到部署避坑指南

发布时间:2026/10/28 7:17:32
Vue+SpringBoot在线点餐系统实战:从架构设计到部署避坑指南 简介在线点餐系统基于Vue与SpringBoot组合开发面向高校毕业设计、课程设计及前后端分离项目实战人群。项目涵盖菜品浏览、购物车、订单管理等核心业务模块分层结构清晰前端Vue组件与后端Java接口对应明确便于二次开发和功能延展。压缩包共269个文件包含39个Java源码、38个Vue组件、15个JS脚本及配套的jar依赖库、png/jpg界面截图与doc说明文档整体约78.89MB涵盖需求分析、设计文档、测试报告等完整资料。源码已经严格测试可放心参考包内附使用说明书与软件测试分析报告有助于读者快速搭建运行环境并理解项目实现流程。目前已有155人学习下载适合需要完整项目参考或快速开展课程设计的开发者获取。1. 认识基于Vue与SpringBoot的在线点餐系统它解决什么问题每年毕业设计季点餐系统都是Java方向出现频率最高的题目之一。原因很直接它体量适中、业务闭环完整从用户登录、菜品浏览、购物车到订单支付刚好覆盖一个真实商业系统的核心链路又不会复杂到一个人做不完。而Vue SpringBoot这套组合恰好是当前中小型 Web 项目最主流的搭档——SpringBoot负责后端接口和数据处理Vue负责页面交互和用户体验两者通过 RESTful API 通信职责清晰也符合企业里前后端分离的开发模式。如果你正在找毕业设计或课程设计题目或者想练习一个能写进简历的全栈项目这个方向值得投入。这篇文章就把从零搭建到部署跑通的路径、关键配置和踩过的坑一次讲清楚。2. 系统架构与数据模型先想清楚再动手写代码2.1 为什么选 Vue SpringBoot 而不是其他组合做点餐系统技术选型上的选择其实不少。有人用 Django Vue有人用 SpringBoot Thymeleaf 做服务端渲染也有人直接用若依这类脚手架改。但如果你面向的是毕业设计答辩或者想通过这个项目理解企业级开发的完整流程Vue SpringBoot 前后端分离方案是更稳的一条路。原因有三点。第一前后端分离意味着你可以把接口调试和页面开发并行推进不用互相等答辩时也能清晰展示 RESTful API 设计能力这比把页面和逻辑揉在一起更容易讲清楚。第二SpringBoot 的生态太成熟了MyBatis-Plus 操作数据库、Spring Security 或拦截器做权限、Redis 做缓存每一块都有大量文档和现成案例遇到问题基本都能搜到解决方案。第三Vue 的学习曲线比 React 平缓Element UI 组件库让后台管理页面和用户下单页面的开发效率明显提升一个初阶开发者花一周左右就能上手。2.2 数据表设计订单状态机是核心点餐系统的数据库设计我建议不要一上来就想着把所有表都建全。核心表只有 5 张用户表、菜品表、订单表、订单明细表、购物车表。菜品分类可以做成一个简单的字段也可以单独建一张分类表——如果菜品少于 50 个用字段就够了省去联表查询的复杂度。订单表的 status 字段是整个系统的灵魂建议用 int 类型存状态码后端用常量类或枚举统一管理。常见的状态流转是0 待付款 → 1 已付款/待接单 → 2 商家已接单/制作中 → 3 配送中/待取餐 → 4 已完成 → 5 已取消。注意 5 的状态要从 0 或 1 才能进入2 之后商家端就不允许取消了这个规则要在后端接口里校验不能只靠前端按钮控制。订单明细表必须冗余一份菜品快照包括菜品名称、价格、图片地址。为什么因为菜品表的价格以后可能会改但用户已经下的订单里应该保存下单那一刻的价格和描述。如果不做快照历史订单显示的菜品信息会被最新数据污染这在答辩演示时被老师问出来会很被动。2.3 前后端项目结构与接口约定后端建议按标准 SpringBoot 分层结构组织controller 接收请求、service 写业务逻辑、mapper 操作数据库、entity 放实体类、dto 放前端传参对象、vo 放返回给前端的视图对象。分清楚 dto 和 vo 是个加分项很多课程设计里这两个混在一起用答辩时能讲出区别会显得你确实理解分层意义。前端用 Vue CLI 或 Vite 创建项目后按 views、components、router、api、utils 五个目录组织。api 目录下每个模块一个 js 文件统一封装 axios 请求utils 目录放 request.js 封装拦截器统一处理 token 注入和 401 跳转。接口前缀建议统一用/api通过后端配置 context-path 或者前端 axios baseURL 来实现。这样将来如果要把接口独立部署到网关只需要改配置不用动代码。登录接口用/api/user/login菜品相关用/api/dish/list、/api/dish/detail/{id}订单相关用/api/order/create、/api/order/list这种 RESTful 风格命名在答辩时比随意命名更有说服力。提示前后端分离项目最忌接口路径前后端各写各的。开工第一天先把接口文档定下来哪怕用一张共享表格也够用。3. 后端 SpringBoot 实现从搭建到核心业务代码3.1 初始化项目与基础配置创建 SpringBoot 项目我建议直接去 Spring Initializr 生成基础骨架而不是手写 pom.xml。选 Java 8 或 11 都行不要选 Java 17除非你确定自己的 IDE 和依赖版本都兼容SpringBoot 版本选 2.7.x 系列这个版本对应的 MyBatis-Plus 和其余生态组件都验证过比较稳定3.x 版本虽然新但部分组件兼容性还需要排查。基础依赖只需要五个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、spring-boot-starter-validation。做毕设不需要引入 Redis用户量不大数据库直接扛得住多引入一个组件就多一个需要调试的点。数据库连接配置写在 application.yml 里核心参数如下server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ordering?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai 这一项必须带上否则 MySQL 8.x 默认的时区设置会导致数据库连接报错。map-underscore-to-camel-case 开启后数据库里的create_time字段能自动映射到实体的createTime属性省去大量手写映射。MyBatis-Plus 的逻辑删除配置加上之后删除菜品就变成了更新操作不会真的从表里删记录这在答辩演示时是个值得讲的细节。3.2 登录认证拦截器方式的 Token 校验在线点餐系统里用户端和商家端需要权限区分。用户端要能自助注册、下单商家端要看订单列表、更新订单状态。最简单可靠的方案是使用 JWT 生成 token配合 SpringBoot 拦截器做校验。JWT 相关依赖只需要一个dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency生成 token 的工具类核心代码public class JwtUtil { private static final String SECRET your-256-bit-secret-key-change-in-production; private static final long EXPIRE 1000 * 60 * 60 * 24; // 24小时 public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }这段代码里claim(role, role) 把用户角色塞进 token拦截器里取出 role 判断是否能访问商家端接口。注意 SECRET 至少需要 32 个字节否则 HS256 算法会报错这是 jjwt 0.11.x 版本的一个硬性要求我第一次用的时候在这里卡了半小时。拦截器需要注册到 WebMvcConfigurer 里并配置放行路径。前端页面相关的静态资源和登录、注册、菜品列表这些基本接口不需要拦截订单相关接口要拦截商家端接口要拦截且校验角色。3.3 下单业务事务与库存扣减下单接口是业务核心隐藏了最多的坑。一个完整的下单流程包含创建订单主记录、插入订单明细、清空购物车三个步骤这三步必须在一个事务里完成任何一步失败都要回滚。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto, Integer userId) { // 1. 计算订单总价 ListCartItem cartItems cartMapper.selectByUserId(userId); BigDecimal totalAmount cartItems.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 2. 创建订单主记录 Order order new Order(); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(0); // 待付款 order.setAddress(dto.getAddress()); order.setPhone(dto.getPhone()); orderMapper.insert(order); // 3. 插入订单明细 for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(item.getDishId()); orderItem.setDishName(item.getDishName()); orderItem.setDishImg(item.getDishImg()); orderItem.setPrice(item.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 4. 清空购物车 cartMapper.deleteByUserId(userId); return buildOrderVO(order); }Transactional(rollbackFor Exception.class) 里这个 rollbackFor 参数容易被忽略。默认情况下 Spring 只对 RuntimeException 回滚如果抛的是检查异常事务不会回滚数据会处于半写状态。加上 rollbackFor Exception.class 是最保险的写法无论什么异常都回滚。金额计算必须用 BigDecimal不能用 double 或 float。这个坑在订单总价和支付回调里尤其明显二进制浮点数计算金额会产生精度问题比如 0.1 0.2 不等于 0.3。答辩时如果老师追问金额精度问题你能说出 BigDecimal 这个点印象分会提高不少。3.4 商家端订单管理状态流转的业务规则商家端给订单更新状态的接口需要严格校验当前状态是否允许流转到目标状态。最简单的实现方式是定义一个状态流转映射表用 Map 或者 switch 判断。public boolean canTransition(Integer currentStatus, Integer targetStatus) { switch (currentStatus) { case 0: // 待付款 return targetStatus 5; // 只能取消 case 1: // 已付款待接单 return targetStatus 2 || targetStatus 5; case 2: // 制作中 return targetStatus 3; case 3: // 配送中 return targetStatus 4; default: return false; } }这个逻辑放在 service 层而不是 controller 层。controller 只负责接收参数和返回响应业务规则必须下沉到 service这样接口看起来干净也方便写单元测试。我在实际项目里见过很多直接把规则写在 controller 里的情况当时跑通没问题后期加一个状态机就处处受制。4. 前端 Vue 实现从环境搭建到下单全流程4.1 Vue 环境配置与项目创建Vue 环境这块是很多新手第一个翻车点。npm 安装依赖慢、node-sass 编译失败、vue-cli 版本不一致这些问题几乎每个人都会遇到。说两个比较稳的做法一是用淘宝镜像源替代默认源安装速度快一个量级二是如果只需要组件库和路由不要引入 sass 相关的依赖用原生 CSS 足够了省去 node-sass 这个著名的玄学编译问题。# 安装 Vue CLI如果还没装 npm install -g vue/cli # 创建项目选择 Vue 3 或 Vue 2 均可 vue create ordering-web # 进入项目目录安装核心依赖 cd ordering-web npm install axios vue-router4 element-plus --save # 启动开发服务器 npm run serve创建项目时Vue CLI 会问你要选哪些特性建议勾选 Router 和 VuexCSS 预处理器选 Less 或不选。Babel 和 ESLint 保持默认。如果你是照着网上的教程走注意教程里的 vue-router 是 3.x 还是 4.xVue 2 配 vue-router 3Vue 3 配 vue-router 4这两代的路由 API 有一些区别照着混用最容易出现页面空白的问题。4.2 axios 封装与登录态管理前端所有请求都走统一封装的 axios 实例好处是拦截器只写一遍token 注入、错误提示、401 跳转都集中处理。我的做法是import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器注入 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } ) export default requestbaseURL 写/api是利用了 Vue CLI 的 devServer 代理能力。在vue.config.js里配置代理把/api开头的请求转发到后端 8080 端口这样开发时前端跑在 8081请求后端接口不会遇到跨域问题。生产部署时再把/api交给 Nginx 转发这算是最灵活的路径设计。// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }4.3 菜品页与购物车的状态管理点餐系统的前端页面一般有三块用户登录/注册页、菜品列表页、下单与订单页。菜品列表页用 Element UI 的 el-card 组件展示菜品卡片点击加入购物车按钮后将菜品信息和数量写入 Vuex 或 localStorage。这里有一个取舍如果购物车数据存在 Vuex刷新页面后购物车会清空存在 localStorage 则可以持久化。我建议用 Vuex localStorage 同步的写法Vuex 管页面交互状态localStorage 做持久化兜底刷新后重新拉取。// store/modules/cart.js import request from /utils/request const state { items: JSON.parse(localStorage.getItem(cart) || []) } const mutations { ADD_ITEM(state, dish) { const existing state.items.find(item item.dishId dish.id) if (existing) { existing.quantity } else { state.items.push({ dishId: dish.id, dishName: dish.name, price: dish.price, quantity: 1 }) } localStorage.setItem(cart, JSON.stringify(state.items)) }, REMOVE_ITEM(state, index) { state.items.splice(index, 1) localStorage.setItem(cart, JSON.stringify(state.items)) }, CLEAR_CART(state) { state.items [] localStorage.removeItem(cart) } }4.4 路由守卫与页面跳转路由设计的核心是权限控制。前端路由表分成三部分公开路由登录页、注册页、菜品列表、用户路由购物车、订单列表、个人中心、商家路由订单管理、菜品管理。用 vue-router 的全局前置守卫判断访问权限router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login || to.path /register) { next() return } if (!token) { next(/login) return } if (to.meta.requiresRole to.meta.requiresRole ! role) { next(/) return } next() })to.meta.requiresRole 是在路由配置里声明的商家端页面的路由加上meta: { requiresRole: merchant }普通用户访问商家页面就会被拦截跳回首页。这个守卫是前端层面的提示真正的安全校验还是靠后端拦截器前端路由守卫解决的是用户体验问题后端接口校验才解决安全问题。这个思路答辩时能讲出来也是加分项。5. 前后端联调与部署避坑清单5.1 把前端打包放进 SpringBoot静态资源路径踩坑开发完成后要把前端部署到后端或者打包成单个可运行 jar最省事的做法是把前端构建产物放进 SpringBoot 的src/main/resources/static目录。但这个过程有两个高频坑。第一个坑是前端路由模式。Vue Router 默认使用 history 模式URL 路径是http://localhost:8080/order/list刷新页面时 SpringBoot 会去查找这个路径对应的 controller结果返回 404。解决方法是后端加一个路径转发兜底把所有非/api开头的请求转发到 index.html由前端路由接管。Controller public class ViewController { RequestMapping(value {/, /index, /**}) public String index() { return forward:/index.html; } }但这个方法有个限制有 static 下的真实文件时要注意优先级/**兜底不能把静态资源请求也转发走了。SpringBoot 对静态资源的处理优先级高于这个 forward所以实际使用时一般只在无法访问页面时才加这个兜底。第二个坑是打包后资源路径。Vue 默认的 publicPath 是/在开发环境没问题但如果后端接口带了 context-path或者前端文件不是部署在根路径资源就会 404。解决方法是修改vue.config.js里的publicPath: ./让资源路径变成相对路径这样部署到任何子路径下都正常。5.2 常见问题排查现象、原因与解决问题一前端请求接口报 404后端控制台没有日志现象前端页面能打开但所有请求都返回 404后端没有任何请求日志。原因浏览器里请求的 URL 没有正确代理转发。可能是 vue.config.js 的 proxy 配置没生效或者后端 context-path 与前端 baseURL 不对齐。解决先在后端任意一个接口写一条输出日志确认后端能收到请求然后在浏览器开发者工具里看请求路径确认是/api/**前缀最后检查 devServer.proxy 配置是否写在 module.exports 而不是 exports 里。问题二跨域请求报 CORS 错误现象本地启动前端 8081、后端 8080请求被浏览器拦截控制台报 CORS policy 错误。原因最常见的不是没加跨域配置而是 Nginx 或代理层已经做了转发后端又配置了跨域导致响应头重复或者前端请求带了自定义 Header后端 OPTIONS 预检请求没有放行。解决开发环境优先用 devServer 代理的方式让浏览器始终访问同源地址从根上规避跨域。生产环境让 Nginx 统一配置add_header Access-Control-Allow-Origin即可后端不要重复配置。问题三刷新页面白屏现象部署后从首页点进去正常但手动刷新/order/list页面就白屏。原因history 模式的 Vue Router 没有后端兜底静态资源服务器不知道把这个路径映射到 index.html。解决改回 hash 模式是新手最快的解法把createWebHistory()改成createWebHashHistory()URL 会变成/#/order/list刷新不会 404。如果想要美观的 history 模式需要后端把非 API 请求全部转发到 index.html还要注意避免与静态资源路径冲突。问题四上传头像或菜品图片后无法显示现象图片上传成功接口返回路径也正常但img标签加载图片返回 404。原因上传的文件保存在本地磁盘开发环境和生产环境的磁盘路径不一致或者 SpringBoot 没有把上传目录映射成静态资源路径。解决定义统一的文件存储路径并在 SpringBoot 配置静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/upload/); } }注意 Windows 和 Linux 路径写法不同Linux 下是file:/usr/local/upload/。生产部署时建议用绝对路径不要用相对路径否则工作目录变化会导致图片全丢。5.3 接口联调的效率工具前后端分离联调阶段不要靠 Postman 手动测完每个接口再让前端去试。两个工具能显著提升效率一是 Swagger/OpenAPI 自动生成接口文档后端启动后访问/swagger-ui.html就能看到所有接口前端照着文档调二是直接在浏览器里用 axios 调试前端写好请求函数后立刻调用配合 Vue DevTools 看响应数据。如果后端接口返回的数据结构和前端预期不一致最快速的对齐方式是让后端在 JSON 返回对象上统一包装一层{ code: 200, message: success, data: {...} }前端在响应拦截器里统一处理。这样后端改了字段名前端能一眼看到是哪里出错不用每个页面逐个排查。6. 毕业设计答辩与后续进阶把项目从能用变成禁得起问6.1 答辩前要准备的三块内容答辩时老师最常问的三类问题是项目技术选型理由、某个模块的实现细节、遇到了什么难点怎么解决的。这三块最好提前准备成口述稿不要临时组织语言。技术选型理由核心一句话SpringBoot 简化了 Spring 配置内置 Tomcat 让部署变得简单Vue 的组件化开发让前端代码可维护性更好Element UI 提供了现成的组件库。再补充一句前后端分离是当前企业开发的主流模式做这个项目能完整掌握从接口设计到联调部署的流程。模块实现细节重点准备两个订单状态是怎么流转的、token 认证是怎么做的。订单状态流转把那张状态表讲清楚token 认证把拦截器和 JWT 工具类的配合讲清楚。遇到的难点不要泛泛说遇到了很多问题挑一两个具体的技术点讲。比如跨域问题的排查思路、前端打包后刷新 404 的解决方案、金额计算的精度问题。每讲一个难点就说清楚现象、原因、解决过程和最终效果比说十个浅尝辄止的问题更有说服力。6.2 部署验证从本地 jar 包到可以被访问部署这一步毕业设计阶段不需要上云服务器但至少要在本地完成 jar 包构建和运行验证。命令如下# 后端构建 mvn clean package -DskipTests # 运行 java -jar target/ordering-0.0.1-SNAPSHOT.jar # 前端构建如果选择与后端合并部署 npm run build # 构建后把 dist 目录内容复制到 src/main/resources/static/ cp -r dist/* ../backend/src/main/resources/static/验证步骤分三层第一层看 SpringBoot 启动日志确认端口监听正常第二层用浏览器访问接口文档Swagger确认后端逻辑没问题第三层完整跑通一遍下单流程注册用户 → 浏览菜品 → 加入购物车 → 提交订单 → 在订单列表看到订单 → 切换商家账号接单 → 用户确认完成。这条链路全部走通说明项目基本可用。6.3 后续可以扩展的方向如果时间充裕或者你想把这个项目做得更深入一些有几个低成本的扩展点。接入支付功能对接支付宝沙箱环境费用只有商户号的注册门槛但能让项目完整度上一个台阶引入 Redis 缓存菜品分类和首页热点数据并解释缓存穿透、缓存雪崩的基本应对思路给后端补充单元测试用 JUnit 写几个核心业务接口的测试用例这在简历上是一个亮点。我自己的习惯是每个项目做完至少留一晚上专门处理别人可能要问但还没做的点比如状态机校验、金额精度、事务回滚、静态资源映射。这些东西不一定全部实现但每条都能说出自己的理解和方案。心里有底答辩自然不慌。希望这篇实战笔记能帮你把系统从能跑做到能讲、能答。照着上面的路径走一遍踩过该踩的坑这个项目就真正长在你身上了。本文还有配套的精品资源点击获取