news 2026/9/14 8:05:35

SpringBoot+Vue点餐平台开发实战:从表设计到订单状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue点餐平台开发实战:从表设计到订单状态机

简介:以点餐平台网站作为主题的Java毕业设计项目,基于Spring Boot与Vue技术栈实现前后端分离的B/S架构,是一份包含源码、说明与数据库的完整工程资料。项目面向计算机专业学生,适用于毕业设计、课程设计及全栈开发练习,完整覆盖管理员端、用户端和前台首页三大模块,包含菜品分类、菜品信息、订单管理、购物车、在线客服、菜品评价等核心业务功能。压缩包内共1652个文件,整体大小约31.25MB,主要文件类型有java与class后端源码、vue与js前端脚本、html与css静态页面、sql数据库脚本及docx说明文档,可方便查看设计文档与数据库结构。目前已有115人学习下载,资源附带数据库与说明文件,能帮助快速搭建运行环境,并通过完整源码深入理解权限控制、业务流转和前后端交互逻辑,便于二次开发与论文写作。

1. 点餐平台为什么是 Java 毕业设计的“闭环首选”

点餐平台(在线订餐系统)在 Java 毕业设计里属于业务闭环最完整的选题:前台负责菜品展示、购物车、下单、评价,后台负责用户、菜品分类、菜品信息、订单和系统管理,把 SpringBoot 的 ORM、事务控制和 Vue 的路由、组件通信都串了起来。

拿到源码包先确认三件事:数据库脚本能否单独导入、配置文件里的 MySQL 账号密码是否匹配、前后端是否分端口启动。项目里的update-password.vue.bakIndexAsideStatic.vue.bak等备份,是交付前改过登录页和后台布局留下的痕迹,页面改坏了可以直接用.bak回滚。

适合课程设计赶时间、答辩要现场演示的学生,也适合想快速搭一套带权限和订单流的管理系统骨架的初级工程师。后面按表结构、后端接口、前端交互、权限状态机、部署验证的顺序展开,每步都能在源码里找到对应位置。

2. SpringBoot 后端:数据库表设计与菜品模块的实体映射

先看后端整体结构,标准的三层:Controller 收参数、Service 写业务、Mapper 操作数据库。点餐平台这类项目的核心不在一张表多少字段,而在表与表之间的关联关系怎么建。用户、菜品、订单、评价、收藏这几张表一旦打通,前台展示和后台管理就都能挂在同一套数据模型上。

2.1 点餐系统最少需要几张表

表名关键字段在系统里的职责
userusername、password、role普通用户与管理员共用,靠 role 区分(0 用户 / 1 管理员)
dish_categoryname、sort菜品分类,前台按分类筛选的依据
dishname、category_id、image、price、sales菜品主表,sales 用于按销量排序
ordersorder_no、user_id、amount、status、address订单主表,status 驱动订单流程
order_detailorder_id、dish_id、dish_name、number下单时的菜品快照,防止菜品改名后订单无据可查
dish_commentdish_id、user_id、star、content菜品评价,star 用 1~5 的整数
collectuser_id、dish_id用户“我的收藏”
newstitle、content、cover前台菜品资讯

一个直接踩得上的坑:订单主表别叫order,它是 MySQL 的保留字,建表时会报语法错误,源码里用的是orders,课程设计里也建议统一用复数或加tb_前缀。

价格字段我一般建议用「分」存整数,而不是用decimal存元。理由是浮点精度问题,0.1 加 0.2 在计算机里不是 0.3,点餐这种高频加购计算的场景一旦用浮点累加,订单金额最后一位会对不上。用整数分存储,前端展示时除以 100 就行,这个细节也常被用来回答 java 面试题里的金额精度问题。

2.2 实体类与 MyBatis-Plus 的映射关系

源码用的持久层框架是 MyBatis-Plus,实体类通过注解映射到表,不需要写一堆 XML:

@TableName("dish") public class Dish { @TableId(type = IdType.AUTO) private Integer id; private String name; @TableField("category_id") private Integer categoryId; private String image; private Integer price; // 单位:分 private Integer sales; // getter / setter 省略 }

@TableName指定实体对应的表名,@TableId(type = IdType.AUTO)声明主键自增,@TableField只在驼峰字段和下划线字段对不上时用。MyBatis-Plus 默认开启驼峰转换,categoryId会自动映射到category_id,所以大部分字段不用额外加注解。

注意:如果改动过数据库字段名,启动后报Unknown column,先去application.yml确认map-underscore-to-camel-case有没有被改成 false,这是改配置时最容易被误伤的一项。

2.3 菜品分页查询接口的参数设计

前台菜品列表要支持分页、按名称模糊搜索、按分类过滤,Controller 里对应的接口参数如下:

@RestController @RequestMapping("/dish") public class DishController { @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "6") Integer pageSize, @RequestParam(required = false) String name, @RequestParam(required = false) Integer categoryId) { Page<Dish> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Dish> qw = new LambdaQueryWrapper<>(); qw.like(StrUtil.isNotBlank(name), Dish::getName, name) .eq(categoryId != null, Dish::getCategoryId, categoryId) .orderByDesc(Dish::getSales); return Result.ok(dishMapper.selectPage(page, qw)); } }
参数类型默认值说明
pageNumInteger1页码,从 1 开始
pageSizeInteger6每页条数,建议设上限 20
nameString菜品名称模糊匹配
categoryIdInteger分类精确过滤

like方法第一个条件为 false 时,整段查询条件会被跳过,所以name为空时不会拼WHERE name LIKE '%%'这种低效条件。orderByDesc(Dish::getSales)让销量高的菜品排在前面,前台首页不做额外 SQL 就能拿到热销排序。

我一般会在 Service 层再加一道pageSize = Math.min(pageSize, 20)的兜底,防止前端传 9999 一次性拉全表。分页返回的totalrecords都来自同一个Page对象,前端 Vue 的分页组件直接绑定total就能正常显示总页数。

3. Vue 前端:菜品展示、购物车与评价的交互实现

前端分成两个区域:面向用户的商城页和面向管理员的控制台。后台布局用的 Element UI,源码里的几个.bak文件正好暴露了布局组件的构成,从这些备份能反推出原项目的页面结构,改起来也有据可依。

3.1 从 .bak 文件推断后台布局结构

组件文件职责对应备份
IndexAsideStatic.vue后台侧边菜单,静态版IndexAsideStatic.vue.bak
IndexHeader.vue顶部栏,含头像和退出IndexHeader.vue.bak
BreadCrumbs.vue面包屑导航BreadCrumbs.vue.bak
update-password.vue个人中心修改密码update-password.vue.bak

说明原作者在交付前把侧边栏从动态渲染改成了静态菜单,同时重做了密码修改页。备份文件的作用很直接:改组件前cp IndexHeader.vue IndexHeader.vue.bak,布局改崩了随时还原。答辩时能讲出这层用意,比单纯说“我用了 Vue”更有信息量。

3.2 路由拆分与页面懒加载

// router/index.js const routes = [{ path: '/', component: () => import('@/views/front/Layout.vue'), children: [ { path: '', component: () => import('@/views/front/DishList.vue') }, { path: 'cart', component: () => import('@/views/front/Cart.vue') }, { path: 'order', component: () => import('@/views/front/MyOrder.vue') } ] }, { path: '/admin', component: () => import('@/views/admin/Layout.vue'), children: [ { path: 'dish', component: () => import('@/views/admin/DishManage.vue') }, { path: 'order', component: () => import('@/views/admin/OrderManage.vue') } ] }]

路由按用户端和管理员端拆成两个 Layout,各自有独立的侧边栏和头部。() => import()是懒加载,首屏只下载当前路由对应的组件,而不是把整个后台代码一次性打进app.js。vue 打包后布局异常的一个常见原因就是首屏包太大渲染慢,懒加载能顺带缓解这个问题。

注意:/admin下的页面必须配合登录权限守卫,否则直接输 URL 就能绕过前端菜单进后台。守卫通常写在router.beforeEach里,检查 localStorage 里的登录态和 role,不满足条件就next('/login')

3.3 购物车的本地状态与下单前校验

购物车这个模块,很多课程设计会建一张cart表,但常见做法是放前端,用 localStorage 持久化:

// store/cart.js const KEY = 'cart_items' export default { namespaced: true, state: { items: JSON.parse(localStorage.getItem(KEY) || '[]') }, mutations: { add(state, dish) { const item = state.items.find(i => i.id === dish.id) if (item) { item.count++ } else { state.items.push({ id: dish.id, name: dish.name, price: dish.price, count: 1 }) } localStorage.setItem(KEY, JSON.stringify(state.items)) }, clear(state) { state.items = [] localStorage.removeItem(KEY) } } }

加购、改数量、清空购物车全部在浏览器端完成,刷新页面数据不丢,后端不需要维护中间态数据。要点是下单提交时,商品单价必须以服务端查询结果为准,不能用 localStorage 里的价格直接算总额——用户可以改浏览器里的值,服务端必须重新校验一遍价格和库存。

3.4 菜品评价的提交与重复校验

用户端有“菜品评价管理”,管理员端也能看到评价,评价不是随便发的,必须和订单状态绑定。常见规则是订单完成后才允许评价,防止没消费就刷好评,后端校验:

Order order = orderService.getById(orderId); if (order == null || !order.getUserId().equals(loginUserId) || !"3".equals(order.getStatus())) { return Result.error("订单未完成,不能评价"); } DishComment comment = new DishComment(); comment.setDishId(dishId); comment.setUserId(loginUserId); comment.setStar(star); // 1~5 comment.setContent(content); commentService.save(comment);

"3"对应订单状态机里的“已完成”,这个值和第 4 章的状态定义必须一致,前后端任何一处写死成别的数字都会导致评价功能失效。如果再严一点,给dish_comment加上order_id字段,查询时先select count判断是否评过,就能避免同一个订单对同一道菜重复评价。

4. 双端权限与订单状态机:从下单到完成的闭环

用户端和管理员端最核心的差异在权限和数据范围:用户只能看自己的订单和自己的收藏,管理员能看到全部订单且能推进订单状态。这两件事靠一张user表的 role 字段和一套订单状态机就能实现。

4.1 登录态与角色权限的两种做法

课程设计级别的项目,session 加拦截器是最稳的方案,登录成功后把用户对象放进 session,需要管理员权限的接口统一走拦截器:

public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object admin = request.getSession().getAttribute("admin"); if (admin == null) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } return true; } }

注册到WebMvcConfigurer里,只拦截/admin/**路径。这套做法的好处是不用引入 JWT 依赖,session 失效就自动退出,适合演示环境。如果改为 JWT,请求头里带 token,后端每次解析,优点是前后端完全分离、扩展性好,但要在拦截器里自己处理 token 过期。两种实现二选一即可,答辩被问到就讲清楚各自取舍。

4.2 订单状态机的五态流转与防跳变

订单状态是整个项目里最容易出 bug 的地方。直接update orders set status = 新值 where id = ?会允许从“已完成”跳回“已接单”,逻辑上就乱了。规范做法是先定义合法流转:

状态值状态含义允许的下一步
0待处理(新订单)1 接单、4 取消
1已接单2 配送中、4 取消
2配送中3 已完成
3已完成无(仅可评价)
4已取消

对应的判定逻辑:

private static final Map<Integer, List<Integer>> FLOW = new HashMap<>(); static { FLOW.put(0, Arrays.asList(1, 4)); FLOW.put(1, Arrays.asList(2, 4)); FLOW.put(2, Arrays.asList(3)); } public boolean canTransit(int from, int to) { return FLOW.getOrDefault(from, Collections.emptyList()).contains(to); }

管理员接单、发货、完成三个操作分别调用canTransit校验,不合法直接返回“当前状态不可执行该操作”。更新 SQL 也建议带上旧状态条件:

UPDATE orders SET status = 2 WHERE id = #{id} AND status = 1

即使两个请求并发进来,只有一条能真正更新成功,这是数据库层面的最后一道防线。状态机防跳变的思路,在 springboot 面试题里也常被拿来问“如何防止业务状态被非法修改”,答上这一层会比只说if判断留下更好的印象。

4.3 订单列表的关联查询与订单明细快照

订单列表要显示用户名、菜品名、金额和状态,一次关联查询搞定:

SELECT o.id, o.order_no, u.username, GROUP_CONCAT(d.dish_name) AS dish_names, o.amount, o.status, o.create_time FROM orders o LEFT JOIN user u ON o.user_id = u.id LEFT JOIN order_detail d ON d.order_id = o.id GROUP BY o.id ORDER BY o.create_time DESC

GROUP_CONCAT把同一个订单的多个菜品名拼成一行,适合列表展示。注意userorder这类名字建议加反引号,避免在不同 MySQL 版本下触发保留字问题。订单提交时order_detail里存的是菜品名称和单价快照,之后菜品改价或下架都不影响历史订单的展示,这是订单系统的基本要求。

管理员端的“系统管理”通常是轮播图和菜品资讯这类简单 CRUD,前台“在线客服”在课程设计里一般是页面右下角的对话面板,复杂一点的接 WebSocket 做实时聊天,但为了答辩稳定性,模拟对话比长连接更不容易在演示时翻车。

5. 部署与答辩前的验证清单

5.1 解压后跑通全流程的三个高频坑

# 1. 导入数据库(先建库再导数据) mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS ordering DEFAULT CHARSET utf8mb4;" mysql -uroot -p ordering < sql/ordering.sql # 2. 启动后端 mvn spring-boot:run # 3. 启动前端 cd frontend npm install npm run serve

第一个坑是 MySQL 连接报Public Key Retrieval is not allowed,在 jdbc url 上加allowPublicKeyRetrieval=true&useSSL=false即可,本质是 mysql-connector-j 8.x 对加密连接的行为变化;如果 springboot 版本太高导致驱动类加载异常,优先把驱动版本降回和数据库匹配的版本,而不是盲目升级框架。

第二个坑是 vue 打包后布局异常,npm run build出来的页面空白或 CSS 路径 404,多半是静态资源用了绝对路径。在vue.config.js里设publicPath: './',资源改成相对路径就正常了。开发模式下的异常则优先检查 Element UI 版本和 Vue 2/3 是否匹配。

第三个坑是端口占用。后端改application.yml里的server.port,前端通过devServer.proxy/api代理到后端地址,代理配错的表现是前端能打开但所有请求 404。改页面代码前,按原作者的习惯先cp 目标文件 目标文件.bak,改坏了能立刻还原。

5.2 答辩演示的完整验证路径

演示顺序建议直接沿着业务主线走:管理员登录后新增菜品分类和菜品,前台用户注册登录,把菜品加入购物车并下单,管理员在订单管理中接单、标记配送中、完成订单,用户端确认订单完成后对菜品评价,最后切到管理员端的评价管理查看这条评价。每个关键步骤后,打开数据库客户端查一眼ordersorder_detail的记录,让评委看到数据是真实落库的,而不是前端写死的假数据。

准备一个能自圆其说的失败场景也很有用,比如演示时故意用未完成的订单去评价,让接口返回“订单未完成,不能评价”,再解释这是状态机校验在起作用。现场能把一个报错讲清楚,比一路顺畅地跑完更有说服力。答辩前把这三件事验证到位:数据库重新导入一次能成功、前后端从零启动一次能通、核心链路完整走一遍,这份源码就能稳稳撑起整场演示。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 8:02:45

Netty不是Java必修课,却是高并发架构分水岭:线程模型与实战避坑

做Java这么久&#xff0c;不管是在技术群还是面试现场&#xff0c;“要不要深入学Netty”这个问题我听了不下几十遍。有人觉得Netty就是搞网络编程的框架&#xff0c;业务开发根本碰不到&#xff1b;也有人一头扎进Netty源码&#xff0c;结果被Reactor模型和堆外内存折腾得怀疑…

作者头像 李华
网站建设 2026/9/14 8:02:35

工业视觉云边协同架构设计与落地实践:从固化困境到持续进化

前阵子跟一个做汽车零部件视觉检测的朋友吃饭&#xff0c;他说了一句话让我印象深刻&#xff1a;“我们那套视觉系统&#xff0c;验收那天就是它最好用的一天&#xff0c;之后每天都在走下坡路。”三年前上线的设备&#xff0c;当时节拍、准确率全部达标&#xff0c;可如今客户…

作者头像 李华
网站建设 2026/9/14 7:59:20

Rocky 9.4 下 ELK 日志分析系统部署实战与性能优化

1. 项目概述与整体方案设计1.1 为什么要在Rocky 9.4上搭ELK日志分析这件事&#xff0c;只要是跑业务的服务器&#xff0c;基本都绕不开。服务器一多&#xff0c;靠着tail -f逐个翻日志的日子就过不下去了。ELK这套组合——Elasticsearch负责存储和检索&#xff0c;Logstash负责…

作者头像 李华
网站建设 2026/9/14 7:56:57

ESP32-S3 N16R8开发实战:Flash+PSRAM精准适配指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 7:56:52

大模型训练数据的时间边界:2024年现象解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华