简介:这是一套面向计算机专业毕业设计场景的外卖配送管理系统完整源码包,采用SpringBoot后端与Vue前端分离架构,数据库使用MySQL,适合正在准备毕设或课程设计的学生参考与二次开发。系统覆盖用户管理、优惠券领取、通知提醒、多支付方式(银行卡、微信、支付宝)以及账号登录与密码找回等核心模块,功能链路较为完整。压缩包共1993个文件,约37.41MB,包含388个vue组件、284个js脚本、206个css样式、131个java后端类以及1个sql数据库脚本,另有大量png、jpg图片与xml配置,前端页面、后端逻辑与数据脚本一应俱全。目前已有39人学习下载。对于需要快速搭建可运行项目、理解前后端交互流程或撰写论文系统实现章节的读者,该资源提供了清晰的目录结构与可参考的模块划分,便于对照调试与功能扩展。
1. 外卖配送管理系统:从下单到骑手签收,一套 SpringBoot+Vue 全栈骨架怎么搭
外卖配送管理系统这个题目,很多人第一反应是「不就是个 CRUD 后台吗」。真做过就知道,它比普通商城后台多了一层实时状态流转:用户下单后,订单要在「待接单 → 已接单 → 配送中 → 已送达」之间跳,骑手端要能抢单、改状态,商家端要能接单出餐,管理端要能看全局。这套 SpringBoot+Vue 的外卖配送管理系统源码和数据库,核心价值不在页面多漂亮,而在于把订单状态机、骑手调度、多角色权限这三件事用一套清晰的后端接口串起来。适合两类人:一是想拿一个完整全栈项目练手的学生或转行者,二是需要快速搭一套配送类业务骨架的开发者。下面按「先立数据模型、再搭后端、再接前端、最后避坑」的顺序讲透。
2. 先把数据库表设计对:订单状态机是整套系统的地基
外卖配送系统翻车,十有八九不是代码写错,而是表结构一开始就没想清楚。订单表如果只放一个 status 字段,后面加「骑手取消」「商家拒单」「超时未接」这些分支时就会到处打补丁。所以第一步不是写 Controller,而是把数据模型和状态流转定死。
2.1 核心五张表与字段取舍
一套能跑通的外卖配送系统,最少需要用户表、商家表、骑手表、订单表、订单明细表。用户、商家、骑手可以合成一张 user 表加 role 字段,也可以拆开。我一般建议拆开,因为骑手有「在线状态、当前接单数、配送区域」这些独有字段,塞进 user 表会越来越臃肿。
订单表是整个系统的核心,关键字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| order_no | varchar(32) | 业务订单号,唯一索引 |
| user_id | bigint | 下单用户 |
| merchant_id | bigint | 商家 |
| rider_id | bigint | 骑手,未接单时为 null |
| status | tinyint | 订单状态,见状态机 |
| total_amount | decimal(10,2) | 订单总额 |
| delivery_address | varchar(255) | 配送地址 |
| create_time | datetime | 下单时间 |
| accept_time | datetime | 骑手接单时间 |
| finish_time | datetime | 送达时间 |
status 用 tinyint 而不是字符串,是为了索引效率和状态比较方便。常见取值:0 待支付、1 待接单、2 已接单、3 配送中、4 已送达、5 已取消。这里有个血泪经验:不要用「已支付」直接等于「待接单」,支付和接单是两件事,中间可能因为商家休息而卡住,拆开才能排查。
2.2 订单状态流转与数据库约束
状态机不能只靠代码里的 if-else 维护,数据库层面也要有约束。做法是在订单表加一个 version 字段做乐观锁,骑手抢单时用update ... where id=? and status=1 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> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>application.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 章那条条件更新 SQL,where status=1 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.servlet,MyBatis 等依赖也要升到兼容版本。如果项目里第三方库还没适配,就老实留在 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分钟且状态为待接单的订单 List<Order> list = orderMapper.selectTimeoutOrders(15); for (Order o : list) { o.setStatus(5); orderMapper.updateById(o); } }cron 表达式0 * * * * ?表示每分钟执行一次。参数 15 是超时分钟数,按业务调。注意这个方法要加分布式锁,否则多实例部署时会重复取消。单机练手可以忽略,但你要知道边界。
第二个是接口幂等。用户重复点提交订单,不能生成两笔。做法是前端提交时禁用按钮,后端用订单号唯一索引兜底,插入冲突就返回已有订单。第三个是骑手位置上报,真实系统需要地图,热搜里 mapbox vue 就是干这个的,但练手项目用文字地址足够,别一上来就接地图,先把状态流转跑顺。
最后一个习惯:每加一个状态,先画状态流转图再写代码,别边写边想。我早期做配送系统时就是没画图,结果「已取消」和「已送达」的边界没定清楚,测试时出现取消单还能被骑手接的荒唐 bug,返工了两天。数据库和状态机是这套系统的后悔药,前期多花一小时,后期省两天。希望帮到你。
本文还有配套的精品资源,点击获取