资讯动态

SpringBoot外卖系统毕业设计:从状态机到订单闭环的完整实现

发布时间:2026/10/8 3:50:01 来源:尧图企业网站定制
1. 项目概述与选题思路计算机毕业设计选SpringBoot外卖系统几乎是每个做Java方向的学生都躲不过的经典题目。这个题既能体现前后端基本功又能带出数据库设计、缓存、定时任务、接口安全这些知识点答辩的时候可讲的东西非常多。我当年做毕业设计选了“基于SpringBoot的智能化餐饮外卖订购系统”实际开发下来发现真正难的不是CRUD而是订单状态怎么流转、超时单怎么处理、库存怎么防止超卖、前后端怎么顺畅联调。这篇就把我完整做这套网上点餐配送管理平台的过程以及踩过的坑全部梳理出来。先明确一下这个题目到底要做什么。一套完整的外卖系统按角色拆开来看是这样用户端负责注册登录、浏览菜品、加购物车、下单支付、追踪订单配送状态商家端负责菜品上下架、修改价格库存、接单出餐骑手端负责查看待配送订单、抢单/接单、更新配送进度管理后台则处理用户/商家/骑手的审核与整体数据统计。很多同学把这个题目做小了只做了用户点餐和商家后台缺少骑手配送这条线答辩时很容易被老师追问“配送状态谁来维护”。所以我在设计初期就把配送环节纳入核心流程这也是“智能化”三个字落地的关键点之一。1.1 为什么选外卖系统作为毕设课题选这个题有个很现实的好处业务链路长但每个环节都不过于复杂。从下单到支付、接单、配送、完成每一个状态切换都有明确的业务含义老师一看就觉得这是个“完整系统”。同时它足够贴近生活哪怕老师不是搞软件的也能理解外卖是怎么运作的沟通成本低。从技术训练的角度看外卖系统几乎能把Java后端主流技术都串起来。SpringBoot框架做基础骨架MyBatis-Plus操作数据库Redis做缓存和分布式锁定时任务处理超时未支付订单JWT做登录鉴权再配合Vue写前端页面。这套组合覆盖了校招面试和毕业设计的高频考点做完之后你对SpringBoot的自动装配原理、事务传播机制、缓存一致性这些概念也会有更落地的理解。1.2 项目最终要做成什么样我给自己定的目标是做一个“能跑通全流程”的闭环系统而不是一堆页面堆在一起。最终交付的东西包括一个基于SpringBoot的后端工程、一个Vue3前端工程、MySQL数据库脚本、项目部署文档和答辩PPT。演示的时候从用户注册开始下单、模拟支付、商家接单、骑手配送、用户确认收货一条完整链路走完再展示后台的销售数据统计页面整套演示大概五分钟就能讲清楚。这里我给后来者的建议是先用自己的话把整个流程画出来。画一张泳道图把用户、商家、骑手、系统四个参与者之间的交互画清楚后面写代码、建表、写文档都会轻松很多。不要在需求还没理清的时候就去建SpringBoot工程那只会越写越乱。2. 技术选型与整体架构技术选型这块我的核心原则是“能用稳定版本绝不用最新版本能用常见组合绝不搞花活”。毕业设计的核心是展示你对技术原理的理解而不是追新版本。下面是我最终定的方案。2.1 SpringBoot版本选择的坑SpringBoot版本这个坑很多同学都踩过尤其是“springboot版本太高”导致的连环问题。我刚开始为了图新鲜创建工程时选了SpringBoot 3.x结果发现它强制要求JDK 17及以上而且原来的javax包全部改名成jakarta网上大量教程里的老代码直接报错Lombok版本、MyBatis-Plus版本都要跟着升级。折腾了一晚上最后还是老老实实换回SpringBoot 2.7.x JDK 8的组合。如果你的电脑已经装了JDK 8项目就直接用SpringBoot 2.7.x这是目前兼容性最好的稳定组合。如果学校机房是JDK 17那你可以用SpringBoot 3.x但要注意把MyBatis-Plus、Lombok、Swagger这些依赖版本对齐。我的建议是优先参考SpringBoot官网的版本说明不要只看某篇博客说“最新的最好”。毕业设计求稳跑起来比什么都强。2.2 单体还是前后端分离外卖系统这种规模没必要微服务单体应用完全够用。但我依然推荐把代码分层做清楚Controller层只管接收参数和返回结果Service层写业务逻辑Mapper层操作数据库再单独建一个config包管理配置。这样的好处是答辩的时候你可以很自然地说“我使用了分层架构实现了代码解耦”。前端我选了Vue3 Element Plus通过npm run build把打包后的dist目录放进SpringBoot的src/main/resources/static里。也就是热搜词里说的“vue打包放进springboot中”。这样做之后整个系统只有一个端口部署时直接java -jar运行jar包就能访问省去配置Nginx的麻烦。不过这么做有个前提前端路由要用hash模式如果用history模式刷新页面会404需要额外写一个路由回退Controller这个细节我后面专门讲。2.3 数据库与缓存设计数据库用的MySQL 8.0持久层框架选MyBatis-Plus。选择MyBatis-Plus主要是看中它的代码生成器和条件构造器写CRUD效率特别高能把时间留到核心业务逻辑上。缓存方面引入Redis主要用于三块存登录token、存验证码、做库存预扣和防重复提交的分布式锁。这里特别说一下SpringBoot的自动装配原理。每次启动项目SpringBoot都会根据starter依赖自动配置对应的Bean比如MyBatis-Plus、RedisTemplate都是自动装配进来的。理解这一点你会明白为什么只要引入了依赖、配好了application.yml就能直接用这些组件。如果在运行时发现某个Bean没有初始化排查方向就是它的自动配置类是否满足条件比如Redis没启动、配置项缺失、类路径缺少某个类都会导致自动装配不生效。这个原理不需要死记硬背面试或答辩时用自己的话讲一遍老师就觉得你不是只会用框架。3. 数据库建模与核心表设计数据库设计是这个项目的灵魂。我见过很多同学外卖系统做完代码能力没问题但数据库表设计一塌糊涂字段类型混乱、没有索引、父子关系指向错误。表格设计得好业务实现自然顺畅设计得差后面全是补丁代码。3.1 用户、商家、骑手三端角色怎么建模我的做法是建了user、merchant、rider三张表分别管理没有把三类角色塞进一张统一的user表。这样虽然表多了一些但每个角色的字段差异很大分开更清晰后面扩展也方便。user表核心字段包括id、openid如果接微信登录、nickname、phone、password、avatar、status。merchant表在基本信息之外还要有store_name、store_address、latitude、longitude因为配送派单要基于距离计算经纬度必须存。rider表核心字段是id、name、phone、status在线/休息/配送中、current_latitude、current_longitude。三个角色各自登录用的是同一套登录接口只是登录成功后返回的角色类型不同。这里有个细节不要用普通字段装饰数据库里的大文本。比如商品图片地址存的就是一个相对路径字符串不要存base64否则数据库会迅速膨胀。头像和菜品图全部走文件上传服务器磁盘保存文件数据库存URL路径。3.2 订单与配送状态机设计订单表是整个系统的中心。字段设计时一定要把状态、取消原因、退款状态这类业务字段想全。我的orders表关键字段如下。字段名类型说明idbigint主键order_novarchar(32)订单号唯一user_idbigint下单用户merchant_idbigint所属商家rider_idbigint接单骑手初始为空statustinyint订单状态total_amountdecimal(10,2)订单总金额pay_amountdecimal(10,2)实付金额address_idbigint配送地址快照remarkvarchar(255)用户备注pay_timedatetime支付时间delivery_timedatetime送达时间create_timedatetime创建时间订单状态我用数字来表示实际代码中定义一个枚举类0待支付、1已支付待接单、2已接单配送中、3已完成、4已取消、5退款中、6已退款。每一次状态变更都只允许从当前状态跳转到规定的下一个状态不能用“随便改status字段”的粗暴方式否则会出现已取消的订单还在配送这种逻辑灾难。3.3 购物车、菜品、地址等辅助表细节菜品表dish要存store_id、category_id、name、price、image、stock、status其中status用来做上下架status为0时前端不展示库存扣减也要绕过已下架的菜品。购物车表cart字段有user_id、dish_id、quantity不需要单个商品金额因为最后的金额以下单时的数据库价格为准。地址表user_address要存id、user_id、receiver_name、receiver_phone、province、city、district、detail_address、is_default。下单时不能直接引用地址表必须把收货人姓名、电话、详细地址复制到orders表的几个冗余字段里这是为了避免用户后来修改地址导致历史订单信息不准。订单明细表order_item也非常重要它的字段包括order_id、dish_id、dish_name、dish_image、price、quantity。dish_name、dish_image、price三列是从菜品表冗余过来的快照字段。为什么要冗余因为商家改菜价、改图片甚至删除菜品之后你仍然希望历史订单能看到当时的商品信息。直接用外键关联dish表的话一删菜全乱套所以快照字段是必须的。4. 核心功能实现与难点拆解架构和表设计做完后进入真正的编码阶段。这一节我挑几个最核心、最容易被老师追问的业务实现来拆解。4.1 点餐下单的事务与锁下单流程大概是用户前端点击去结算后端校验库存、扣减库存、生成订单、生成明细、清空购物车。这里最怕的是两个问题一是库存扣成负数二是用户重复点击按钮产生两条一模一样的订单。解决超卖最简单的办法是数据库层面的原子操作。MyBatis-Plus里写一个update方法UPDATE dish SET stock stock - 1 WHERE id ? AND stock 0返回值是受影响行数如果返回0说明库存不足。这种方式比先select再update安全得多不用额外加悲观锁。在Redis里做前置库存预扣也行但毕设项目直接走数据库扣减已经够了。防重复提交我用了两种手段。前端在点击下单后立刻置灰按钮同时后端用Redis的SETNX实现简单幂等下单前先以“order_lock:用户ID”为key尝试设置值设置成功才继续下单下单完成后删除锁。这样就算前端按钮被绕过后端也能挡住重复请求。核心思路和分布式锁类似但实现成本低很多适合单体应用。Override Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { String lockKey order_lock: dto.getUserId(); Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { throw new BizException(订单处理中请勿重复提交); } try { // 1. 校验地址、购物车、商家 // 2. 遍历购物车逐件扣减库存 // 3. 生成订单号保存orders // 4. 保存order_item明细 // 5. 清空购物车 return orderId; } finally { redisTemplate.delete(lockKey); } }事务注解一定要注意rollbackForException.class。默认情况下Spring声明式事务只回滚RuntimeException如果你在事务里抛了一个自定义的普通Exception它不会回滚数据就乱了。我建议所有Service层方法都统一用rollbackFor{Exception.class}。4.2 订单状态流转与定时任务订单状态流转的核心不是直接改status而是用“条件更新”来确保状态合法。比如用户取消待支付订单SQL就是UPDATE orders SET status4 WHERE id? AND status0受影响行数为1才代表取消成功。如果订单已经支付过了这个更新就不生效从根上杜绝了错误状态覆盖。超时未支付订单是外卖系统躲不开的需求。用户下单后如果不支付不能让它一直占着库存。我在项目里用了SpringBoot定时任务来处理EnableScheduling开启Scheduled(cron 0 */1 * * * ?)每分钟执行一次扫描把创建时间超过15分钟且状态为0的订单自动取消同时回补库存。Component public class OrderTimeoutTask { Scheduled(cron 0 */1 * * * ?) public void cancelExpiredOrders() { ListOrder expiredOrders orderMapper.selectExpiredOrders(15); for (Order order : expiredOrders) { int updateCount orderMapper.cancelOrder(order.getId()); if (updateCount 0) { // 回补库存 restoreStock(order.getId()); } } } }这个方案在单机部署下完全没问题但如果将来系统部署了多个节点定时任务会在每个节点都执行一遍就会重复处理订单。简单的解法是使用Redis实现任务锁抢到锁的节点才执行。毕设答辩时能主动说出这个隐患和方案是很加分的点。4.3 配送派单逻辑派单逻辑我实现得比较朴素但效果稳定。当商家点击接单后系统从rider表里筛选出status为1在线的骑手然后计算骑手当前位置到商家的距离取距离最近的骑手发送配送推送。距离计算用经纬度Haversine公式单条记录级别的计算量非常小不需要引入地图SDK。骑手接单后在骑手端点击“取餐”订单状态从2变3再点击“送达”状态变3变4用户端同步看到配送进度。如果骑手长时间不接单系统会自动把订单重新派给下一位在线骑手这个机制我用了一个延迟任务商家接单5分钟后如果订单还没有骑手接就把orderId重新加入派单队列。这块如果想再“智能”一点可以做成“骑手自主抢单”也就是把所有待配送订单按距离倒序展示给骑手骑手手动接单。我最终选择了系统派单因为流程演示更直观用户下单、商家接单、系统派单、骑手接单、配送中、完成整个链路一气呵成。4.4 支付回调与订单一致性真实接入微信支付或支付宝支付需要商户号、证书等资质毕设阶段通常办不下来。我采用的是模拟支付方案用户点击去支付后端直接调用一个payService.mockPay(orderNo)方法模拟支付回调成功后更新订单状态。但即使是模拟支付我也把回调接口的幂等性做得很严格。支付回调里不能写“只要收到回调就更新status”因为网络超时会导致回调重发。我每次处理回调都执行条件更新UPDATE orders SET status1, pay_timeNOW() WHERE order_no? AND status0。如果更新成功说明这次回调是有效且第一次的如果更新失败说明订单已经被处理过直接忽略。支付环节还有个细节订单金额的计算必须全部在后端完成。前端传过来的totalAmount不能信用户改动了前端金额参数再下单极有可能出现支付0.01元的订单。我在createOrder时从数据库查询菜品价格、计算总价前端传的金额只用来做展示不参与任何后端计算。5. 前端页面与联调要点后端写得再漂亮前端页面丑也是硬伤。外卖系统的前端我是用Vue3 Element Plus做的没有花太多时间在花哨动画上重点是信息展示清晰、操作路径顺畅。5.1 vue打包放进springboot中“vue打包放进springboot中”这个热搜词我猜是很多同学卡在实际部署环节。说穿了其实很简单前端工程里执行npm run build生成dist目录然后把dist里的内容整体复制到SpringBoot的src/main/resources/static目录重新打jar包访问localhost:8080就能直接看到页面。但有两个容易翻车的点。第一前端的接口请求路径如果在开发环境写的是localhost:8080/api打包后也要保持这个相对路径不要直接把开发环境的绝对地址写死进去。我在Vue工程里配置了环境变量开发环境用Vite的proxy代理转发/api到后端生产环境直接访问同源地址/api。第二如果用了vue-router的history模式刷新一个子路由页面会404因为SpringBoot找不到对应的Controller。解决办法有两个改router使用hash模式或者写一个转发方法把非静态资源的请求转发到index.html。我选择直接用hash模式因为实现成本最低对毕设展示没有任何影响。5.2 接口设计与联调经验接口设计我采用的是RESTful风格统一返回Result 结构code、message、data。后端写了一个GlobalExceptionHandler用RestControllerAdvice统一捕获业务异常和参数校验异常任何位置抛出BizException都会返回统一格式的错误信息。这样前端处理错误时只需要判断code是否为200非常省事。登录鉴权用的是JWT用户登录成功后会返回token和角色信息前端把token存在localStorage里每次请求在请求拦截器中带上Authorization头。后端写了一个拦截器配置在自定义拦截器中排除登录接口其余接口统一校验token。这里要注意SpringBoot默认只拦截controller请求不会拦截静态资源但是Vue打包后的静态资源在resources/static里走SpringBoot静态资源处理器不经过拦截器所以不会出现token失效导致页面加载不了的情况。联调过程中我最推荐的是Knife4j也就是增强版Swagger文档。SpringBoot集成Knife4j之后接口文档自动生成前端可以直接在页面上测试接口。省去了手工维护接口文档的功夫答辩演示时也能打开文档页面给老师看显得项目很规范。6. 常见问题与排查实录这个部分是我把开发过程中踩过的坑和网上同学问得比较多的问题合并整理出来的基本都是血泪教训。问题现象解决方案SpringBoot版本太高编译报错javax不存在换成SpringBoot 2.7.x配JDK8或统一升级到JDK17接口返回中文乱码页面显示问号在WebMvcConfig中配置StringHttpMessageConverter的UTF-8编码文件上传后图片404重启jar图片就没了把上传文件保存到jar包外的磁盘目录不要放resources订单表数据量变大查询慢后台列表秒开变秒卡给order_no、user_id、merchant_id、status建联合索引定时任务重复执行订单被重复取消增加Redis任务锁只有抢到锁的节点执行Vue打包后刷新404子页面刷新白屏vue-router使用hash模式或编写fallback转发数据库连接失败启动报Connection refused检查MySQL版本、端口、时区配置加connectTimeout参数6.1 SpringBoot版本太高的问题再展开前面提过版本问题这里再展开说一个细节如果你非要用SpringBoot 3.x最麻烦的不只是JDK版本而是第三方starter的兼容性。MyBatis-Plus当时适配SpringBoot 3的版本号是3.5.3Lombok得换新版Knife4j也要用OpenAPI3版本的依赖。如果某个依赖官网明确写着不支持SpringBoot 3你就不要硬凑。我见过有同学把SpringBoot降级到2.7后一堆依赖冲突不治而愈。顺带说一句不管用什么版本启动类上标着的SpringBootApplication注解下面都是由EnableAutoConfiguration这个元注解触发的自动装配。所谓“xxx不管用”八成不是这个注解失效了而是某个自动配置类的条件不满足。6.2 高并发下单导致库存超卖我的系统峰值量不大但答辩时老师非常喜欢问“如果多个人同时下单买最后一份菜品怎么办”。我当时的回答分了三层第一层数据库扣减时使用UPDATE ... WHERE stock 0保证不会扣成负数第二层在Redis里维护一个菜品库存key下单提前用DECR命令做预扣库存不足直接返回第三层如果真的要做极高并发再用消息队列串行化创建订单请求。虽然毕设没真正上消息队列但这个回答本身就展示了思路的深度。6.3 前后端联调时跨域问题本地开发时前端跑在8080后端跑在5173默认存在跨域。我用的是Vite代理配置一个/dev请求转发到后端地址这样浏览器看起来是相对路径。如果你是前后端单独部署就需要后端开启CORS跨域配置。我建议能走代理尽量走代理因为明面上配置CORS有时候还会因为预检请求、自定义Header等原因出各种幺蛾子。代理方式对毕设演示最友好前后端打包到一起后跨域问题自然不存在。6.4 金额计算用Float还是BigDecimal这个坑我必须要强调凡是涉及金额的字段数据库decimal(10,2)Java实体用BigDecimal绝对不能使用float或double。float和double在计算机中是二进制浮点数0.1 0.2并不等于0.3累计下来订单总金额就可能出现0.07这种奇葩小数。用BigDecimal进行金额计算时注意要使用scale设置小数位并使用half-up舍入模式。7. 项目亮点包装与答辩准备毕设做完到答辩之间其实还有很关键的一步把项目里“普通”的部分讲出价值。7.1 如何把普通外卖系统讲出亮点很多同学觉得自己做的系统不就是网上随处可见的CRUD吗但同样的功能不同的组织和表达讲解效果完全不同。我给自己总结了几条主线第一条主线是订单状态机。老师问“你项目最复杂的地方在哪”你就说整个订单从支付、接单、取餐、配送到完成存在完整的状态机约束每一次状态变更都是条件更新从机制上避免非法流转。第二条主线是超时关单与库存回补。系统有定时任务自动取消超过15分钟未支付的订单并且回补库存保证菜品库存实时准确。第三条主线是配送调度。系统根据骑手位置与商家距离自动匹配最近骑手并且有超时转派机制不是写死订单给某个骑手。如果你还想拔高一点可以聊聊热点词“springboot整合flink”的思路。也就是引入流式计算对实时订单数据进行统计和分析比如每分钟各店铺销售额、热门菜品排行榜。注意这里讲思路就够不需要真的在毕设里集成Flink除非你有把握把它跑通。答辩时抛出一句“我了解过SpringBoot整合Flink的实时计算方案后续可以从这个方向扩展”已经能让老师知道你具备技术视野。7.2 答辩前必做的几个准备动作第一把核心表的ER图画出来能够边说表结构边讲业务。第二准备好几个接口的调用链比如下单调用链、支付回调调用链、配送状态更新调用链每一个链路能画出来。第三准备一台能跑起来的电脑数据库、Redis、后端、前端全部一键启动不要等到答辩现场才手忙脚乱地配环境。第四多问自己几个“为什么”——为什么用Redis缓存、为什么订单状态用条件更新、为什么金额用BigDecimal、为什么用hash模式路由。这些“为什么”比功能本身更能证明你是真的做了项目。7.3 我做这个项目的一些实在感受回头再看这个SpringBoot外卖系统我最大的体会是毕业设计考察的不是项目有多炫而是你有没有把最基础的技术点踩扎实。你不需要真的接支付、不需要搞微服务、不需要上Kafka只要把用户端、商家端、骑手端、后台这条线做成完整闭环把一个订单从创建到完成的全生命周期管理清晰就已经超过大部分同学了。最后分享一个小技巧写订单号的时候不要只写时间戳加自增数字我记得自己加了一个用户ID后四位加随机字符串再加时间戳既保证唯一又方便定位问题。类似的细节还有很多比如在Redis里给token设置过期时间、把菜品状态和库存放在一个接口里更新、把定时任务的时间可配置化而不是写死在注解里。这些细节单个看都很小组合起来就是一个老师挑不出明显毛病、自己维护起来也不痛苦的项目。做毕设的过程就像修房子骨架搭稳了水电通了表面再刷点漆整个作品就能立得住。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑