资讯动态

校园外卖全栈系统:SpringBoot+Vue3+MyBatis实战解析

发布时间:2026/10/22 2:55:25 来源:尧图企业网站定制
做Java全栈方向的项目最常被问到的综合练手选题之一就是校园外卖服务系统。听名字以为是“给学校食堂做个网页版点餐”真正拆开之后你会发现这个选题几乎把一个后端开发者的日常业务全部装进去了用户注册登录、商家上架菜品、购物车勾选、下单支付、接单配送、评价管理每一环都对应一串接口、一张表和一段业务逻辑。标题里的SpringBootVue3MyBatis是当前国内团队开发里非常常见的一套组合MySQL负责数据落地前后端分离的架构也省不掉。它适合正在准备毕业设计的同学、想积累Java全栈项目经验的求职者以及刚学完框架想找一个综合实战练手的入门开发者。这篇文章不聊空话我把从数据库设计、核心功能实现到前后端联调和问题排查的思路完整铺开实际开发中踩过的坑放在对应章节里直接对照着做就行。1. 项目到底在做什么校园外卖的业务闭环与技术选型校园外卖和普通商业点餐系统最大的不同在于角色和场景更明确。它一共三类使用方学生或教职工是消费者校内食堂窗口或周边商铺是商家平台方还有一个管理后台。整个系统就是围绕“吃一顿饭”这件事把三方的动作串起来。也正因为场景聚焦很多功能模块可以做得非常具体非常适合用来练手完整项目开发。1.1 三类角色的功能闭环用户端要解决的事情很直接注册登录、浏览店铺、查看菜品、加购物车、下单、付款、查看订单状态、取消订单、评价。商家端要解决的是入驻申请、菜品管理上架、下架、改价、调库存、订单接单与拒单、订单状态变更、营业数据查看。管理端要解决的是商家审核、用户管理、订单总览、平台上架内容管理。这三块业务放一起才能构成完整闭环。很多初学者做这类项目容易犯一个错误只把用户点餐那一套流程做漂亮商家后台和管理后台潦草带过。而实际答辩或面试时考察重点恰恰是商家接单后的状态流转和后台权限控制因为这两部分最能体现对业务的理解也不容易被“照着教程敲”糊弄过去。管理端哪怕只做最基础的商家审核和订单总览也一定要单独留一个入口别把所有角色糊在一套界面上。1.2 为什么选SpringBootVue3MyBatis这套组合不做技术选型分析直接上手容易踩到三个认知偏差。第一个是觉得“SpringBoot已经过时了Java后端现在都该上微服务”第二个是“Vue2资料多为什么不选Vue2”第三个是“MyBatis还要写XML太麻烦JPA更省事”。我在实操里对这三点的判断是SpringBoot本身并没有因为微服务而失去价值。它解决的还是“快速启动一个Web服务”的问题内嵌Tomcat、自动配置、起步依赖这些能力让项目不用纠结Tomcat版本和Spring版本匹配问题。微服务是对大型系统的拆分方式校园外卖这个体量用单体Boot完全够也是最稳的。Vue3加Vite的启动速度和组合式API在写组件时明显比Vue2舒服Vue2资料是多但新项目从Vue3起步对个人成长和项目生命周期都更划算。MyBatis在这个项目中最大的价值是SQL可控外卖系统的订单统计、报表查询、复杂条件筛选用MyBatis写原生SQL时逻辑一目了然调起性能来也方便。JPA适合CRUD为主的项目但订单和菜品的关联查询一旦复杂起来可能还得转成原生SQL中间交接成本反而更高。MySQL则没什么悬念。所有数据最终落在这一张关系型数据库里除了表结构设计要下功夫开发过程中相关文档和问题案例都是最多的出了问题查解决方案也快。为了更直观我做了一张简单的技术选型对照表比较维度SpringBoot传统SSM结论配置复杂度自动配置主类起步大量XML配置Boot更省时间部署方式内嵌Tomcat打jar即可运行外置Tomcat打war包Boot更简单生态兼容兼容主流三方库需要自己整合Boot更稳比较维度MyBatisJPA/Hibernate结论SQL可控性原生SQL优化方便自动生成SQL复杂查询需转JPQLMyBatis适合学习成本理解SQL即可需理解ORM映射、懒加载MyBatis更直接复苏速度XMLMapper接口注解实体类差不多选完之后整体开发节奏就很明确了先表结构后后端接口再前端页面最后联调收尾。2. 数据库设计先把底层表结构打磨扎实业务最后能不能扛住往往不取决于代码而取决于数据库设计。校园外卖系统里最需要动脑子的不是用户表而是订单几件套的拆分和状态字段的规划。这个环节花一天时间反复推敲都是值得的后面写的所有接口都绕不开这些表。2.1 核心表设计与字段清单按照“角色—对象—关系”的思路拆我推荐设计八张表。第一类是账号体系就是用户表。字段包含id、用户名、密码、真实姓名、电话、头像、角色、状态、创建时间。密码字段一定要存BCrypt加密后的字符串长度建议64以上不要用明文也不要只用MD5。角色字段可以用字符串或tinyint我习惯用tinyint加注释0是用户、1是商家、2是管理员配合前端路由权限控制。这个表还有个容易被忽略的点用户状态字段要留出来用于封号或注销场景。第二类是商家与商品对应商家表和菜品表。商家表要记录店铺名、简介、Logo、营业时间、起送价、配送费、评分、月售、状态、审核状态。审核状态单独留一个字段很重要商家注册后不能直接营业必须管理员审核通过后才显示在用户端。菜品表则要记录所属店铺ID、菜品名、图片URL、描述、原价、现价、分类、库存、销量、上下架状态。库存字段是后面做库存扣减的关键必须设计好而且要注意用整数而不是浮点数份数本来就该是离散值。第三类是订单核心对应订单表和订单明细表。订单表只是订单的“头”记录订单号、用户ID、商家ID、总金额、状态、收货人电话、收货地址、下单时间、支付时间、取消原因。明细表才是每道菜的真实记录包括订单ID、菜品ID、菜品名、菜品图片、单价、数量、小计。注意明细表里一定要冗余菜品名称和图片因为商家后来改菜名或删图不能影响历史订单展示。很多初学同学只设计一张订单表把每道菜用逗号拼接存进去这是大坑统计每道菜销量时你会怀疑人生。第四类是辅助业务包含购物车表、地址表和评价表。购物车记录用户ID、菜品ID、数量、选中状态地址表记录用户ID、联系人、电话、详细地址、默认标记评价表记录订单ID、用户ID、商家ID、评分、文字、图片、回复内容、创建时间。八张表的字段不是越多越好能省的冗余不要加但订单明细里的冗余字段必须保留这是实战中经常被忽视的设计点。2.2 订单状态机的设计逻辑订单状态是整个系统最容易乱的部分如果没有清晰的状态规划用户端、商家端、管理后台对同一笔订单的状态理解会对不上。我在这类项目里一直用“状态机”思维来驱动状态转换0待支付、1待接单、2配送中、3已完成、4已取消。数字顺序对应流程推进方向状态只能向后走或跳回取消不允许从3跳回1这种回退。具体流转规则是用户下单后订单为0超时未支付可以取消或系统自动关闭支付成功后变为1待接单商家确认接单后变为2配送中用户确认收货或骑手确认送达后变为3已完成用户支付前或商家接单前取消则变为4已取消。每个状态迁移都要校验当前状态否则并发场景下会出现“用户取消的同时商家正好接单”的脏数据。状态机的规则建议在数据库设计阶段就在文档里写清楚不要边写代码边琢磨接口一多很容易漏条件。另外还有两个设计细节。第一订单状态不要写在用户表或者菜品表里所有状态变更都通过订单表更新完成第二要记录状态变更时间字段比如支付时间、接单时间、完成时间这样管理后台的统计和用户的订单轨迹都能直接查出来。订单表里加一个update_time字段每个状态变更都刷新它这是排查问题时的线索。3. 核心功能实现登录、下单、接单一次讲透数据库定了之后后端接口的实现是工作量最大的部分。这一章我不把每个接口都列出来而是挑三个最容易做错、也最能体现工程能力的功能讲透登录鉴权、下单业务和订单状态流转。这三块每块都能展开讲很久掌握了它们其他接口都属于机械填充。3.1 JWT登录鉴权完整流程选择JWT而不是Session核心原因是前后端分离部署时前端页面可能运行在Vite开发服务器也可能打包后由nginx托管后端则独立跑在另一个服务中。Session方案需要跨域携带Cookie虽然也能配置但跨域安全和维护成本都更高。JWT把用户身份信息签名加密后放在Token里服务器不需要保存会话状态扩展性更好。流程拆开是五步前端提交用户名密码后端校验成功后生成Token并返回前端把Token存到localStorage并在Axios请求拦截器的请求头里带上Authorization字段后端定义拦截器解析Token校验签名和过期时间解析出的userId放进ThreadLocal或请求上下文供后续业务方法直接取用。对管理端请求拦截器再校验角色是否匹配。关键实现注意三点。第一Token密钥不要写死在代码里放到配置文件中避免打包后泄露。第二Token过期时间建议配合“记住我”做双策略短Token加刷新Token会让项目多一个亮点。第三密码校验一定用BCrypt的matches方法不要手动比较字符串。这里给一个核心的JWT生成片段做参考String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();HS256HMAC-SHA256是对称签名密钥只有后端知道前端无法伪造。解析时用同样的密钥验签签名失败或过期就直接抛异常配合全局异常处理器统一返回401。前端拿到401后自动跳转登录页这个链路是前后端分离项目的标配。3.2 下单事务与库存扣减的正确姿势下单流程是校园外卖系统里业务链最长的部分涉及校验用户、校验商家营业状态、购物车查重、库存扣减、创建订单主表、创建订单明细、清空购物车、返回结果。这一连串操作必须保证要么全部成功、要么全部回滚否则最典型的问题就是“购物车清了但订单没生成”或者“订单生成了但库存没扣”。解决这个问题的标准手段是Spring的Transactional注解把下单方法标记为事务方法内任何一步抛出异常整个事务回滚。但这里有一个实战体会很多初学者以为加了Transactional就万事大吉实际上事务只对通过Spring代理调用同一数据源的方法生效。如果下单方法内部调用了另一个Service的update方法只要两者在同一个事务管理器下就还能保持一致如果跨了数据源就需要分布式事务方案校园外卖单体项目用不到。库存扣减是另一个常见的超卖点。假设数据库初始库存是10份两个用户同时下单各买5份如果不加任何约束两个请求都可能先读到库存10然后各自扣减成功最后库存变成0预期能卖10份确实也卖出了10份这还不是最严重的情况。真正麻烦的是库存变成负数比如初始库存1份两个并发请求都通过了“剩余库存大于购买数”的检查最后库存变成-1业务彻底错乱。所以我推荐用数据库条件更新的方式一次SQL完成扣减不需要先查再更新。核心SQL是这样update iddeductStock UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity} /update这里的WHERE条件加上stock quantity是一个原子性的约束。MySQL在更新同一条记录时会对行加锁后来的请求必须等前一个事务提交所以不会出现超卖。如果这个UPDATE影响行数为0说明库存不足直接抛出业务异常让整体回滚。这种“先扣减再判断”的思路比“先查询再判断再更新”安全得多是写库存类业务的底线。3.3 订单状态流转的并发控制订单状态流转和库存扣减一样也需要条件更新。典型场景是用户取消订单和商家接单几乎同时发生用户点了取消商家在同一秒点接单。如果没有校验两步操作都执行了最后订单状态可能是配送中但用户已经以为取消成功了。解决办法是在更新状态时把期望的当前状态放进WHERE条件int rows orderMapper.updateStatus(orderId, targetStatus, expectStatus); if (rows 0) { throw new BizException(订单状态已变化操作失败请刷新后重试); }对应SQL是UPDATE orders SET status #{targetStatus}, update_time NOW() WHERE id #{orderId} AND status #{expectStatus}这一条SQL天然把“读状态-改状态”变成原子操作因为UPDATE会对符合条件的那一行加锁。影响行数为0就代表状态已经变了前端可以弹提示让用户刷新看最新状态。无论接单、拒单还是取消都复用这一个方法只在传入的期望状态上做区分代码会干净很多。这个方法也要用在支付回调上防止重复支付回调把订单状态打乱。4. 前后端分离联调最容易被忽视的四个深坑后端接口都通了不等于项目能跑起来。前后端分离的项目一大半时间消耗在联调上这四个坑我在实际项目里几乎每次都遇到提前看一眼能省很多debug时间。4.1 跨域配置的两种推荐做法什么是跨域前端页面跑在http://localhost:5173Vite默认端口后端接口跑在http://localhost:8080端口不同浏览器就把请求当成跨域请求会先发一个OPTIONS预检预检不通过则请求直接失败。解决方案要么后端放行要么用nginx把前后端收敛到同一个域名。本地开发最推荐的是后端加统一CORS配置。注意一个容易出错的点当需要携带Cookie或Authorization头时不能用简单的allowedOrigins写死一个地址而要用allowedOriginPatterns否则部分浏览器和Spring版本组合会报跨域失败。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果不想在后端写这些另一个稳定方案是在前端开发时配置Vite代理把请求转发到后端由Vite服务器代为转发浏览器看到的始终是同源请求可以规避大部分跨域问题。线上部署时再用nginx统一反向代理把/api开头的请求转发给后端服务。两个方案不冲突本地开发随意选一个线上以nginx为主。4.2 统一响应体Axios拦截器联调过程中最影响效率的就是各接口返回结构不统一。有的接口直接返回数据对象有的返回逗号拼接字符串还有的异常时直接给一段HTML错误页。前端拿到数据要做大量类型判断写起来非常痛苦。正确的做法是从第一天就定好统一响应体格式{ code: 200, message: success, data: {} }code为200表示成功401表示未登录或Token过期403表示无权限500表示业务异常或系统异常。所有Controller方法都不直接返回业务对象而是返回Result.success(data)或Result.error(code, message)。对应的Java工具类可以像这样public class ResultT { private Integer code; private String message; private T data; }前端再配一个Axios拦截器统一切割这个结构。响应拦截器里判断code非200就弹出错误提示401就跳转登录页并清理Token。这样所有页面代码只需要关注data的取值不用每个接口重复处理错误分支。这个套路也方便后续加全局loading和错误日志属于花小钱办大事的基建。4.3 Long类型ID的精度丢失问题这个问题是前后端分离项目特有的而且很隐蔽。MySQL主键如果用自增bigintJava后端的Long类型到了前端JavaScript里会因为JS的Number类型安全整数上限只有2的53次方发生精度丢失。举个例子数据库里一条订单ID是9007199254740993前端拿到可能变成9007199254740992拿着这个ID去查订单自然找不到。解决方案有多种最推荐给后端的JSON序列化做全局配置把Long和long自动转成字符串输出Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); }字符串类型的ID在JS里不会丢精度查询时后端接收的是字符串再转成Long也能还原。另一个思路是干脆把主键设计成UUID或雪花ID这种字符串但字符串主键在索引和关联查询上性能略差涉及订单明细多的时候我一般还是保留bigint自增主键只针对前端做ToString序列化。这个问题不做到联调阶段一定会有人问“为什么我拿到的ID不对”与其被问住不如提前处理掉。5. 实战问题排查与性能调优实录联调跑通之后真正考验人的是上线前后那一轮轮问题。这里把我遇到的几个高频问题和解决思路整理出来按排查顺序写好遇到相似的可以直接对号入座。5.1 数据库连接断开、时区误差这类环境坑典型场景一白天开发一切正常第二天早上第一次请求项目报Communications link failure。这个几乎可以断定是MySQL服务端杀掉了空闲连接。MySQL的wait_timeout默认8小时晚上没人访问连接池里的连接一直空闲超过8小时后服务端断开但连接池还没感知早上第一次请求拿这条死连接去执行SQL自然失败。解决办法是调整HikariCP两个参数idle-timeout要小于MySQL的wait_timeoutmax-lifetime也建议设置为比wait_timeout短的时间比如120分钟。Spring Boot的配置如下spring: datasource: hikari: idle-timeout: 120000 max-lifetime: 190000有个配套坑是时区。MySQL连接串如果不指定serverTimezone在MySQL 8.0版本下默认可能使用服务器本地时区而Java应用服务器和MySQL的时间不一致就会出现时间相差8小时。建议在JDBC连接串里显式写清楚jdbc:mysql://localhost:3306/campus_waimai?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse顺便把useSSL也关掉本地开发不需要证书开着反而多一次握手。5.2 MyBatis嵌套查询导致N1问题N1问题最典型的出现场景是订单列表。订单有100条每条订单要关联商家名称或订单明细。如果用嵌套查询的方式MyBatis会先查100条订单再对每条订单分别执行一次查询商家SQL总共查了101次。随着订单量增加数据库压力直线上升。解决方向有两个。第一个是我比较推荐的在XML里用ResultMap做嵌套结果映射一条SQL通过LEFT JOIN把订单和商家、明细一起查出来映射时再拆到对象里。第二个是前期设计时做冗余订单表直接冗余商家名称和店家联系方式订单明细冗余菜品名和图片这样列表页根本不需要二次查询商家表和菜品表。两种方案对比冗余方案简单直接适合项目初版ResultMap方案灵活适合订单明细需要实时更新价格的场景。毕设项目我建议直接用冗余方案省时间效果还好。夸张一点说把N1消灭在数据库设计阶段比写一堆缓存策略管用得多。5.3 防止超卖和重复下单超卖在前面已经提过用UPDATE的条件约束可以解决库存负数的问题。但还有一个相似的问题是重复下单用户手一抖在支付页点了两次确认按钮前端即使做了按钮禁用后端也应该有能力识别。最简单的防线是给订单号加唯一索引下单前先生成唯一订单号插入订单表时利用唯一索引拦截重复插入。更稳妥的做法是后端加一个基于Redis或数据库的分布式锁以用户ID为key同一时间只允许一个下单动作。分布式锁对毕设来说属于加分项如果不想引入Redis也可以前端按钮置灰加后端唯一索引双重防护效果已经不错。还有一类问题是状态并发前面讲的条件更新就是核心手段。这里补充一个实战心得状态变更的接口最好都返回受影响行数行数为0时一定要主动抛业务异常而不是返回成功否则前端会拿着一个看似成功但实际上没生效的订单数据继续走流程排查成本极高。日志里多打一行“状态更新失败orderIdxxxexpectStatusxxx”能省掉大量拉锯式debug。6. 项目结构规划与开发推进节奏最后聊一下工程组织。这一部分容易被新手当成“写代码前随便建几个文件夹就行”的事其实如果一开始把分层和分工定清楚后面几百个接口写起来会非常顺畅答辩时也拿得出手。6.1 后端分层与前端目录设计后端我建议按controller、service、mapper、entity、dto、vo、common、config分层。这里有一个容易被忽视的约束Controller层只负责参数接收和结果返回业务逻辑全部下沉到ServiceMapper层只做数据访问Controller的入参用DTO出参用VO不要直接把Entity返回给前端。直接返回Entity的第一个风险是密码字段暴露第二个风险是字段结构绑定数据库表前端改版时后端也跟着改耦合太高。前端的结构则建议views、components、api、router、store、utils六块。views按页面放比如LoginView.vue、ShopListView.vue、OrderListView.vuecomponents放复用组件比如菜品卡片、购物车弹窗、订单状态标签api目录每个模块一个文件比如userApi.js、orderApi.js里面统一封装接口调用。这样一个新来的同学看目录就能明白每个文件的职责范围。用一张简表把职责边界划清楚数据层作用entity与数据库表字段一一对应dto接收前端请求参数可做参数校验vo返回给前端的数据结构mapperSQL操作不写业务逻辑service业务逻辑编排与事务控制controller参数接收、鉴权注解、调用service6.2 短周期开发时间线参考如果从零开始做我给一个五到十天的节奏参考。第一天做需求梳理和数据库设计把所有表字段列完第二到第四天写后端接口按功能模块推进先用户登录再商家菜品接着是订单购物车最后是后台管理第五到第六天写前端页面同样按模块推进第七到第八天联调重点处理跨域、Token传递、状态同步这些细节第九天做测试和数据补充把菜品图片、商家信息填充得完整一些第十天检查部署打后端jar包前端dist文件用nginx托管。其中数据库设计那一天不要着急写代码把表结构反复过几遍把状态机画清楚后面所有接口都是照着表结构写的。规划好这一步不仅代码写得顺后期临时改需求也会少很多。很多人项目延期不是代码写得慢而是表结构改来改去Mapper层和前端接口跟着一起改三个地方联动修改时间成倍增加。个人实操体会放最后说。校园外卖服务系统说到底不是多么高深的技术项目但它的价值在于“麻雀虽小、五脏俱全”。一个能跑通完整下单闭环、能处理并发状态变更、能按统一格式输出接口的系统已经足够作为一份拿得出手的Java全栈作品。我自己带过的类似项目里学生最常摔跤的地方往往不是框架而是订单状态一乱、库存变负数、Long类型丢失精度这些看起来很基础的细节。把这些问题在开发阶段提前防住后面的路会特别顺。最后再分享一个小技巧接口文档工具在联调阶段尽早接上前后端对照着文档调比靠记忆调接口省心太多。项目做完之后也建议顺手把数据库索引再过一遍把订单表、菜品表的查询索引补上这个小动作对答辩时的性能问答题来说非常加分。

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

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

免费获取报价 →
↑