SpringBoot+Vue3校园外卖系统全栈实战:从数据库设计到前后端分离部署

发布时间:2026/10/23 8:10:01
SpringBoot+Vue3校园外卖系统全栈实战:从数据库设计到前后端分离部署 刚整理完一套校园外卖服务系统的完整源码跑通了用户点餐、商家接单、骑手配送、管理员管控四个角色的全流程。技术栈是当前Java后端和前端工程化里非常主流的组合SpringBoot Vue3 MyBatis MySQL前后端完全分离。这个项目不是那种只写几个CRUD接口的玩具Demo而是把校园场景里从用户下单到订单送达宿舍楼下的完整业务链路都落了地。如果你正在找一套能二次开发、业务逻辑完整的Java全栈项目或者准备用它来做课程设计、毕业设计的底子这篇文章会把整体架构、数据库建模、后端关键接口、Vue3前端实现思路以及联调部署时踩过的坑一次性讲透。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot Vue3 MyBatis MySQL先说结论这套组合不是最新潮的但一定是校园类业务系统里最稳、最容易招人接手、也最容易在答辩时讲清楚的方案。后端选SpringBoot核心原因有三个。第一是生态成熟SpringBoot 2.x对JWT、Redis、MyBatis、文件上传、参数校验这些常规操作的集成基本是零配置起步开箱即用。第二是内置Tomcat一个java -jar就能把服务跑起来部署成本低对于校园外卖这种单体应用规模的系统完全够用。第三是社区资料极多学生开发者在网上能搜到几乎所有问题的现成答案后期维护不会卡壳。前端选Vue3是因为Composition API在组件复用和业务逻辑抽离上的表现比Vue2的Options API好太多。外卖业务里有大量“商品列表选规格加入购物车”“订单状态倒计时”这类需要共享状态和响应式更新的场景用Vue3的Setup语法配合Pinia代码组织起来非常清晰。加上Vite做开发服务器热更新速度快调试体验比Webpack时代的Vue工程上一个台阶。MyBatis这个选择很多人问我为什么不用MyBatis-Plus。说实话在这套系统里我刻意保留了原生MyBatis。校园外卖的订单查询、报表统计、商家销量排行这类SQL本身就不复杂写成XML里的自定义SQL反而更直白方便学习SQL优化。MyBatis-Plus对单表CRUD的提升很大但真到多表联查和复杂聚合时你还是得写XML不如一开始就习惯这种半ORM的写法。数据库用MySQL免费、通用、校园服务器环境几乎都能装主键用自增ID行锁和事务能力应付外卖场景绰绰有余。1.2 前后端分离的架构优势这套源码采用了标准的Restful前后端分离结构。后端只暴露/api/**形式的JSON接口前端通过Axios统一请求。这样做最大的好处是开发职责解耦两个人同步开发时后端只需要把接口文档定义清楚前端拿着Mock数据就开工不用等对方环境跑通。部署上也是真分离前端Vue项目打包成静态文件后由Nginx托管后端SpringBoot跑在独立端口。这样的好处是静态资源和动态接口互不干扰前端打新版本时不需要重启后端后端发版时前端页面也不用重新构建。对于校园外卖这种会频繁调整前端UI的需求这个优势非常实际。1.3 业务流程与领域模型划分我设计这套系统时先画的不是表结构而是业务流程图。核心链路非常清晰用户在Web端浏览入驻商家把菜品加入购物车确认订单并选择配送地址通常是宿舍楼栋和楼层完成“下单”后商家端收到新订单提醒商家接单后开始出餐餐品做好后商家点击“通知配送”骑手完成取餐并配送到宿舍楼下用户最终确认收货。订单状态沿着“待支付 → 待接单 → 待取餐 → 配送中 → 已完成”这条线推进中间还有“已取消”“退款中”两个异常分支。围绕这条主链路系统拆分出四个领域角色用户端负责浏览、下单、支付、评价商家端负责商品管理、订单接单、出餐状态更新骑手端负责接配送单、更新配送状态管理员后台负责审核商家入驻、查看经营数据、处理用户反馈。这种按领域拆模块的方式比按功能拆模块要好维护得多。比如“订单”这个概念用户端看到的是“我的订单”商家端看到的是“门店订单”骑手端看到的是“配送任务”同一个订单在不同的业务上下文里有不同的字段展示和操作权限如果一开始不按领域划分清楚代码就容易写成一团乱麻。2. 核心功能模块拆解与数据建模2.1 用户端、商家端与后台的菜单式功能清单整套系统的功能按角色拆每个角色对应一套独立的前端页面和后端接口前缀。用户端包含账号注册登录、商家列表浏览、按商品名或分类搜索、菜品详情查看、购物车新增删除修改、购物车结算、提交订单、模拟支付、个人订单列表、订单详情、取消订单、确认收货、发表评价以及个人资料编辑。这部分是外卖系统的核心体验代码量占比最大。商家端包含工作台概览、菜品新增编辑上下架、菜品分类管理、订单列表实时刷新、订单接单/拒单、出餐完成通知、配送状态跟踪、当日营业额统计、门店信息编辑。商家端对实时性要求比用户端高因为订单推送到商家端需要及时提醒。管理后台包含商家入驻审核、用户管理、商家管理、骑手管理、订单全览与异常处理、评论管理、数据统计报表、系统公告管理。这个后台不需要太花哨数据表格为主重点是权限隔离和操作留痕。2.2 数据库表结构设计数据库设计是这套系统里最见功夫的部分。一共设计了14张表核心表包括用户表、商家表、菜品表、分类表、购物车表、订单表、订单明细表、配送地址表、评论表、骑手表、骑手配送单表、公告表、操作日志表、管理员表。其中订单和订单明细是绝对核心。订单表的主键我用了自增ID配合order_no作为业务唯一流水号。order_no是前端展示给用户看的订单编号它不等于主键。原因是主键自增ID容易被人通过遍历猜位数而order_no生成规则是yyyyMMddHHmmss 随机4位 用户ID后4位既能保证业务上可读又能避免接口层暴露具体数据规模。订单表的关键字段包括user_id、merchant_id、order_no、total_amount、status、payment_status、delivery_type、remark、created_at、paid_at、completed_at。这里特别说一下delivery_type字段我设计成可选项支持“配送到楼下”和“到店自取”两种模式这样系统适用范围更广不用改表结构就能支持堂食外带场景。订单明细表是订单表的子表一条订单对应多条明细核心字段包括order_id、dish_id、dish_name冗余、dish_image冗余、price快照、quantity、subtotal。菜品名称、图片和价格这三个字段一定要做冗余存储。原因很简单菜品表数据是可能被商家修改的如果订单明细只存菜品ID用户历史订单里显示的是修改后的商品名和价格这会直接引发投诉。存量订单快照这是外卖系统设计的常识实践里踩过坑才记得住。2.3 关键索引与事务设计思考数据库索引我是这样安排的订单表上建了(user_id, created_at)联合索引因为用户查询“我的订单”基本都按时间倒序翻页建了(merchant_id, status)联合索引因为商家端要按状态过滤订单订单明细表在order_id上建普通索引。购物车表用(user_id, dish_id)作为业务唯一约束防止同一用户同一菜品在购物车里出现两条重复记录。事务处理上下单这个动作被设计为一个大事务扣减购物车商品、生成订单主记录、批量生成订单明细、扣减商家菜品库存、记录订单状态流转日志这五步要么全部成功要么全部回滚。Spring中用Transactional注解实现注意要指定rollbackFor Exception.class否则运行期异常才回滚业务异常和自定义异常可能导致事务不生效这个细节很多初学者会漏。3. 后端服务实现关键接口与业务逻辑3.1 统一响应体与全局异常处理后端接口返回结构我统一用了一个ResultT类包含code、message、data三个字段。成功时code为200业务失败时code为400或401系统异常时code为500。前端Axios拦截器里首先判断code再做后续处理而不是每次请求都去判断HTTP状态码。全局异常处理用RestControllerAdvice实现。我自定义了BusinessException、UnauthorizedException、ForbiddenException三个异常分别处理参数或业务校验失败、登录态失效、权限不足。全局异常处理器捕到后统一转成Result对象返回。这么做的意义在于Controller里的代码可以非常干净不需要写一堆try-catch块业务校验失败直接throw new BusinessException(库存不足)就行。3.2 JWT登录认证与权限控制登录认证用的是JWT方案不是Server Session。原因一是前后端分离后Session处理跨域复杂二是JWT无状态后端接口天然支持分布式横向扩展后面就算拆微服务也不用改认证机制。JWT加密用HMAC-SHA256算法密钥在application.yml中通过环境变量注入。Token里只放了userId、role、expiresAt三个关键信息不放手机号和密码。有效时间设置为7天用户点击“记住我”时延长到30天。权限控制环节我定义了一个AuthInterceptor拦截器在SpringMVC注册时指定拦截/api/**。拦截器里做三件事校验Token签名是否有效、校验Token是否过期、将解析出的用户信息放入ThreadLocal供后续业务代码获取当前登录用户。角色权限方面后端接口URL设计为/api/user/**、/api/merchant/**、/api/rider/**、/api/admin/**在拦截器里检查当前用户角色是否匹配URL前缀不匹配直接返回403。这个方案虽然粗暴但非常直观校园级别系统完全够用。3.3 下单接口与订单状态机实现下单接口是核心中的核心。先看完整流程PostMapping(/api/order) public ResultLong createOrder(RequestBody Valid CreateOrderDTO dto) { Long userId UserContext.getUserId(); return Result.success(orderService.createOrder(userId, dto)); }OrderServiceImpl里的核心逻辑分五步第一步校验配送地址是否属于当前用户且地址状态有效。第二步查询购物车数据按商家维度分组。这个很关键一个订单只能属于一个商家如果用户同时把A商家和B商家的菜品加入购物车下单时必须提示用户分开下订单。我把下单请求设计为merchantId参数前端在下单前会先按商家分组用户选择某个商家后只提交该商家的商品。第三步校验每个菜品是否在售、库存是否充足。这一步要用SELECT ... FOR UPDATE对菜品库存行加锁防止两个下单请求同时扣减库存。第四步生成订单主记录和明细记录同时扣减库存、清空购物车中对应商家的商品。第五步记录订单状态流转日志状态从PENDING_PAYMENT开始。Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, CreateOrderDTO dto) { // 1. 校验地址 // 2. 购物车按商家分组校验 // 3. 菜品库存行锁校验 // 4. 插入订单主记录 明细扣库存清购物车 // 5. 写入订单状态日志 }订单状态机我建议用常量或枚举类定义不要用魔法值。系统中所有订单操作接单、出餐、配送、确认收货、取消、退款都必须调用OrderStateMachine类里的方法流转状态。状态机里校验当前状态是否允许目标转换例如“已完成”的订单不能被取消“已取消”的订单不能被接单这一步用状态机统一管理比到处散落if-else判断要安全得多。3.4 购物车、库存与模拟支付购物车接口实现不复杂核心是防止重复添加。插入前先查(user_id, dish_id)是否存在存在则更新数量不存在则新增。模拟支付是我这套系统特意保留的一个模块。因为真实接入支付宝或微信支付需要商户资质和繁琐的签约流程课程设计和毕设场景没必要卡在这一步。我实现了一个MockPaymentController接收orderId后生成支付成功回执更新订单状态为PAID同时向商家端推送WebSocket消息提醒新订单。这里的核心逻辑是支付后要触发后续流程而不只是改个状态字段。4. 前端Vue3实现页面组织与核心交互4.1 Vue3工程结构与路由设计前端工程的结构如下按角色分包src/ api/ # 按模块封装的请求接口层 assets/ # 静态资源 components/ # 通用组件 layouts/ # 布局组件区分用户端和商家端 router/ # 路由配置 stores/ # Pinia状态管理 views/ user/ # 用户端页面 merchant/ # 商家端页面 admin/ # 管理后台页面 common/ # 登录注册等公共页面 utils/ # 工具类路由守卫是整个前端权限控制的关键。在router.beforeEach里做了三件事从Pinia读取用户状态没有则从localStorage恢复检查目标路由的meta.requiresAuth和meta.role字段不满足条件则跳转到登录页或401页面。这里有一个开发者容易漏掉的细节后端JWT过期后前端请求会收到401响应。处理方式是在Axios响应拦截器里统一清空用户状态并跳转登录页而不是在某个页面组件里一个个处理错误状态。4.2 API封装与Pinia状态管理Axios实例在创建时统一配置了baseURL为/api开发环境走Vite代理请求拦截器里把localStorage中的Token塞入Authorization请求头响应拦截器里统一处理code非200的业务错误和401登录失效。状态管理用Pinia而不是Vuex理由刚才说过Composition API配合Pinia的Composition风格写法逻辑更直观。用户信息userStore存当前登录用户、Token前缀、角色cartStore存购物车列表和增加删除操作购物车状态是所有用户端页面共享的所以放在Pinia而不是组件内部。底部导航栏、菜品详情页、购物车页面都会同时读取购物车数量如果不用全局状态会非常难维护。4.3 核心页面交互实现用户端首页是一个商家列表进入商家详情页后左侧是菜品分类右侧是菜品列表。菜品卡片上有“加入购物车”按钮点击后请求后端接口成功后更新购物车。商家详情页顶部固定展示购物车总价和结算按钮。这里的交互细节是Vue3中菜品列表和购物车之间的数据同步通过cartStore里的action统一更新返回最新数量后驱动UI响应而不是在菜品组件里直接修改购物车数据结构。下单确认页用了Element Plus的Steps组件展示订单状态进度条配送地址选择了附近宿舍楼栋的静态数据模拟实际落地可以接入高德地图API或腾讯位置服务。倒计时组件在订单支付页实现前端倒计时到0后向后端请求一次订单状态如果仍未支付则显示“订单已取消”并禁用支付按钮。5. 联调部署与常见问题排查实录5.1 前后端联调中的跨域与Mock问题开发阶段最常出现的坑是跨域。解决方式不在后端配CORS而是在Vite的server.proxy里配置代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } }这样前端请求/api/user/login时Vite会代理到http://localhost:8080/api/user/login。之所以推荐代理而不是后端放开CORS是因为生产环境前端静态文件由Nginx托管同样配置一层location /api的proxy_pass就能解决生产跨域代码上不用做任何切换。写过前后端分离项目的人都知道把跨域处理逻辑放在架构层业务代码才够干净。5.2 生产环境打包部署流程前端打包命令是npm run build生成dist目录把目录下所有文件拷贝到服务器的/usr/share/nginx/html下。Nginx配置关键段location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }后端打包用mvn clean package -DskipTests生成jar包后用nohup java -jar启动。注意SpringBoot的application-prod.yml要区分数据库连接、Redis地址和JWT密钥不能把开发环境配置带上去。数据库方面首次启动时执行初始化SQL脚本脚本中建库建表并写入管理员账号和示例商家数据。5.3 高频问题与排查方法速查这里整理我实际开发中遇到频率最高的问题和解决思路。第一个是下单并发导致库存超卖。排查思路是在菜品表加上stock字段后更新SQL语句必须带上库存条件判断UPDATE dish SET stock stock - 1 WHERE id ? AND stock 0如果影响行数为0说明库存不足直接抛异常回滚。单纯靠Java代码先查后改并发下一定会出问题。第二个是订单状态错乱。比如用户提交了取消申请同时商家点了接单。这个问题出在状态更新没有加条件。正确的更新方式是在UPDATE order SET status ? WHERE id ? AND status ?用数据库状态作为乐观锁更新行数为0再抛异常避免出现状态被覆盖。第三个是WebSocket连接频繁断开。我的方案是前端加了心跳检测每30秒发送一次ping字符串后端收到后返回pong连续三次收不到心跳则服务端判定连接失效并清理连接。网关这种场景如果不做心跳Nginx的默认空闲超时60秒会直接断开连接。第四个是Vue3组件重复请求导致页面卡顿。商家端订单列表因为要实时刷新最初我做的是每3秒轮询一次接口后来改成WebSocket推送新订单只有收到新订单消息时才触发重新拉取列表服务端压力和前端渲染压力都明显下降。第五个是数据库连接池溢出。MySQL默认最大连接数是151SpringBoot的HikariCP默认池大小是10。如果并发测试时看到Connection is not available的报错基本就是池被耗尽。排查时看HikariPool的active连接数和线程栈找到哪个接口持有连接不放。常见原因是事务方法内部调用的方法捕获了异常但没回滚导致连接被事务占住解决方式是确保事务方法内所有数据库操作都在同一个try-catch逻辑内不要私自吞掉异常。5.4 这套源码后续可以扩展的方向如果拿到这套源码想继续往下做我个人的建议会接这三个方向一是接入高德地图API把配送地址从“手动选择楼栋”升级为地图选点骑手端也能看到配送路线二是增加营销模块加入优惠券、满减活动、新客立减这类功能对表结构是加两张表和几个校验接口的事三是把订单模块里的消息推送换成更正式的推送服务比如接入第三方短信服务下单成功、商家接单、骑手出发时给用户发站内信或模板消息。做好了这三个方向整个系统的完整度基本接近商业项目。我在实际工具使用中有个体会这套系统源码的数据库建表脚本是整套资产里最值钱的部分。很多开发者拿到源码喜欢先跑前端、先研究接口我会反过来让他们先打开SQL脚本把每张表的字段、注释、索引、外键关系读懂再去读后端代码。表结构就代表业务模型业务模型看透了接口设计和前端页面就都能顺理成章地理解。这也是我做这套系统复盘时最深的体会。项目所有的表设计、状态流转、权限控制都是围绕“订单生命周期”展开的只要抓住这条主线后面增加任何功能都不会跑偏。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询