简介一套面向计算机毕业设计的外卖配送管理系统采用前后端分离架构前端基于Vue后端基于SpringBoot数据库使用MySQL包含用户管理、优惠券领取、通知提醒、银行卡/微信/支付宝支付、登录与找回密码等模块核心业务涉及订单、商品等实体适合Java方向与前端学习者作为毕设或课程设计参考。资源包共1993个文件压缩包大小37.41MB内含Vue页面组件、Java源码与编译后class文件、XML配置、数据库初始化脚本、第三方依赖包以及JS/CSS样式脚本、常见图片素材和批处理启动脚本目录结构清晰便于对照前后端接口、业务逻辑与数据表设计。目前已有39人学习读者可依据完整源码和数据库快速搭建外卖配送项目在此基础上进行模块扩展或流程调整显著缩短毕业设计从零开发与调试的时间。1. 先看清这份资源一个能跑通“下单-接单-配送”闭环的外卖管理系统外卖配送管理系统不是个新概念但绝大多数网上下到的 SpringBootVue 毕设源码要么缺数据库脚本要么前端页面和接口对不上。这份资源带完整源码和数据库后端 SpringBoot 提供接口前端 Vue 渲染页面中间走 RESTful 接口通信MySQL 存业务数据主流程覆盖用户点餐、商家接单、骑手配送、后台管理四个角色闭环能跑通。适合两类人一类是正在做 Java 毕设的学生需要一套能讲清楚、能答辩的完整项目另一类是刚学完 SpringBoot 和 Vue、想找个真实系统练手的前端或后端开发者。拿到的第一时间我建议先不看代码先把数据库导入、把项目跑起来再沿着一条订单的完整生命周期读代码——从用户提交订单开始到商家接单、骑手取餐、送达完成把链路串起来比从头读源码效率高得多。2. 技术选型拆解为什么是 SpringBoot Vue MySQL而不是别的2.1 后端为什么用 SpringBoot 而不是 SSM 或微服务很多人在选型时纠结SSM 是不是更经典微服务是不是显得更高级对外卖配送这样的中小型系统来说SpringBoot 是性价比最优解。它内嵌 Tomcat不需要单独部署 Web 容器脚手架全家桶把配置、打包、监控都内置好了开发期一个main方法就能起服务。相比 SSM 还要手动配一堆 XMLSpringBoot 的自动配置机制让项目结构简单得多对毕设答辩更友好——你不需要解释为什么配了三层 XML 还找不到 bean。数据访问层通常搭配 MyBatis 或 MyBatis-Plus。一般我建议用 MyBatis-Plus内置BaseMapper单表 CRUD 不用写 SQL只把多表关联和复杂统计写在 XML 里。比如「商家月订单数统计」「骑手配送完成率」这类聚合查询用注解或 XML 里的select写原生 SQL 反而更直观。项目里选择关系型数据库而非 MongoDB 这类 NoSQL原因是订单、用户、菜品之间有强事务性关联——一张订单的生成要同时扣减库存、计算金额、生成配送记录MySQL 的事务保证这三点要么全成功要么全失败。2.2 前端为什么用 Vue Element UI而不是 React 或原生 JavaScript前端选 Vue 的原因很简单Vue 的双向绑定天生适合表单密集型的后台系统。外卖管理后台里大量页面是「表格 弹窗表单」Element UI 的el-table、el-form、el-dialog组件开箱即用你不需要自己写分页器、日期选择器、级联选择器。Vue 2 还是 Vue 3 的选择上这套源码如果基于 Vue 2 Element UI那就不要强行升级 Vue 3——el-table的不少用法在 Vue 3 生态下不兼容升级成本远高于收益。前端工程化上Vue CLI 脚手架提供devServer代理功能解决开发期跨域问题。接口请求统一用 Axios 封装设置baseURL、请求拦截器挂 token、响应拦截器拆解统一返回体。整个系统采用前后端分离前端跑在8080后端跑在8081端口可以自己改前端通过axios调后端 RESTful 接口后端返回Result统一包装结构前端根据code字段判断业务成功或失败。这种模式下后端接口可以被 Postman 单独测试前端页面也可以脱离后端用 mock 数据开发两边并行推进互不阻塞。3. 数据库设计用户、商家、菜品、订单五张核心表怎么建3.1 基础表设计user 表、merchant 表和 dish 表外卖系统的数据库设计核心是拆分角色。第一张表user统一存用户信息通过role字段区分角色类型1普通用户2商家3骑手4管理员。单表多角色的好处是登录鉴权只用一张表查用户配合 Spring Security 或拦截器的角色判断就能区分权限。字段里除了常见的username、password、phone别忘了加status字段——商家和骑手都有审核和封禁状态0正常1禁用查询时要过滤。第二张表merchant是商家详情表一个 user 角色为商家的用户对应一条商家记录通过user_id外键关联。里面存shop_name、address、phone、business_hours、status营业/休息。为什么不在 user 表里直接加这些字段因为普通用户和骑手用不到这些信息拆开后 user 表保持精简商家表的扩展字段不会干扰其他角色的查询逻辑。第三张表dish是菜品表通过merchant_id归属到商家。字段设计要分清「属性字段」和「状态字段」dish_name、price、description、image属于前者status上架/下架、stock库存、category菜品分类属于后者。有个细节price字段建议用decimal(10,2)而不是float浮点类型在金额计算上会有精度误差常见做法是decimal配合BigDecimal在后端运算。菜品表还涉及一个高频查询用户端按照merchant_id和status 1查菜品列表这个查询一定要建联合索引(merchant_id, status)否则商家菜品多的时候会很慢。3.2 订单链路设计order 表、order_item 表和配送记录的拆分逻辑点餐系统的核心链路在订单相关表。这里不能只建一张order表把菜品名直接冗余进去正规的设计必须拆成三张order主表和order_item子表以及配送信息表。order_item存订单里每个菜品项order_id、dish_id、dish_name冗余一份用于快照、price下单时价格快照、quantity。为什么要冗余dish_name和price因为商家可能改名或改价订单里的历史快照不能跟着变这是一个非常典型的「运行时冗余」场景。order表是连接用户、商家、骑手的枢纽字段包括order_no订单号、user_id、merchant_id、rider_id可空未接单前为空、total_amount、status、create_time、pay_time、delivery_time、finish_time。status是状态机核心字段后端的订单状态流转完全靠它驱动0待付款1待接单2待取餐3配送中4已完成5已取消6退款中。状态设计上要注意不要只用整数裸奔建议在 Java 里建订单状态枚举类把状态流转动作方法pay()、accept()、pickUp()封装在枚举里避免 service 层到处散落魔法数字。配送记录单独建delivery表存order_id、rider_id、pickup_time、deliver_time、delivery_fee、status。这样做的目的是区分「订单流程」和「配送流程」订单流程管理支付、退款、取消配送流程管理骑手的接单和送达。两个流程在业务上强关联但在数据状态更新上要解耦——订单状态更新到「配送中」时配送表才生成一条delivery记录这样职责划分明确排查问题的时候看表结构就知道去哪张表查数据。4. 后端实现从 JWT 鉴权到订单状态机核心模块如何落地4.1 JWT 鉴权与拦截器token 如何做到无状态校验整个后台除了登录接口其他接口都需要鉴权。项目采用的方案是 JWTJSON Web Token流程很清晰// JwtUtil.java 核心方法 public class JwtUtil { // 生成 token有效期 2 小时 public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } // 解析 token返回 Claims public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); } }登录接口收到用户名和密码后校验通过则调用generateToken生成 token返回给前端。后续每次请求前端在 Axios 请求拦截器里把 token 放到 Header 的Authorization字段后端写一个拦截器统一校验。// JwtInterceptor.java public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, Integer.parseInt(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { throw new BusinessException(401, token 无效请重新登录); } } }preHandle方法里把解析出的userId和role放进 request 属性Controller 里直接用RequestAttribute取当前用户就不用每个接口都查一遍数据库确认身份了。需要注意一个细节拦截器要排除登录接口和菜品浏览接口——用户没登录也可以进系统浏览菜品下单时才强制登录这个白名单写在 WebMvcConfigurer 里别把所有接口都拦住。4.2 订单状态机SpringBoot 里的枚举 goNext 流转设计订单状态是整个系统最复杂的业务逻辑。之前见过很多代码把状态判断写成嵌套 if-else后面维护的人看着就想骂人。这套源码用的是枚举状态机核心思想是状态的合法流转路径提前定义好非法流转直接抛异常。// OrderStatusEnum.java public enum OrderStatusEnum { WAIT_PAY(0, 待付款) { Override public OrderStatusEnum next(Integer actionType) { if (actionType 1) return WAIT_ACCEPT; // 支付成功 if (actionType 5) return CANCELED; // 用户取消 throw new IllegalStateException(非法操作); } }, WAIT_ACCEPT(1, 待接单) { Override public OrderStatusEnum next(Integer actionType) { if (actionType 2) return WAIT_PICKUP; // 商家接单 throw new IllegalStateException(非法操作); } }, // 其他状态省略 ; // 核心方法根据操作类型返回下一状态 public abstract OrderStatusEnum next(Integer actionType); }Service 层调用时逻辑就非常干净// OrderServiceImpl.java 商家接单 public void acceptOrder(Integer orderId, Integer merchantId) { Order order orderMapper.selectById(orderId); // 校验商家是这单归属商家 if (!order.getMerchantId().equals(merchantId)) { throw new BusinessException(无权操作该订单); } // 状态机流转待接单 接单操作 - 待取餐 OrderStatusEnum current OrderStatusEnum.fromCode(order.getStatus()); OrderStatusEnum next current.next(2); order.setStatus(next.getCode()); orderMapper.updateById(order); }订单状态机的好处有三个一是所有状态迁移路径集中在枚举里代码审查一眼扫过就知道整个订单生命周期应该是怎样的二是非法操作比如待付款直接改为已完成在编译期就能被IllegalStateException拦住不会出现订单状态错乱的数据脏数据答辩时讲这个设计可以直接甩出 「枚举 state pattern」两个名词评委能听出设计感。4.3 配送调度与订单列表查询优化两个容易被忽略的高频接口配送调度的逻辑在「分配骑手」这个动作上。系统用的是最简单的抢单模式新订单状态变为待接单时系统把订单推送给附近骑手骑手终端轮询或者浏览器定时刷新接口看到可以接的订单调用接单接口绑定rider_id。这里的技术点是「附近骑手」怎么计算——如果数据量不大简单方案是直接把所有状态为「空闲」的骑手查询出来在 Java 层用经纬度计算距离Haversine 公式筛选 5 公里内的骑手。数据量大了再考虑 Geohash或引入 Redis GEO 做骑手位置存储毕设阶段用第一种方案足够。订单列表查询是另一个常见性能瓶颈。用户端「我的订单」列表要关联order_item查菜品如果直接 for 循环查询 N 个订单、每个订单查一次子表接口会慢得不能看。正确做法是一次性把订单主表查出来收集 orderId 集合用IN查询拿到所有子表记录在 Java 内存里按 orderId 分组再组装进 VO 里返回前端把两次 SQL 完成的工作量压缩到一个网络往返内接口耗时从 300ms 降到 50ms 级别。5. 避坑与排查指南这套系统最容易踩的四个坑5.1 账号登录后接口全部 401前端页面进不去现象输入正确的账号密码登录接口返回 token 成功跳转首页后后续所有请求都是 401 未授权。原因绝大多数情况是前端 Axios 没有在请求头携带 token。有些改过的代码把 token 存在 localStorage但 Axios 拦截器里没有读取并设置Authorization另一种情况是后端拦截器放行规则写错把/**全拦了登录接口也被拦住。解决打开浏览器开发者工具 Network 面板点击一个接口请求查看 Request Headers 里有没有Authorization字段。没有就检查前端request.js里的拦截器代码有就检查后端WebMvcConfigurer里的excludePathPatterns确认/user/login、/user/register等接口已被放行。排查思路是前后端各看一遍请求头定位在哪一边断的。5.2 订单表里 create_time 时间差了 8 小时现象订单创建时间比实际时间早了或者晚了 8 个小时数据库里的时间看着像 UTC 时间但操作系统时间是北京时间。原因MySQL 连接串里没有加时区参数。默认驱动行为会把数据库的TIMESTAMP类型按 JVM 默认时区解析如果你的 MySQL 是 UTC、应用是 Asia/Shanghai时间就会偏 8 小时。解决在数据库连接串jdbc:mysql://localhost:3306/db_name后面补上?serverTimezoneAsia/ShanghaiuseSSLfalsecharacterEncodingutf8重建连接后刷新页面时间就对了。另一个坑点是order表字段类型选择建议用DATETIME加上后端LocalDateTime避免TIMESTAMP的 2038 年问题。5.3 菜品图片上传成功但页面上不显示现象后端有上传接口上传成功后返回了图片路径/upload/xxx.jpg但前端 img 标签拿这个路径访问是 404。原因图片保存到了本地磁盘目录但 SpringBoot 默认不会把磁盘路径映射为可访问的静态资源。前端在8080端口后端在8081端口即使后端能访问这个路径前端跨域访问也会被拦截。解决在后端写一个静态资源映射配置类把本地的上传目录映射到 URL 路径Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }同时前端要把图片的完整地址拼成http://localhost:8081/upload/xxx.jpg不要直接拿相对路径用。更省事的方向是换用 MinIO 或阿里云 OSS但毕设阶段本地磁盘方案够用注意两个端口和路径映射别漏。5.4 MyBatis-Plus 分页查询失效返回全量数据现象调用分页接口时传了pageNum1pageSize10但返回结果里有 50 条数据total也不对。原因MyBatis-Plus 的分页插件没有注册。很多人只引入了mybatis-plus-boot-starter依赖但没加MybatisPlusInterceptor配置类导致Page对象传进去后没有任何拦截器处理SQL 里没有 LIMIT 语句自然全量返回。解决加一个配置类注册分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个注意点很典型——依赖有了但 Bean 没装配。排查方法也简单打开 MyBatis 的 SQL 日志输出看执行的 SQL 里有没有LIMIT ?没有就说明插件没生效。6. 部署联调与验证从本地启动到跑通一条完整订单到这一步前端后端代码都拆清楚了剩下的就是把环境真正跑起来。先确认本机环境JDK 8 或 11看 pom.xml 里java.versionNode.js 14 以上MySQL 5.7 或 8.0。数据库导入脚本一般在 sql 目录下用 Navicat 或者命令行source导入注意字符集选 utf8mb4不然中文菜品名会乱码。启动顺序固定先启动 MySQL再启动后端最后启动前端。后端启动前要改application.yml里的数据库账号密码和你本机一致。如果用 IDEA 打开后端工程Maven 依赖会自动下载等右下角进度条走完再启动。前端在目录下执行npm install如果下载报错换成淘宝镜像源npm config set registry https://registry.npmmirror.com再npm run serve。启动成功后沿着这条链路测试一遍注册一个普通用户账号 → 浏览商家和菜品 → 添加购物车 → 提交订单 → 支付测试代码里大概率是模拟支付直接调支付接口→ 用商家账号登录后台接单 → 用骑手账号接单配送 → 用户端确认收货订单状态变为已完成。日志就是最好的老师前端看浏览器 Network后端看 IDEA 控制台——接口返回 500优先看后端堆栈第一行报错信息90% 的情况都能直接定位。最后有一个我养成很久的习惯凡是拿到新项目第一步不是看代码而是拉一份接口清单用 Postman 把所有 GET 请求先打一遍再逐个测 POST 请求。接口全通了说明数据库没问题、框架没问题剩下的就是业务逻辑阅读。从那以后我每次拿源码包都是先还原环境再读代码事实证明这样走一遍后踩坑率低了很多。希望这次实战拆解能帮你把这套外卖系统真正跑起来也让你在跑链路的过程中对 SpringBoot Vue 的配合方式有更具体的体感。本文还有配套的精品资源点击获取