☰
Springboot+Vue在线点餐系统:从环境搭建到答辩升级的完整拆解
2026/10/7 16:38:16 网站建设 项目流程

简介:这套基于Spring Boot和Vue的在线点餐系统源码包,定位为计算机专业毕业设计级项目,适合用于毕设答辩、期末课程设计或就业项目练习。系统按前后端分离思路组织业务模块,覆盖管理员与用户信息管理、商品类型维护、订单处理、广告位与链接管理等环节,从内容预览可见后端Controller和Service类划分清晰。压缩包共472个文件,总大小约19.67MB,核心包括65个Java源文件、97个XML配置/映射文件、68个编译后的class文件,以及54个HTML页面、38个JS脚本、30个CSS样式和数据库SQL文件,可直接部署运行,也方便对照代码学习接口设计和业务逻辑。源码经过作者严格调试,评审分达95分以上,附带的SQL脚本有助于快速初始化环境;目前已有185人学习使用,对希望获取可运行完整项目作为参考的中高级学习者具有较好的借鉴价值。

1. 为什么偏偏是 Springboot+Vue 的在线点餐系统

如果你正在为毕业设计发愁,大概率已经在各种源码站里翻过一轮了。搜出来的结果要么是前后端不分离的 JSP 老项目,要么是只给前端不给后端接口的半吊子。真正能让你交上去、并且能在答辩现场讲清楚前后端怎么协作的,反而是这套基于 Springboot+Vue 的在线点餐系统源码加数据库的组合。它值钱的地方不在于菜品的 CRUD 写得有多花哨,而在于它覆盖了一个完整业务系统最常见的几条链路:用户从微信端或网页端点餐、购物车结算、订单流转、商家后台管理菜品和订单。这些模块刚好对应毕设评分表里「需求分析、数据库设计、系统实现、项目部署」每一个得分点。

这套资源适合两类人:一类是时间紧,希望直接拿一套能跑通的项目改成自己的课设或毕设;另一类是确实想搞懂 Vue 前端怎么调用 Springboot 接口、MySQL 里订单表该怎么设计。接下来我按自己拆项目的习惯,从技术选型、本地运行、核心代码、踩坑记录一直讲到答辩前怎么给项目做升级,全程给你能直接抄走的步骤和参数。

2. 先看懂这套系统的技术栈:前后端分离的选型逻辑

2.1 为什么毕业设计选 Springboot 而不是 SSM

在正式碰代码之前,得先搞清楚这套系统为什么用 Springboot + Vue,而不是教科书里常讲的 SSM(Spring + SpringMVC + MyBatis)组合。Springboot 本质上是对 SSM 的封装,把 Spring 和 SpringMVC 的繁琐 XML 配置全部改成了自动装配,你在 application.yml 里写几行配置就能启动一个 Web 服务。对于毕设场景,这意味着你少写大量 spring-mvc.xml、mybatis-config.xml 这类配置文件,把时间省给业务代码。

Vue 这边用的是当前主流的前后端分离方案。后端只提供 RESTful API,前端用 axios 发请求。这样的好处是,你答辩时可以直接说「系统采用前后端分离架构,前端使用 Vue.js 生态,后端使用 Springboot 提供 API 服务,两者通过 JSON 数据交互」,这一句话就比 SSM + JSP 的组合听起来更符合当前企业的开发模式。如果你时间充裕,Springboot + Vue 的组合还能顺带写进简历,而 JSP 项目基本是减分项。

2.2 项目目录结构与数据库表设计的门道

下载解压后,你会看到一个典型的前后端分离目录。后端是 Maven 工程,前端是 Vue CLI 工程。我建议你第一步不是急着启动,而是花十分钟把目录结构过一遍。

后端主目录下,常见的包结构是 controller、service、mapper、entity、config。前端结构里,src 下按 views、components、router、api 分层。注意 api 目录,里面通常按业务模块拆了文件,比如 order.js、dish.js、user.js,每个文件里封装了 axios 请求函数,对应后端 controller 接口的 URL。

数据库脚本是这套资源里另一个关键部分。一般是一个 .sql 文件,里面包含建库建表语句和初始数据。我见过的在线点餐系统,核心表至少是这几张:用户表、菜品表、菜品分类表、购物车表、订单表、订单明细表。其中订单明细表的出现很关键,它记录了订单里每个菜品的快照信息,比如菜品名称、单价、数量。

CREATE TABLE `order_detail` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` varchar(32) DEFAULT NULL COMMENT '订单编号', `dish_id` int(11) DEFAULT NULL COMMENT '菜品id', `dish_name` varchar(50) DEFAULT NULL COMMENT '菜品名称', `dish_price` decimal(10,2) DEFAULT NULL COMMENT '单价', `dish_num` int(11) DEFAULT NULL COMMENT '数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

这段建表 SQL 是点餐系统里一条非常重要的设计决策。订单明细表不直接关联菜品表的外键,而是把菜品名称和价格冗余存储了一份。原因很简单:商家改了菜品价格或删除了菜品,历史订单已经生成,不能再跟着变。这叫「订单快照」,属于正经电商系统都在用的做法。你如果答辩时能主动讲清楚这张表为什么这样设计,老师会认为你懂业务而不仅仅是会写代码。

2.3 数据流梳理:从菜品展示到订单落库

整套系统的数据流动方向是:前端页面加载时向后端请求菜品列表,后端通过 MyBatis 查询 MySQL 返回 JSON 数组,前端用 v-for 渲染成菜单;用户点菜后把菜品加入购物车,购物车数据可以存在前端 localStorage 也可以提交给后端;确认下单时,前端把购物车商品组装成订单数据 POST 给后端,后端同时向订单表和订单明细表插入记录,并清空购物车。

这里你要重点关注的是「下单接口的事务」处理。因为一次下单要写两张表,如果订单主表写入成功、明细表写入失败,会出现数据不一致。正常的事务写法是在 service 层方法上加上@Transactional注解,让两张表的操作处于同一事务中。后面我会在第 4 章展示具体代码。

3. 把项目本地跑起来:环境配置与两步启动

3.1 环境清单与版本匹配

在动手之前先确认环境。做毕设的机器一般不会太差,这套系统对环境要求也不高。后端需要 JDK 1.8 或以上,Maven 3.6 及以上版本;前端需要 Node.js 10.x 以上——装太新的 Node 版本有概率报 OpenSSL 错误,后面避坑章节我会单独说。数据库方面是 MySQL 5.7 或 8.0,Navicat 或 DataGrip 任选一个用于导入脚本。

有一个点我在多个版本里碰到过:前后端分离项目的端口冲突。后端默认跑在 8080,前端 Vue 开发服务器跑在 8080 或 8081。如果同时启动,必须保证前端配置的代理端口和后端端口对得上。常见的做法是在前端根目录的 vue.config.js 里配置 devServer 代理,把所有 /api 开头的请求转发到后端地址。

3.2 数据库导入的三种方式

数据库脚本导入是最容易翻车的一步,但原理很简单。先创建一个数据库,名称建议用项目里的命名,比如foodie或order_system。然后选择该数据库,通过 Navicat 的「运行 SQL 文件」功能导入下载目录里的 .sql 文件。如果没有图形工具,命令行也一样。

mysql -u root -p -e "create database foodie default character set utf8mb4;" mysql -u root -p foodie < db_foodie.sql

这里推荐使用utf8mb4,而不是老项目里常见的utf8,因为 utf8 在 MySQL 里并不是真正的全量 UTF-8,emoji 字符存不进去。点餐系统如果用户备注里带了表情,用 utf8 字符集会直接报错。导入完成后,检查一下各表的数据量,菜品表有初始数据、管理员账号已存在,才算导入成功。

3.3 后端启动流程与参数检查

然后启动后端。用 IDEA 打开后端目录,等待 Maven 下载依赖完成后,先打开 application.yml 文件核对数据库连接配置。你需要重点看这几个参数:url里的数据库名、username、password。大多数源码包给的是 root 和 123456,如果你本机密码不是这个,改掉再启动。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/foodie?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

需要注意的是serverTimezone=Asia/Shanghai这段参数。MySQL 8.0 的驱动对时区比较敏感,如果这里不配置,启动时会报The server time zone value异常。另外useSSL=false是关闭安全连接,因为本地开发环境用不着证书,不关可能会报 SSL 连接警告。保存配置后直接运行启动类里的 main 方法,看到 Spring Boot 启动成功的日志,就算后端起来了。

3.4 前端启动与代理转发配置

前端工程打开后,第一件事是在终端里执行npm install安装依赖。这个过程时长取决于网络状况,快则两分钟,慢则十分钟。如果你在安装过程中看到大量红色报错,别慌,先看是不是网络问题,用国内镜像源可以解决大部分烦恼。接下来是启动前端开发服务器,执行npm run serve,默认端口一般是 8081。

// vue.config.js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这段代理配置意味着,你在前端代码里请求/api/dish/list,开发服务器会自动帮你转发到http://localhost:8080/api/dish/list,从而避免了跨域问题。这一步是整个前后端联调的关键。如果你不加代理,前端页面启动后请求会直接 404 或者报跨域错误。浏览器地址栏输入http://localhost:8081后,如果有默认路由跳转到登录页,说明前后端已经打通,系统跑起来了。

4. 核心代码解读:从登录鉴权到下单事务

4.1 前端路由与后端接口的分工

项目跑起来之后,先从代码层面理解这个系统是怎么组织的。前端router目录里定义了所有页面路径,比如/login、/home、/cart、/order、/admin,后端controller则暴露了对应的业务接口。登录页面的逻辑是提交用户名密码到/api/user/login,后端校验通过后返回一个 token 字符串,前端把它存入 localStorage。

这里有一个容易在答辩时被追问的点:为什么菜品的增删改查接口路径基本都是/api/dish/**,而要加上/api前缀?其实就是为了配合前端代理的转发规则。所有带/api的请求统一走后端 8080,不带的则走前端静态资源,两者不冲突。

4.2 登录校验的会话方案:Token 还是 Session

在线点餐系统比 SSM 时代的项目先进的一点,是登录态一般用的是 Token 方案而不是 Session。Session 依赖服务器内存和 Cookie,Token 则是无状态的:用户登录成功后,后端生成一个加密字符串返回给前端,前端每次请求在请求头里带上Authorization: token 值,后端通过拦截器校验合法性。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } // 这里对 token 做解析校验,失败则抛出业务异常 return true; } }

这段拦截器代码在系统里通常注册在 WebMvcConfigurer 里,并且排除登录接口和菜品列表这类公开接口。用 Token 的好处是,你将来想把系统改成小程序版本或者 App 版本,后端完全不用改登录逻辑,前端从 Cookie 换成 Header 存储而已。答辩时说出这一层,属于明显的加分项。

4.3 下单接口的事务控制与库存判断

下面看核心的下单接口。下单时用户提交购物车数据,后端先计算总价,生成订单号,再循环向订单明细表插入记录。临界问题在哪里?如果用户下单后菜品价格在缓存里被管理员修改了,导致明细表的单价和前端展示的不一致。比较严谨的做法是:后端直接读取当前数据库里的菜品价格,而不是信任前端传过来的价格参数。也就是说,前端只传菜品 id 和数量,单价由后端查询计算。

@Transactional(rollbackFor = Exception.class) public Order createOrder(Integer userId, List<CartItem> items) { String orderNo = "OD" + System.currentTimeMillis(); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); BigDecimal totalPrice = BigDecimal.ZERO; List<OrderDetail> details = new ArrayList<>(); for (CartItem item : items) { Dish dish = dishMapper.selectById(item.getDishId()); OrderDetail detail = new OrderDetail(); detail.setOrderNo(orderNo); detail.setDishName(dish.getName()); detail.setDishPrice(dish.getPrice()); detail.setDishNum(item.getNum()); details.add(detail); totalPrice = totalPrice.add(dish.getPrice().multiply(new BigDecimal(item.getNum()))); } order.setTotalPrice(totalPrice); orderMapper.insert(order); for (OrderDetail detail : details) { orderDetailMapper.insert(detail); } return order; }

这段逻辑里有两个值得注意的参数点。第一是@Transactional(rollbackFor = Exception.class),它保证订单主表和明细表的写入在同一个事务中,任何一条插入失败都会回滚全部操作,避免产生只有订单没有明细的脏数据。第二是 BigDecimal 而不是 double 来计算金额,浮点数在金额计算中会有精度丢失的问题,0.1 加 0.2 都不等于 0.3,而 BigDecimal 能精确到分。这两个细节都写进答辩说辞里,专业度直接上一个台阶。

4.4 管理员模块的权限控制

系统的管理员模块一般负责菜品管理和订单管理。菜品管理包括上架、下架、修改价格和库存;订单管理则是查看每笔订单的状态,可以手动标记为已完成或已取消。前端通过路由守卫判断当前用户的角色,后端接口配合拦截器校验请求来源。如果前端只有普通用户权限想访问管理员页面,路由守卫会直接把他重定向到登录页。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path.startsWith('/admin') && !token) { next('/login') } else { next() } })

这段前端路由守卫的写法是毕设项目里很常用的。它把页面访问控制放在了前端,而后端拦截器兜底校验接口权限。层层设防,逻辑就完整了。如果项目里没有这段代码,你自己补上也不难,十分钟的事。

5. 常见问题与避坑记录:这套系统的边界和坑

5.1 端口占用导致后端启动失败

现象:Springboot 启动时报Web server failed to start. Port 8080 was already in use。

原因:电脑上其他程序已经占用了 8080 端口,最常见的是之前残留的 Java 进程或者本地已经跑了一个 Tomcat 服务。

解决:如果是自己机器的残留进程,可以用netstat -ano | findstr 8080命令查到占用端口的进程 PID,然后到任务管理器里结束对应进程。更省事的办法是直接改后端端口,比如改成 8082,然后同步修改前端代理配置里的 target 地址——但所有以绝对路径写死端口的地方都要跟着改,包括后端配置和前端代理,这就是我推荐的「统一走代理、少写死绝对地址」的原因。

5.2 Node.js 版本过高导致前端安装依赖失败

现象:执行npm install时报错,错误信息里出现ERR_OSSL_EVP_UNSUPPORTED或者digital envelope routines::unsupported。

原因:Node.js 17 以上的 OpenSSL 版本对 md5 摘要算法的默认策略改成了拒绝,而一些旧版本的 Webpack 和 Vue CLI 项目还在用旧算法,两者不兼容。

解决:有两个方案。方案一是降级 Node.js 到 16.x LTS 版本,这是最稳妥的,因为 Vue CLI 项目本来就是基于 Node 10 到 16 这个区间开发的。方案二是不降级,在启动命令上加上NODE_OPTIONS=--openssl-legacy-provider让它使用旧版 OpenSSL 策略。我自己的习惯是直接用 nvm 做 Node 版本管理,切换到 16 之后基本没再为这个坑烦过。

5.3 前后端联调跨域报错

现象:前端页面能打开,但请求接口时报Access to XMLHttpRequest at 'http://localhost:8080/...' from origin 'http://localhost:8081' has been blocked by CORS policy。

原因:前端运行在 8081,后端在 8080,两个端口不同就构成了跨域。如果前端没有配代理,或者没有直接访问后端地址,就会触发浏览器的同源策略拦截。

解决:确认 vue.config.js 里的代理配置是否正确,确保请求路径以/api开头。如果还是不行,可以临时把代理改成target: 'http://127.0.0.1:8080',注意 localhost 和 127.0.0.1 在某些系统上会被当成不同的源。另一种兜底方案是在后端加一个全局 CORS 过滤器,允许所有来源访问。注意,毕设答辩环境如果网络受限,代理会比 CORS 方式更稳,因为代理对浏览器来说根本没发生跨域。

5.4 MyBatis 查询结果字段对应不上

现象:菜品列表接口返回的 JSON 里,某些字段是 null,比如dish_name有值但返回的dishName是 null。

原因:数据库字段是下划线风格dish_name,而 Java 实体类属性是驼峰风格dishName,MyBatis 默认不会自动做映射。

解决:在 application.yml 的 MyBatis 配置里加一段内容,开启驼峰映射。

mybatis: configuration: map-underscore-to-camel-case: true

这条配置加了之后,MyBatis 会自动把查询结果的dish_name列映射到实体类的dishName属性。如果项目里用的是 MyBatis-Plus,通常默认已经开好了,不需要单独配置。这个坑很隐蔽,因为启动不报错,只有打开页面看数据时才暴露。

5.5 前端打包后刷新页面 404

现象:执行npm run build打包,把 dist 目录扔到 Tomcat 或 Nginx 里,页面能打开但刷新子路由页面时 404。

原因:Vue Router 默认使用 history 模式,路由路径是真实的浏览器地址,服务端没有配置对应的回退规则,直接请求/home时服务端找不到这个路径。

解决:如果项目部署在 Nginx,加一条 try_files 配置让所有未知路径都回退到 index.html;如果部署在 Springboot 的静态资源目录,就得让后端 controller 对非接口路径做转发。更省心的做法是改用 hash 模式——把 Vue Router 的 mode 改成 hash,URL 变成/#/home,刷新就不会再 404。毕设答辩演示时用 hash 模式安全得多,意外最少。

6. 答辩前必做的两件事:把代码升级成增量创意

6.1 给系统补充一个「订单超时自动取消」的状态机

基础版点餐系统一般只有未支付、已支付、已完成三个状态,如果你想让系统看起来比同组同学复杂一点,可以在订单表加一个status字段的流转逻辑。常见方案是在下单时把状态设为「待支付」,通过一个定时任务,每五分钟扫描一次超过三十分钟未支付的订单,把状态改成「已取消」。

实现思路是在后端加一个 Spring 的@Scheduled定时任务。在启动类上加上@EnableScheduling注解后,定时方法就会按固定间隔扫描数据库。

@Scheduled(cron = "0 */5 * * * *") public void autoCancelOrder() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(30); List<Order> expiredOrders = orderMapper.selectTimeoutOrders(deadline); for (Order order : expiredOrders) { order.setStatus(3); // 3 代表已取消 orderMapper.updateById(order); } }

这个功能在答辩现场演示效果很好:你可以在下单后把数据库里的下单时间手工改早 31 分钟,等下一次定时扫描跑完,页面上的订单状态就自动变成已取消。这比单纯讲 CRUD 有说服力得多,而且代码量不大,20 行以内搞定。

6.2 提前预置演示账号与演示数据

答辩翻车最多的时候是临时起意演示,结果发现管理员账号密码忘了、菜品分类是空的、没有历史订单可以点开看详情。我的习惯是答辩前固定一套演示数据并写成一份data_prepare.sql脚本。

准备至少一个普通用户账号和一个管理员账号,用户名密码用简单的demo/123456;菜品分类要有热菜、凉菜、主食、饮料四类,每类至少 5 个菜品;订单区提前造三笔不同状态的订单——已完成、待支付、已取消,方便讲订单状态流转的时候直接点开看。这些数据用一个独立脚本存好,答辩当天如果数据库被人动过,重新执行一遍脚本就能恢复现场。

INSERT INTO `user` (`username`, `password`, `role`) VALUES ('demo', 'e10adc3949ba59abbe56e057f20f883e', '1'); INSERT INTO `user` (`username`, `password`, `role`) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '2');

这段 SQL 里的密码是123456的 MD5 值,具体生成方式要看项目里用的是不是 MD5 加密。你拿到源码后第一件事就是去 UserServiceImpl 里看密码加密方式,然后把演示数据脚本改成和你系统一致的形式。从那以后我每次拿到一套新源码,都会强制走一遍「先改数据库密码配置、再测登录、再测下单、最后导演示数据」的流程,确认最关键的四步都没问题才敢说准备完毕。希望这份拆解能帮你少走一些弯路,把这个系统真正变成能从容讲清楚的作品。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询