简介:在线点餐系统基于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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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: 0serverTimezone=Asia/Shanghai 这一项必须带上,否则 MySQL 8.x 默认的时区设置会导致数据库连接报错。map-underscore-to-camel-case 开启后,数据库里的create_time字段能自动映射到实体的createTime属性,省去大量手写映射。MyBatis-Plus 的逻辑删除配置加上之后,删除菜品就变成了更新操作,不会真的从表里删记录,这在答辩演示时是个值得讲的细节。
3.2 登录认证:拦截器方式的 Token 校验
在线点餐系统里,用户端和商家端需要权限区分。用户端要能自助注册、下单,商家端要看订单列表、更新订单状态。最简单可靠的方案是使用 JWT 生成 token,配合 SpringBoot 拦截器做校验。
JWT 相关依赖只需要一个:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</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. 计算订单总价 List<CartItem> 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-router@4 element-plus --save # 启动开发服务器 npm run serve创建项目时,Vue CLI 会问你要选哪些特性,建议勾选 Router 和 Vuex,CSS 预处理器选 Less 或不选。Babel 和 ESLint 保持默认。如果你是照着网上的教程走,注意教程里的 vue-router 是 3.x 还是 4.x,Vue 2 配 vue-router 3,Vue 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 写几个核心业务接口的测试用例,这在简历上是一个亮点。
我自己的习惯是:每个项目做完至少留一晚上专门处理"别人可能要问但还没做"的点,比如状态机校验、金额精度、事务回滚、静态资源映射。这些东西不一定全部实现,但每条都能说出自己的理解和方案。心里有底,答辩自然不慌。
希望这篇实战笔记能帮你把系统从能跑做到能讲、能答。照着上面的路径走一遍,踩过该踩的坑,这个项目就真正长在你身上了。
本文还有配套的精品资源,点击获取