
简介这是一份基于Spring Boot与Vue.js的网上点餐系统毕业设计项目主要面向计算机相关专业毕业生、正在准备课程设计或期末作业的学生也适合想快速掌握前后端分离项目开发的初学者。系统围绕用户点餐、菜品浏览、购物车与订单管理等核心场景展开前端使用Vue.js实现交互界面后端基于Spring Boot提供接口服务并包含完整的数据库脚本可支撑从环境搭建到功能演示的整个流程。资料包大小约66.37MB包含项目源码、数据库文件、答辩PPT、毕业论文、使用文档及演示视频其中演示视频能直观展示系统操作论文与PPT可作为撰写文档与答辩汇报的参考。该资源已在Windows10/11测试环境中严格调试确保解压后按部署教程即可运行属于答辩评审97分的高分项目。目前已有206人学习下载无论是用于毕业设计二次开发还是作为期末项目提交都具有较高的参考价值。1. 点餐系统拆解Spring Boot Vue 各管哪一段这套网上点餐系统源码拆开看其实是一个很标准的“前端展示 后端交易”项目。Spring Boot 负责菜品管理、下单、支付状态变更和订单查询Vue 负责把菜牌渲染出来、维护购物车、提交订单并回显状态。数据库用 MySQL整个链路从页面点击到最后落库中间隔着一条 REST 接口。对做数据库课程设计或者毕业设计的人来说它最大的价值不是功能多而是能完整看到“用户加购物车、提交订单、商家改状态”这条业务线是怎么被切开、又怎么拼回去的。对已经工作几年的开发这项目反而适合用来检查自己是否漏掉了事务、状态机、跨域代理这些基本功。下面按从下到上的顺序拆。2. 数据库设计菜品、订单与状态流转的后端实现2.1 四张业务表从 ER 关系到表结构点餐系统的核心不是“菜”而是“订单”。用户会点多道菜订单与菜品是多对多关系所以必须拆出一张订单明细表。常见的表结构如下表名作用关键字段user用户/会员id, username, password, phone, addressdish菜品id, name, price, image, category, statusorders订单主表id, user_id, order_no, total_amount, status, create_timeorder_detail订单明细id, order_id, dish_id, dish_name, price, quantity为什么订单明细里要冗余dish_name和price而不是下单时只存dish_id因为菜品价格会变。商家今天把宫保鸡丁从 28 改成 32历史订单里的金额不应该跟着变。明细表里保存下单那一刻的名称和价格快照后厨出菜、财务对账才说得清楚。另一个原因是统计“哪些菜卖得多”直接对order_detail按dish_id分组求和就行性能也比关联菜品表再聚合好。订单号建议用order_no字符串不要直接把自增 id 暴露给用户。我一般生成ORD yyyyMMddHHmmss 4位随机数比如ORD202506011030123456。这样餐厅服务员对单号时不会因为“id 是 1024 还是 1025”扯皮也是防爬的一种小手段。2.2 订单状态机用整型字段避免文案漂移订单状态是这套系统里最容易出 bug 的地方。很多毕设源码用String字段直接存“未支付”“已完成”前端页面显示倒是简单但后厨改一次文案所有 if 判断跟着碎。我建议用整型状态码加常量类public class OrderStatus { public static final int PENDING_PAY 0; public static final int PAID 1; public static final int SERVED 2; public static final int FINISHED 3; public static final int CANCELED -1; }判断逻辑里写order.getStatus() OrderStatus.PAID前端拿到状态码再映射成“待支付”“制作中”“已出餐”。这样后端逻辑稳定前端想改显示文案直接改映射表。状态流转也要收口只有PENDING_PAY能点取消只有PAID能置为SERVED不能从FINISHED跳回PAID。这个项目的规模用 if 判断足够了不必上状态模式。具体放在 Service 层处理public boolean cancelOrder(Long orderId) { Orders order orderMapper.selectById(orderId); if (order null || order.getStatus() ! OrderStatus.PENDING_PAY) { throw new BusinessException(当前订单状态不可取消); } order.setStatus(OrderStatus.CANCELED); return orderMapper.updateById(order) 0; }这段代码有两个关键点先查再改两个操作之间如果并发进来两次取消请求理论上都会读到PENDING_PAY。对这个量级的项目直接把状态更新写成UPDATE orders SET status -1 WHERE id ? AND status 0用数据库行锁兜底比在应用层加锁简单可靠。要注意异常抛出后事务回滚BusinessException上要标Transactional(rollbackFor Exception.class)。2.3 Spring Boot 后端Controller 只管接收业务放 Service这套源码的三层结构比较标准Controller 负责参数接收和结果包装Service 负责事务和业务规则Mapper 负责 SQL。一个创建订单的接口大致是这样RestController RequestMapping(/api/order) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } PostMapping(/create) public ResultLong create(RequestBody OrderCreateDTO dto) { Long orderId orderService.createOrder(dto); return Result.success(orderId); } }ResultT是统一返回体包含code、message、data三个字段。前端拿到code 200才取data否则弹错误信息。这里的RequestBody要求前端传的 JSON 字段名与 DTO 属性名一致比如userId、items、remark不一致会直接反序列化失败报HttpMessageNotReadableException。Service 里创建订单要干三件事校验菜品是否存在且在售、计算总价并快照明细、一次事务里插入订单主表和明细表。重点说一下明细插入很多人用 for 循环逐条 insert小项目没问题但如果菜品数量多建议批量插入。MyBatis 的foreach拼一条多值 SQL能少几次网络往返insert idinsertBatch INSERT INTO order_detail (order_id, dish_id, dish_name, price, quantity) VALUES foreach collectionlist itemdetail separator, (#{detail.orderId}, #{detail.dishId}, #{detail.dishName}, #{detail.price}, #{detail.quantity}) /foreach /insertforeach的collection要写list如果方法参数用Param(details)注解这里就要改成details。批量插入的 SQL 长度会随数据量增大超过max_allowed_packet会报错但点餐场景一次最多十几道菜远达不到限制。2.4 Mapper 写法从注解到 XML 的边界如果项目用的是 MyBatis-Plus单表 CRUD 直接继承BaseMapperT就不用写 XML。但“查询最近订单”“按销量排序”这类带条件的语句我推荐用注解方式写在 Mapper 接口里可读性比 XML 好Select(SELECT * FROM orders WHERE user_id #{userId} ORDER BY create_time DESC LIMIT 5) ListOrders selectRecentOrders(Param(userId) Long userId);#{}是预编译占位符MyBatis 会把它转成?防止 SQL 注入。${}是字符串拼接只能用来拼接表名或者排序字段而且前端传进来的值必须白名单校验。比如排序字段只允许create_time、total_amount这两个值否则直接返回参数错误。数据库表和索引设计起来不难真正容易翻车的是状态字段的取值没人维护。把这套表结构和状态定义拿出来单独也能当一份数据库课程设计交。3. Vue 前端点餐页、购物车与路由守卫3.1 Vite 工程与依赖安装Vue 端我用的是 Vite Vue 3 Pinia Element Plus这套组合也是目前 springboot vue 项目最常见的搭档。拿到源码后先看package.json里 dependencies再执行依赖安装npm install如果安装慢可以换淘宝镜像源npm config set registry https://registry.npmmirror.com。这里要提醒的是项目里的package-lock.json如果锁了旧版本依赖在 Node 高版本下可能报ERR_PACKAGE_PATH_NOT_EXPORTED。我一般会删掉node_modules和锁文件重新装rm -rf node_modules package-lock.json npm install。Vue 安装依赖时最常见的坑是sass和vite版本不匹配报错会提示legacy-js-api这种时候把sass降到 1.32.x 即可。3.2 axios 封装把鉴权和错误处理收口前后端联调时每个页面都写一遍axios.get会非常散。我通常先做一层封装import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.response?.data?.message || error.message) return Promise.reject(error) } ) export default servicebaseURL写成/api开发环境靠 Vite 代理转发到后端生产环境靠 Nginx 转发前端代码里不出现具体 IP 和端口避免换环境到处改。code ! 200时统一弹错误信息业务代码里就不用每个接口都 try catch 一遍。这里有个细节如果后端返回的data里还包了一层分页对象前端拿到的类型是res.data.list而不是res.data本身接口分层时要把这个约定写清楚。3.3 购物车用 Pinia刷新不丢跨页面共享点餐页最核心的是购物车状态。用localStorage直接存选中的菜品也能用但组件之间同步状态得靠事件代码很快会臭。用 Pinia 管理更清晰import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [] }), getters: { totalCount: (state) state.items.reduce((sum, i) sum i.quantity, 0), totalAmount: (state) state.items.reduce((sum, i) sum i.price * i.quantity, 0) }, actions: { addItem(dish) { const existing this.items.find(i i.dishId dish.id) if (existing) { existing.quantity } else { this.items.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1 }) } } } })注意price要取后端返回的实时价格不要取展示在页面上的格式化字符串。有些接口把价格序列化成28.00字符串用String做加法就会变成28.0028.00。购物车状态需要持久化时用 Pinia 的pinia-plugin-persistedstate插件把items同步到sessionStorage比手写watch干净。为什么不用localStorage一直存着因为用户关掉浏览器再打开购物车还在会很奇怪点餐场景会话结束就该重置。3.4 点餐页联动与下单提交菜品分类用侧边栏右侧展示菜品列表点击“加入购物车”按钮后右上角角标变化。这个联动逻辑就是categoryId变化时重新请求/api/dish/list?categoryIdxxx拿到数据后渲染卡片。页面里最常见的低级错误是请求菜品列表时没有处理加载态接口慢时用户连点两次购物车里出现重复项。我的做法是点击后先 disable 按钮等await cartStore.addItem(dish)完成后再恢复。下单提交时路由要带订单号跳转到详情页const submitOrder async () { const { data: orderId } await api.post(/order/create, { userId: userStore.id, items: cartStore.items }) router.push({ name: OrderDetail, params: { id: orderId } }) }这里的params传参刷新页面会丢因为路由参数存在内存里。要保证刷新后还在应当把订单 id 放到 query 上router.push({ path: /order/detail, query: { id: orderId } })。Vue 路由参数这个坑在点餐系统里尤其明显因为用户很可能会在支付中途刷新页面。提交完订单要清空购物车否则返回点餐页发现菜品还在这是体验问题也是答辩时容易被问到的细节。4. 前后端联调Vite 代理、Nginx 部署与报错排查4.1 开发期跨域Vite proxy 配置Spring Boot 后端跑在 8080Vue 跑在 5173端口不同必然有跨域。前端接跨域最简单的不是在后端写CrossOrigin而是用 Vite 代理。修改vite.config.jsexport default { server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样页面里请求/api/order/list会被 Vite 转发到http://localhost:8080/api/order/list。changeOrigin: true会让后端拿到的请求头Host变成localhost:8080避免某些框架根据 Host 做校验时报错。注意如果后端接口本身带了/api前缀代理不需要 rewrite如果后端接口是/order/list前端请求/api/order/list那就要加rewrite: (path) path.replace(/^\/api/, )。不改 rewrite 的后果是请求打到 8080 的/api/order/list而后端 Controller 映射是/order/list直接 404。4.2 生产环境打包与 Nginx 反向代理开发联调通过后执行npm run build生成dist静态文件。这里要注意vite.config.js里的base配置。默认base是/如果部署在服务器子路径/order/下必须设置base: /order/否则 js/css 资源全部 404页面空白。部署到 Nginx 时我一般这样配置server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html;是必须的。因为 Vue Router 默认用 history 模式用户直接访问/order/detail?id123Nginx 找不到这个真实文件要把它重写到index.html否则刷新就是 404。如果项目路由用的是 hash 模式URL 长这样/#/order/detail就不需要这个配置。我这个配置里/api/转发到后端时路径会保留/api所以后端接口必须带/api前缀否则就把proxy_pass http://127.0.0.1:8080/;带斜杠来去除前缀。4.3 常见报错端口占用、数据库连接失败、刷新 404这套源码在 window10/11 上跑最容易栽的坑有三个。第一个是后端启动失败报Port 8080 was already in use说明端口被占了。在 cmd 里查占用进程netstat -ano | findstr 8080 taskkill /PID 1234 /F第二个是数据库连接不上Access denied for user rootlocalhost。检查application.yml里的数据库名、用户名、密码是否和本地 MySQL 一致特别是spring.datasource.url里的characterEncodingutf8和useSSLfalse少了后面那个在 MySQL 8 高版本会警告但不至于连不上。第三个是 Vue 打包后布局异常图片裂掉、样式丢失先看浏览器 Network 里 css/js 的路径如果是绝对路径/assets/xxx.js但部署在子目录就是base配置问题。这套排查顺序基本覆盖了我给 springboot 项目做联调时遇到的九成问题。5. 答辩前把项目改成自己的三个可验证的改动技巧5.1 给菜品增加“销量”字段和排序很多同类毕设的菜品列表都是按id或分类排答辩时容易一眼看穿是模板。给dish表加一个sales字段在下单事务里同步累加销量UPDATE dish SET sales sales 1 WHERE id #{dishId}然后菜品列表接口增加一个sortByhot参数按销量倒序。前端在点餐页顶部加一个“按销量排序”的切换按钮。这个改动能让整个项目的业务逻辑多一个闭环而且涉及数据库、后端、前端三层是很好的加分项。5.2 把订单状态数字替换成可读枚举前端页面里如果到处都是order.status 0答辩时老师随机问“状态 2 是什么”会卡壳。在utils/orderStatus.js里定义映射export const ORDER_STATUS_MAP { 0: { label: 待支付, type: warning }, 1: { label: 制作中, type: primary }, 2: { label: 已出餐, type: success }, 3: { label: 已完成, type: info }, -1: { label: 已取消, type: danger } }页面里统一通过ORDER_STATUS_MAP[order.status]获取文案和 tag 类型以后想改“制作中”为“备餐中”只改一处。这个改动不涉及后端但能让代码质量看起来高一个档次。5.3 用 curl 写一份接口验证清单答辩演示时现场点页面最怕后端突然连不上。我习惯把核心接口验证写成脚本答辩前跑一遍curl -s -X POST http://localhost:8080/api/order/create \ -H Content-Type: application/json \ -d {userId:1,items:[{dishId:1,quantity:2}]} | jq .把上面命令保存为verify.sh之后每次启动项目执行bash verify.sh看到返回code: 200就说明数据库、后端、接口链路都是通的。演示视频里的操作顺序也是这套先启动 MySQL再启动 Spring Boot最后跑前端。按这个清单验证过再进答辩现场能少很多突发状况。本文还有配套的精品资源点击获取