SpringBoot+Vue构建私房菜上门O2O系统:订单状态机与预约冲突检测
2026/9/16 9:10:18 网站建设 项目流程

简介:这是一个基于Java Spring Boot + Vue + MySQL的私房菜定制上门服务系统毕业设计资源包,面向计算机专业学生,可用于毕业设计、课程设计或期末大作业。资源包含前后端完整源码、数据库脚本、软件工具和论文文档,下载后按文档说明即可直接运行。压缩包共801个文件、约27.69MB,主要涵盖Java后端源码、Vue前端组件、JS/CSS/HTML页面资源、SQL数据库脚本、Word论文文档及bat启动脚本等,类型覆盖开发、配置、部署与文档各个环节。系统支持用户根据口味定制菜单、在线预约厨师上门,后端采用Spring Boot处理业务逻辑,前端使用Vue提供流畅交互体验,MySQL负责存储用户、订单与菜品数据,整体功能完善且经过严格调试,具备较高实际应用价值。压缩包中附带完整设计论文、启动脚本和清晰项目结构,便于理解系统设计思路、数据库表关系及二次开发;目前已有72人学习下载,可作为Spring Boot+Vue全栈项目的快速上手范例。

1. 私房菜上门服务系统的业务模型与技术选型

私房菜定制上门服务,本质上是一个典型的 O2O 预约履约系统:用户在线定制菜品、选择口味偏好、预约厨师上门时段,厨师接单后按约定时间上门烹饪。整套流程里最容易被忽视、但也是最值钱的部分,不是页面长得是否好看,而是订单状态机的设计、预约时段的冲突检测、以及厨师的接单履约链路。这三个点任何一个没处理好,系统上线后就会出现用户下单成功、厨师也接单了,但到了约定时间双方都不知道去哪儿碰面的尴尬局面。

这套系统选用 java + springboot + vue + mysql 的组合,是比较成熟的毕业设计技术栈。springboot 负责后端业务逻辑,vue 做单页面应用,mysql8.0 存储用户、订单、菜品、厨师等核心数据,maven 管理依赖构建。项目的压缩包里包含完整源码、数据库脚本和设计论文,后端采用 Spring Boot 2.x 版本配合 MyBatis 操作数据库,前端基于 Vue 2 + Element UI 组件库,构建产物里能看到 element.min.css、app.cfd6f916.css 这类典型的前端打包文件。整体适合两类人:一类是需要毕业设计或课程设计交付物的在校生,直接跑通后改改业务细节就能用;另一类是刚接触前后端分离项目、想弄清楚一个完整 O2O 流程怎么落地的初级工程师。接下来按后端、前端、数据库、部署排错的顺序,把关键设计拆开讲。

2. SpringBoot后端:JWT鉴权与订单状态机的落地实现

2.1 项目结构与Maven多模块拆分

解压源码包后,先看整体目录布局。典型的 SpringBoot 单模块项目通过包名区分层次,这套系统在src/main/java下按 controller、service、mapper、entity、config 分包。启动类上标注了@SpringBootApplication@MapperScan,后者负责扫描 MyBatis 的 Mapper 接口,application.yml 里配置了数据源、端口和 MyBatis 的驼峰映射规则。

src/main/java/com/example/privatechef ├── controller │ ├── UserController.java │ ├── OrderController.java │ ├── ChefController.java │ └── AdminController.java ├── service │ ├── impl │ │ ├── OrderServiceImpl.java │ │ └── UserServiceImpl.java ├── mapper │ ├── OrderMapper.java │ ├── UserMapper.java │ └── ChefMapper.java ├── entity │ ├── Order.java │ ├── User.java │ └── Chef.java ├── config │ └── WebMvcConfig.java └── util └── JwtUtil.java

需要注意WebMvcConfig这类配置类的作用:它注册了拦截器,放行登录注册接口,其余接口全部校验 Token。启动脚本里常见3-build.bat2-run.bat1-install.bat三个批处理文件,执行的顺序是先 install 安装依赖、再 build 打包、最后 run 启动,实际上分别对应mvn clean installmvn packagejava -jar target/xxx.jar三个阶段。

2.2 基于JWT的登录鉴权设计

系统没有采用传统 Session 方案,而是用 JWT 做无状态鉴权。用户登录成功后,后端生成 Token 返回给前端,前端存储到 localStorage,之后每次请求在 Header 中携带Authorization: Bearer <token>。后端拦截器解密 Token 并取出用户 id、角色等信息放入 ThreadLocal 或直接写入 Request 属性,供后续业务使用。这样做的好处是服务端不需要维护 Session,扩展多实例部署时不需要额外做 Session 同步。

public class JwtUtil { private static final String SECRET = "private-chef-secret-key"; private static final long EXPIRE_MS = 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_MS)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

代码逻辑拆开看:生成 Token 时把 userId 和 role 放进 claim,过期时间设为 7 天,使用 HS256 对称加密。解析 Token 时如果签名不正确或已过期,会抛出异常,拦截器捕获后直接返回 401。这里有一个值得注意的点:JWT 的过期时间是写在客户端 Token 里的,服务端无法主动让其失效,所以在做用户禁用功能时,需要额外维护一个黑名单表或用 Redis 存用户状态,否则被禁用的用户还能在有效期内继续访问接口。

2.3 订单状态机与支付流程的代码实现

订单模块是整个系统最复杂的部分。用户从下单到完成,订单状态会经历多次流转,如果直接写 if-else 判断状态,后期每加一个分支代码就会乱一倍。这套系统的方案是使用状态枚举 + 流转校验方法,把状态迁移集中管理。

public enum OrderStatus { WAIT_PAY(0, "待支付"), WAIT_ACCEPT(1, "待接单"), ACCEPTED(2, "已接单"), SERVICE_ING(3, "服务中"), WAIT_COMMENT(4, "待评价"), FINISHED(5, "已完成"), CANCELLED(6, "已取消"); private final int code; private final String desc; public int getCode() { return code; } public String getDesc() { return desc; } OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } }

状态流转表如下:

当前状态允许的下一状态触发动作
待支付待接单、已取消用户支付 / 超时或手动取消
待接单已接单、已取消厨师接单 / 用户取消
已接单服务中、已取消厨师开始服务 / 双方协商取消
服务中待评价服务完成,用户确认
待评价已完成用户提交评价

核心的状态校验逻辑集中在一个方法里,避免散落在各业务代码中:

public void checkTransition(OrderStatus from, OrderStatus to) { switch (from) { case WAIT_PAY -> { if (to != WAIT_ACCEPT && to != CANCELLED) { throw new BizException("待支付订单只能接单或取消"); } } case WAIT_ACCEPT -> { if (to != ACCEPTED && to != CANCELLED) { throw new BizException("待接单订单只能接单或取消"); } } case ACCEPTED -> { if (to != SERVICE_ING && to != CANCELLED) { throw new BizException("已接单订单只能开始服务或取消"); } } case SERVICE_ING -> { if (to != WAIT_COMMENT && to != FINISHED) { throw new BizException("服务中订单只能确认完成或直接结束"); } } default -> throw new BizException("终态订单不允许变更"); } }

这里用了 Java 14+ 的 switch 箭头语法,如果本地环境是 JDK 8,需要改成传统 case-break 写法。生成订单时默认状态是待支付,记录创建时间;支付回调里先做幂等判断——同一订单不能重复支付——再把状态改为待接单。接单操作要注意事务:更新订单状态和锁定厨师时段必须放在同一个事务里,避免厨师同时接了两单。这套状态机的设计逻辑,也是 java 面试题里高频出现的场景,考察点在于状态校验和并发处理。

3. Vue前端:定制菜单、预约下单与Element UI组件封装

3.1 路由设计与页面骨架

前端通过 vue-router 管理页面跳转,使用懒加载方式引入组件,构建时自动分包。系统按角色拆分路由模块:用户端有首页、菜品列表、菜品定制、订单列表、个人中心;厨师端有接单大厅、我的服务;管理端有用户管理、厨师审核、订单管理、菜品管理。

const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/user', component: () => import('@/layout/UserLayout.vue'), children: [ { path: 'menu', component: () => import('@/views/user/MenuList.vue') }, { path: 'customize', component: () => import('@/views/user/CustomizeOrder.vue') }, { path: 'orders', component: () => import('@/views/user/OrderList.vue') } ] } ]

vue-router 的懒加载写法() => import()在 webpack 打包时会生成独立的 chunk 文件,首屏只加载登录页和布局组件。路由参数的使用在菜品详情页比较典型:this.$route.query接收从列表页传递的菜品 id 和分类 id,而this.$router.push跳转时通过 query 对象携带参数。需要注意,路由守卫beforeEach里做了登录态校验,没有 Token 直接跳转/login?redirect=目标路径,登录成功后回跳原页面。这套逻辑在 vue 面试题中经常被问到,重点是守卫钩子的执行时机和 next() 的用法。

3.2 定制菜单的组件编写

定制菜单是本系统区别于普通外卖系统的核心模块。用户选择菜品后,需要进一步选择口味偏好、忌口信息、上门时间、服务地址,最后提交订单。整个定制流程拆成三个步骤组件:菜品选择、口味定制、预约确认。

<el-steps :active="step" finish-status="success"> <el-step title="选择菜品" /> <el-step title="口味定制" /> <el-step title="预约确认" /> </el-steps>

菜品选择组件从后端接口拉取菜品分类和菜品列表,左侧是分类树,右侧是菜品卡片。每张卡片上有加号按钮,点击后菜品进入购物车并弹出口味选择框。口味定制组件收集用户对辣度、咸淡、忌口食材等自定义需求,存放于一个customizeOptions对象中:

export default { data() { return { selectedDishes: [], customizeOptions: { spiciness: '', // 辣度 saltiness: '', // 咸淡 allergies: '', // 忌口 remark: '' // 备注 }, appointmentTime: '', step: 0 } }, methods: { submitOrder() { const orderPayload = { dishes: this.selectedDishes, options: this.customizeOptions, appointmentTime: this.appointmentTime, address: this.userAddress } post('/order/create', orderPayload).then(res => { this.$router.push({ path: '/user/orders', query: { orderId: res.data.orderId } }) }) } } }

提交订单时把菜品数组、定制选项、预约时间、地址一次性传给后端。appointmentTime是日期时间字符串,形如2025-06-15 10:30:00。这里有一个实现细节:口味定制组件把辣度、咸淡等选项用 v-model 绑定到对象属性上,用户每次切换选项都会触发重新渲染,不需要手动写事件监听。菜品数量的增减逻辑则写在方法里,每次点击加号时同步更新数量并计算总价。

3.3 Axios拦截器与Token刷新机制

Axios 拦截器是前后端交互的关键环节。请求拦截器从 localStorage 读取 Token 并添加到请求头,响应拦截器统一处理错误码:

service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error)) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

这段代码的后端在 401 时会返回{ code:401, message:'未登录或Token过期' },响应拦截器检测到后清除本地 Token 并跳转登录页。在实际生产环境中,Token 过期不会刚好在用户操作时触发,常见做法是刷新 Token 机制——后端返两个 Token,access_token 有效期 2 小时,refresh_token 有效期 7 天,access_token 过期后用 refresh_token 换取新的 access_token。但这个系统为了简化逻辑,直接把有效期设为 7 天,牺牲了部分安全性换来了实现简单,作为毕业设计是够用的。

4. MySQL数据库设计:菜品SKU、预约时段与订单查询优化

4.1 核心表结构设计

数据库脚本在 sql 目录下,核心表有用户表、厨师表、菜品表、订单表、订单明细表、预约时段表、评价表。这里重点说两个设计关键点:菜品 SKU 和预约时段。

菜品定制会生成不同的规格组合,比如"辣味排骨"和"不辣排骨"在厨师的采购清单和成本核算上不同,所以菜品表拆出基础信息和 SKU 信息。实际表设计如下:

CREATE TABLE `dish` ( `dish_id` bigint(20) NOT NULL AUTO_INCREMENT, `dish_name` varchar(100) NOT NULL COMMENT '菜品名称', `category_id` int(11) NOT NULL COMMENT '分类ID', `base_price` decimal(10,2) NOT NULL COMMENT '基础价格', `image_url` varchar(500) DEFAULT NULL COMMENT '图片地址', `description` varchar(500) DEFAULT NULL COMMENT '菜品描述', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`dish_id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表'; CREATE TABLE `order_info` ( `order_id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `chef_id` bigint(20) DEFAULT NULL COMMENT '厨师ID', `appointment_time` datetime NOT NULL COMMENT '预约上门时间', `address` varchar(255) NOT NULL COMMENT '服务地址', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态', `customize_options` varchar(1000) DEFAULT NULL COMMENT '定制选项JSON', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`order_id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_chef_id_status` (`chef_id`, `status`), KEY `idx_appointment_time` (`appointment_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单表的customize_options字段直接存储 JSON 字符串,内容就是前端提交的辣度、忌口等信息。这种设计虽然违反了第一范式的规范,但在业务场景中反而合理:定制选项没有独立的查询需求,也不会用于关联统计,单独建表只会增加 JOIN 成本。mysql 8.0 提供 JSON 类型支持,可以直接用JSON_EXTRACT函数提取字段查询,不过该系统的查询逻辑并未用到。

4.2 预约时段冲突检测的SQL写法

预约上门服务的核心约束是"一个厨师在同一时间段只能服务一个订单"。后端的冲突检测是查同一厨师的订单表中是否存在时间重叠的订单:

SELECT COUNT(*) FROM order_info WHERE chef_id = #{chefId} AND status IN (1, 2, 3) AND appointment_time BETWEEN #{startTime} AND #{endTime}

这段 SQL 的逻辑是:插入新订单前,查询该厨师在预约时间点前后是否存在状态为待接单、已接单、服务中的订单。如果 count 大于 0,说明该时段已占用,直接提示用户更换时间。在服务时长固定的场景下(比如每单服务 2 小时),这个查询实际上是查某个时间点是否落在其他订单的 [开始, 开始+2小时) 区间内,可以用appointment_time < #{endTime} AND DATE_ADD(appointment_time, INTERVAL 2 HOUR) > #{startTime}更精确地表达。这个查询在生产环境的并发下会用到索引,idx_chef_id_status覆盖 chef_id 和 status 两个查询条件。

4.3 分页与多条件查询的索引优化

订单列表查询是典型的动态 SQL 多条件分页场景。用户端按"只查自己的订单"过滤 user_id;厨师端按"待接单接单大厅"过滤 status=1;管理端则可能同时按订单状态、下单时间范围、订单号模糊查询三个维度过滤。MyBatis 中配置对应的 SQL:

SELECT order_id, order_no, appointment_time, address, total_amount, status, create_time FROM order_info WHERE 1=1 <if test="orderNo != null and orderNo != ''"> AND order_no LIKE CONCAT('%', #{orderNo}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND create_time &gt;= #{startTime} </if> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}

这里1=1是 MyBatis 动态 SQL 拼接的常见写法,实际执行时 SQL 优化器会直接忽略这个恒真条件,不会带来性能损耗。排序字段 create_time 有索引的话,MySQL 可以使用索引避免 filesort。订单表按 create_time 倒序分页时,深分页偏移量过大会导致扫描大量无用索引记录,常见优化手段是记录上一页最大 create_time,用WHERE create_time < #{lastTime} ORDER BY create_time DESC LIMIT 20代替OFFSET。mysql 存储过程也常用于生成订单号的序列逻辑,但系统目前用时间戳拼接随机数生成 32 位订单号,足够支撑单机场景。

5. 部署、排错与二次开发:从毕业设计到商用系统

5.1 批量启动脚本与资源打包

源码包里的1-install.bat3-build.bat2-run.bat三个批处理脚本分别对应 maven 的 install、package 和 java 启动命令。生产环境部署时,建议将前端打包后的 dist 目录和后端 jar 包分离部署,前后端通过 API 网关或直接在 Nginx 层做请求转发。

server { listen 80; server_name privatechef.example.com; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; } }

Nginx 配置里try_files的作用是处理 vue-router 的 history 模式刷新 404 问题。如果是 hash 模式则不存在这个问题。静态资源目录中出现的app.cfd6f916.css这类带内容哈希的文件名,是 webpack 打包时基于文件内容计算出的指纹,浏览器会缓存带有哈希的静态资源,而 index.html 的缓存时间则需要设置得很短,否则前端发布新版本后用户浏览器仍在加载旧的 HTML 入口,导致 vue 打包后布局异常或白屏。排查资源加载问题时,可以直接在浏览器 Network 面板查看 index.html 和 js/css 文件的缓存状态,确认是否返回 304。

5.2 启动失败与vue打包后布局异常的排查

springboot 项目最常见的启动失败原因是本地 JDK 版本与项目编译目标不一致。如果使用 springboot 版本太高,同时本地环境是 JDK 8,会因为 class 文件版本不兼容直接报UnsupportedClassVersionError,该错误位于初始化阶段,日志中会明确标注「Unsupported major.minor version」。排查顺序是:先确认 pom.xml 中 springboot 父依赖版本对应的 JDK 要求,再执行java -version对比,最后在 IDEA 中点击 Project Structure 检查 Project SDK 与 Maven 的 Java 版本是否一致。

mysql 连接失败是另一类高频启动错误。确认 mysql 8.0 已安装并启动服务后,重点检查 application.yml 中的连接串,mysql8.0 驱动类的 URL 必须带时区参数serverTimezone=Asia/Shanghai,否则启动时会报时区错误。用户名密码有误则报Access denied for user,可以先用 navicat 测试连接,确认连接成功后项目再启动。启动脚本的顺序也有讲究:第一次运行时必须按 1-install -> 3-build -> 2-run 的顺序执行,因为 install 会安装依赖到本地仓库,跳过这一步直接 build 可能因为内网仓库无法访问而失败。

前端方面,vue 项目从开发到部署最容易出问题的就是接口地址。开发环境通过 webpack 的 devServer proxy 把/api前缀代理到后端,生产环境则通过 Nginx 路径转发。如果忘记在 axios 的 baseURL 中保留/api管道,会导致部署后接口请求 404。针对 vue 打包后布局异常,通用排查思路是先打开浏览器控制台看 CSS 是否加载成功,再检查 element-ui 的样式文件是否被全局覆盖,最后确认项目中是否启用了样式隔离的 scoped 属性。

5.3 容易扩展的二次开发点

这套系统已经具备可运行的基础能力,从毕业设计向商用系统演进时,有几个成本低、收益高的扩展点。第一个是接入微信小程序端:后端接口基于 RESTful 风格设计,天然支持多端共用,新增小程序端只需要重新写前端逻辑,接口层无需大改。第二个是引入支付回调的完整流程:目前系统的支付是模拟支付,实际商用需要对接微信支付或支付宝支付,重点在于处理回调幂等性和订单超时关单逻辑。第三个是消息通知:订单状态变更时,通过短信或微信模板消息通知用户和厨师,这个功能可以基于 Spring 的事件机制实现,在订单状态更新时发布事件,监听器统一发送通知,核心业务代码不用耦合通知逻辑。

系统编译时需要确认 maven 的 settings.xml 中仓库地址为国内镜像仓库,外网直连 Maven Central 经常超时。数据库初始化则直接执行 sql 目录下的脚本即可完成建库建表,mysql 8.0 的默认字符集已经是 utf8mb4,不需要额外配置中文支持。整体来看,这个项目比较适合作为学习前后端分离开发流程的完整案例,订单状态机和预约冲突检测这两块的代码可以直接迁移到其他 O2O 类项目中使用。

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

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

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

立即咨询