ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

外卖配送管理系统全栈实战:SpringBoot+Vue订单状态机与骑手抢单并发处理

外卖配送管理系统全栈实战:SpringBoot+Vue订单状态机与骑手抢单并发处理 简介这是一套面向计算机专业毕业设计场景的外卖配送管理系统完整源码包采用SpringBoot后端与Vue前端分离架构数据库使用MySQL适合正在准备毕设或课程设计的学生参考与二次开发。系统覆盖用户管理、优惠券领取、通知提醒、多支付方式银行卡、微信、支付宝以及账号登录与密码找回等核心模块功能链路较为完整。压缩包共1993个文件约37.41MB包含388个vue组件、284个js脚本、206个css样式、131个java后端类以及1个sql数据库脚本另有大量png、jpg图片与xml配置前端页面、后端逻辑与数据脚本一应俱全。目前已有39人学习下载。对于需要快速搭建可运行项目、理解前后端交互流程或撰写论文系统实现章节的读者该资源提供了清晰的目录结构与可参考的模块划分便于对照调试与功能扩展。1. 外卖配送管理系统从下单到骑手签收一套 SpringBootVue 全栈骨架怎么搭外卖配送管理系统这个题目很多人第一反应是「不就是个 CRUD 后台吗」。真做过就知道它比普通商城后台多了一层实时状态流转用户下单后订单要在「待接单 → 已接单 → 配送中 → 已送达」之间跳骑手端要能抢单、改状态商家端要能接单出餐管理端要能看全局。这套 SpringBootVue 的外卖配送管理系统源码和数据库核心价值不在页面多漂亮而在于把订单状态机、骑手调度、多角色权限这三件事用一套清晰的后端接口串起来。适合两类人一是想拿一个完整全栈项目练手的学生或转行者二是需要快速搭一套配送类业务骨架的开发者。下面按「先立数据模型、再搭后端、再接前端、最后避坑」的顺序讲透。2. 先把数据库表设计对订单状态机是整套系统的地基外卖配送系统翻车十有八九不是代码写错而是表结构一开始就没想清楚。订单表如果只放一个 status 字段后面加「骑手取消」「商家拒单」「超时未接」这些分支时就会到处打补丁。所以第一步不是写 Controller而是把数据模型和状态流转定死。2.1 核心五张表与字段取舍一套能跑通的外卖配送系统最少需要用户表、商家表、骑手表、订单表、订单明细表。用户、商家、骑手可以合成一张 user 表加 role 字段也可以拆开。我一般建议拆开因为骑手有「在线状态、当前接单数、配送区域」这些独有字段塞进 user 表会越来越臃肿。订单表是整个系统的核心关键字段如下字段名类型说明idbigint主键自增order_novarchar(32)业务订单号唯一索引user_idbigint下单用户merchant_idbigint商家rider_idbigint骑手未接单时为 nullstatustinyint订单状态见状态机total_amountdecimal(10,2)订单总额delivery_addressvarchar(255)配送地址create_timedatetime下单时间accept_timedatetime骑手接单时间finish_timedatetime送达时间status 用 tinyint 而不是字符串是为了索引效率和状态比较方便。常见取值0 待支付、1 待接单、2 已接单、3 配送中、4 已送达、5 已取消。这里有个血泪经验不要用「已支付」直接等于「待接单」支付和接单是两件事中间可能因为商家休息而卡住拆开才能排查。2.2 订单状态流转与数据库约束状态机不能只靠代码里的 if-else 维护数据库层面也要有约束。做法是在订单表加一个 version 字段做乐观锁骑手抢单时用update ... where id? and status1 and rider_id is null这种条件更新影响行数为 0 就说明被别人抢了。这比先查再改可靠得多。-- 骑手抢单条件更新天然防并发 UPDATE orders SET rider_id #{riderId}, status 2, accept_time NOW(), version version 1 WHERE id #{orderId} AND status 1 AND rider_id IS NULL;这条 SQL 的逻辑是只有订单还处于「待接单」且没有骑手时才能抢成功。参数 orderId 是订单主键riderId 是当前登录骑手。执行后判断返回值等于 1 才算抢到等于 0 就提示「手慢了订单已被接走」。很多新手写成先 select 判断再 update压测时必然出现两个骑手同时接到同一单这就是典型的翻车点。订单明细表 order_item 存菜品快照注意要把下单时的菜名和单价冗余存一份不要只存菜品 id。因为商家改价后历史订单金额不能跟着变这是财务对账的底线。3. SpringBoot 后端接口分层、状态流转与骑手抢单的并发处理数据库定好之后后端就是把这套模型翻译成接口。SpringBoot 版本选择上有个热搜词叫「springboot版本太高」确实很多老教程用 2.x新项目直接上 3.x 会遇到 javax 换 jakarta 的包名问题MyBatis 和部分工具类也要跟着升。我一般新项目直接用 SpringBoot 3.x JDK 17老项目维护就锁 2.7.x别混着来。3.1 项目分层与核心依赖配置标准分层是 controller → service → mapper → entity配送系统额外加一个 state 包专门管状态机。pom.xml 里核心依赖就几个spring-boot-starter-web、mybatis-plus、mysql-connector、lombok。用 MyBatis-Plus 是因为它的条件构造器写抢单那种带多条件的 update 很顺手。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencyapplication.yml 里重点配数据库连接和 MyBatis-Plus 的驼峰映射。连接池用默认 HikariCP 就够别一上来就换 Druid除非你要监控 SQL。参数上maximum-pool-size默认 10本地开发够用上线按并发量调到 20~50。3.2 订单状态流转的 Service 写法状态流转统一收口到一个 OrderStateService不要让每个 Controller 自己改 status。这样以后加「超时自动取消」只需要改一处。Service public class OrderStateService { Autowired private OrderMapper orderMapper; // 骑手抢单返回是否成功 public boolean grabOrder(Long orderId, Long riderId) { int rows orderMapper.grabOrder(orderId, riderId); if (rows 0) { throw new BizException(订单已被接走或状态不允许); } return true; } // 骑手送达 public void finishOrder(Long orderId, Long riderId) { Order order orderMapper.selectById(orderId); if (order null || !riderId.equals(order.getRiderId())) { throw new BizException(无权操作该订单); } if (order.getStatus() ! 3) { throw new BizException(订单不在配送中); } order.setStatus(4); order.setFinishTime(new Date()); orderMapper.updateById(order); } }grabOrder 直接调 Mapper 的条件更新把并发控制交给数据库finishOrder 先校验归属和状态再改防止骑手改别人的单。参数 riderId 必须从登录态里取不能从前端传否则就是越权漏洞。这里用 BizException 统一业务异常配合全局异常处理器返回友好提示。3.3 骑手在线状态与接单上限真实配送场景里骑手不能无限接单。常见做法是在骑手表加online_status和current_count两个字段抢单成功后 current_count 加一送达后减一超过阈值比如 5 单就不允许再抢。// 抢单前先校验骑手负载 Rider rider riderMapper.selectById(riderId); if (rider.getOnlineStatus() ! 1) { throw new BizException(请先上线); } if (rider.getCurrentCount() 5) { throw new BizException(当前接单已达上限); }这段校验要放在抢单条件更新之前但注意它和抢单不是原子的高并发下仍可能超限一两单。要严格限制就得把 current_count 也放进条件更新里或者用 Redis 计数器。对练手项目来说前置校验足够但你要知道这个边界在哪。4. Vue 前端多角色路由、订单列表与状态实时刷新后端接口通了前端要解决的是「三种角色看到不同界面」和「订单状态怎么及时更新」。Vue 3 Vue Router Pinia 是当前主流组合Element Plus 做 UI 能省很多事。热搜里「vue路由」「vue安装依赖」是新手最容易卡的地方这里把关键配置讲清楚。4.1 多角色路由与登录态存储路由要按角色做权限控制。登录后把 token 和 role 存进 Pinia路由守卫里判断。用户端、骑手端、商家端、管理端用不同的路由前缀比如 /user、/rider、/merchant、/admin。// router/index.js 路由守卫片段 router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next(/login) return } // 角色不匹配则跳回各自首页 if (to.meta.role to.meta.role ! userStore.role) { next(userStore.homePath) return } next() })逻辑说明requiresAuth 标记需要登录的页面role 标记允许访问的角色。userStore.homePath 根据角色返回不同首页。参数上token 建议存 localStorage 并设置过期时间Pinia 里只做内存缓存刷新页面时从 localStorage 恢复。别把角色判断只写在前端后端每个接口都要再校验一次前端路由只是体验优化。4.2 订单列表与状态轮询骑手端要看到「待接单」列表用户端要看到自己订单的状态变化。最简单可靠的方案是轮询每 5 秒拉一次订单列表。WebSocket 更实时但练手项目里轮询足够而且不容易出连接断开的玄学问题。// 骑手待接单列表定时刷新 const orderList ref([]) let timer null async function fetchOrders() { const res await getPendingOrders() orderList.value res.data } onMounted(() { fetchOrders() timer setInterval(fetchOrders, 5000) }) onUnmounted(() { clearInterval(timer) // 离开页面必须清理否则内存泄漏 })参数说明5000 是轮询间隔毫秒数订单高峰期可以调到 3000但别低于 2000否则后端压力大。onUnmounted 里清理定时器是必须的很多新手忘了这步切页面后定时器还在跑控制台一直报错。抢单按钮点击后调后端接口成功就手动刷新一次列表给用户即时反馈。4.3 前后端联调与跨域处理开发阶段前端跑在 5173 端口后端 8080必然跨域。两种做法后端加 CORS 配置或者前端 vite.config.js 里配 proxy。我一般用 proxy因为生产环境前端打包后放进 SpringBoot 的 static 目录同源就不存在跨域了这也是热搜里「vue打包放进springboot中」的常见做法。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求 /api/order/list 会被代理到后端 8080。打包时执行 npm run build把 dist 目录内容拷到 SpringBoot 的 src/main/resources/static 下重新打包 jar 即可。注意 Vue Router 要用 history 模式的话后端需要配一个 fallback 到 index.html否则刷新页面 404。5. 避坑与排查这套系统最容易翻车的五个地方前面讲的是怎么搭这一章讲怎么不翻车。下面五条都是我在实际项目里踩过或见别人踩过的按「现象 → 原因 → 解决」写。5.1 骑手抢单出现一单多接现象两个骑手同时点抢单都提示成功订单表里 rider_id 被覆盖。原因先查后改没有并发控制。解决用第 2 章那条条件更新 SQLwhere status1 and rider_id is null靠数据库行锁保证只有一个成功。这是最典型也最致命的坑务必一开始就做对。5.2 订单金额和菜品改价后对不上现象商家改了菜品价格历史订单金额跟着变了。原因order_item 只存了菜品 id展示时关联查菜品表。解决下单时把菜名、单价、数量快照进 order_item订单金额以快照为准。数据库设计阶段就要冗余别想着省字段。5.3 SpringBoot 3.x 启动报 javax 找不到现象升级到 SpringBoot 3.x 后编译报package javax.servlet does not exist。原因3.x 把 javax 换成了 jakarta。解决所有 servlet 相关 import 改成 jakarta.servletMyBatis 等依赖也要升到兼容版本。如果项目里第三方库还没适配就老实留在 2.7.x别硬升。5.4 前端打包后刷新页面 404现象开发环境正常打包放进 SpringBoot 后刷新非首页路由报 404。原因Vue Router history 模式下后端没有对应路径。解决后端加一个配置把未匹配的请求转发到 index.html或者前端改用 hash 模式URL 带 #但体验差一些。生产环境推荐前者。5.5 定时轮询导致页面卡顿现象骑手端挂久了越来越卡控制台定时器报错。原因组件销毁时没清理 setInterval。解决onUnmounted 里 clearInterval或者用 VueUse 的 useIntervalFn 自动管理生命周期。这个坑不致命但很烦养成清理副作用的习惯。6. 让这套骨架更接近生产几个我常用的加固技巧练手项目跑通只是起点真要往生产靠还有几处值得加固。第一个是订单超时自动取消。用户下单后 15 分钟没有骑手接单应该自动取消并退款。做法是用 Spring 的 Scheduled 每分钟扫一次超时订单或者用延迟队列。定时任务简单可靠适合中小规模。Scheduled(cron 0 * * * * ?) public void cancelTimeoutOrders() { // 查出创建超过15分钟且状态为待接单的订单 ListOrder list orderMapper.selectTimeoutOrders(15); for (Order o : list) { o.setStatus(5); orderMapper.updateById(o); } }cron 表达式0 * * * * ?表示每分钟执行一次。参数 15 是超时分钟数按业务调。注意这个方法要加分布式锁否则多实例部署时会重复取消。单机练手可以忽略但你要知道边界。第二个是接口幂等。用户重复点提交订单不能生成两笔。做法是前端提交时禁用按钮后端用订单号唯一索引兜底插入冲突就返回已有订单。第三个是骑手位置上报真实系统需要地图热搜里 mapbox vue 就是干这个的但练手项目用文字地址足够别一上来就接地图先把状态流转跑顺。最后一个习惯每加一个状态先画状态流转图再写代码别边写边想。我早期做配送系统时就是没画图结果「已取消」和「已送达」的边界没定清楚测试时出现取消单还能被骑手接的荒唐 bug返工了两天。数据库和状态机是这套系统的后悔药前期多花一小时后期省两天。希望帮到你。本文还有配套的精品资源点击获取
返回列表