瑞吉外卖源码解析:Spring Boot + Redis + MyBatis Plus 实战指南
2026/9/16 8:38:21 网站建设 项目流程

简介:瑞吉外卖作为Java开发者高频练习的实战项目,这份源码包完整覆盖餐饮外卖系统的前后端实现,特别适合刚学完Spring Boot、希望积累项目经验的初学者,也适合用其完成课设或毕设的学生。项目围绕菜品管理、购物车、订单流转、后台管理等典型业务展开,包含可直接运行的Java工程与配套页面资源,便于边读源码边理解整体调用链。压缩包共194个文件,大小约29.77MB。其中69个Java文件承载核心业务逻辑,22个HTML和22个JS构架前端页面,18个CSS实现样式,加上JSON、YAML、XML等配置类文件以及大量PNG图片资源,可满足完整项目的调试与部署需要。目前已有349人学习/下载。通过分析源码结构,可以快速梳理外卖系统的接口设计、数据模型和页面联动,是提升Java Web开发能力的实用参考资料。

1. 瑞吉外卖源码解压之后:先理解 Java 工程和一堆 CSS 文件的关系

基于 Java 的瑞吉外卖项目源码 zip 解压后,如果第一眼看到的是 index.css、vant.min.css、add-order.css,而不是满屏的 .java 文件,不用怀疑拿错包了。这套资源里移动端 H5 的页面样式占了不小比例,真正的后端逻辑在 src/main/java 底下。它是典型的 Java Web 练习项目:Spring Boot 做接口,MySQL 存业务数据,Redis 做缓存,前端用 Vant 类组件库拼出点餐、购物车、下单页面。

适合想要把 Java 基础、Spring Boot、MyBatis Plus 串成完整链路的开发者,也适合在准备 Java 面试题时对照实际代码理解缓存、拦截器、事务这些概念。下面先讲启动前的工程结构,然后走一遍登录拦截、缓存和下单链路,最后给出移动端页面的调试技巧。

2. 瑞吉外卖源码的工程分层与配置项:从 pom.xml 读到 MySQL 初始化

把 zip 解压后,先别急着点启动。瑞吉外卖这类 Java 工程的结构通常分为左右两部分:左边是 src/main/java 里的 Controller、Service、Mapper,右边是 src/main/resources/static 下直接暴露给浏览器的 css、js、html。zip 里出现 common.css、index.css、vant.min.css、main.css、demo.css 这些文件,说明它把移动端 H5 也打包在了一起。认清这个边界后,再去看 pom.xml 会顺畅很多。

2.1 pom.xml 依赖选型:为什么是 Spring Boot + MyBatis Plus + Redis

先看一段典型的依赖配置,version 位置用占位符,实际版本以解压后的 pom.xml 为准,这里主要看选型思路:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

starter-web 提供 REST 接口的 MVC 能力,MyBatis Plus 把单表 CRUD 从“写一遍 XML”压缩到“继承一个 ServiceImpl”,Redis 用来缓存菜品分类和菜品列表,mysql-connector 负责连库。和 Spring Cloud 全家桶相比,这个组合没有服务注册、网关、配置中心,单体部署时更轻,启动也快,很适合用来把 Java 基础到后端接口这一段知识链条补完整。

依赖解决的场景常见坑
spring-boot-starter-webHTTP 接口、静态资源托管版本和 Spring Boot 父工程冲突
mybatis-plus-boot-starter单表 CRUD、LambdaQueryWrapper需要配置分页插件,否则分页失效
spring-boot-starter-data-redis缓存、登录会话默认 JDK 序列化导致缓存乱码
mysql-connector-java访问 MySQLMySQL 8 必须带 serverTimezone

这张表对应了大多数人第一次跑瑞吉外卖源码时遇到的四个问题:依赖版本、分页查不出数据、Redis 出现\xAC\xED前缀、数据库连接报时区错误。后面几章都会逐个展开。

2.2 数据库初始化与 application.yml 中必须改的参数

数据库脚本一般放在 sql 目录下,文件名可能是 db_ruiji.sql 或 ruiji.sql。导入前先确认 MySQL 版本,再用命令行导入:

mysql -uroot -p < sql/ruiji.sql

如果你用的是 Windows,也可以在 MySQL 客户端里执行source C:/path/sql/ruiji.sql。导入完成后,登录 MySQL 执行show tables;应该能看到 employee、category、dish、shopping_cart、orders 这类业务表。看不到完整表结构就继续找脚本,否则后面的接口 100% 会报 Table doesn't exist。

导入脚本后改 application.yml,注意这几个参数:

spring: datasource: url: jdbc:mysql://localhost:3306/ruiji?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0

url 里必须带 serverTimezone,否则高版本 MySQL 驱动会抛 java.sql.SQLException;useSSL=false 不是必须,但能省掉本地环境的一堆 SSL 警告。数据库名以实际加载的脚本为准,如果脚本建的库叫 other,这里也要同步改成 other。password 换成你本机数据库密码。redis 如果没改过默认端口,host 和 port 保持原样即可,database 建议单独用 0 或 1,别去挤生产库。启动后如果控制台一直报 connection refused,优先排查 Redis 有没有启动,Windows 下可以执行redis-cli ping验证。

2.3 静态资源目录约定:index.css 和 add-order.css 到底被谁加载

Spring Boot 默认从 classpath:/static 读取静态文件,所以 zip 里的 common.css、vant.min.css、main.css、demo.css、index.css、add-order.css、user.css、page.css、address.css 这些文件,最终要落到 src/main/resources/static 下对应的 css 目录里。常见的前端入口页面会直接引用它们:

<link rel="stylesheet" href="/css/index.css"> <link rel="stylesheet" href="/css/vant.min.css">

如果项目里配置了自定义的 addResourceHandlers,访问规则会变,但瑞吉外卖这类单体项目一般不需要改。常见做法是把整个 static 映射到根路径,让浏览器直接以 /css/xxx.css 的方式拿文件。

提示:如果你解压后 css 文件在项目根目录而不是 static 下,Spring Boot 默认是访问不到的。先把它们挪到 src/main/resources/static/css 下,再启动应用,浏览器访问 http://localhost:8080/css/index.css 应该能直接返回样式内容。

这里有一个很多人忽略的细节:Spring Boot 2.x 默认静态资源路径是固定的,不需要写代码;写了 addResourceHandlers 反而可能覆盖默认行为。所以你打开源码看到 WebMvcConfigurer 里已经有相关配置,先不要加重复映射,确认原有映射是否指向正确目录即可。

3. 登录拦截器与 Token 失效机制:瑞吉外卖权限链路怎么做才不踩坑

瑞吉外卖源码里最常见的权限实现不是 Spring Security,而是自定义 HandlerInterceptor。原因很直接:这类项目只有“用户端 H5”和“后台管理”两种角色,Security 的过滤器链、UserDetailsService、密码编码器配置起来要写十几行,而拦截器注册一个配置类就能跑通。但拦截器用不对,会出现“页面能打开但静态资源全被拦”“接口明明需要登录却直接放行”这类问题。

3.1 拦截器比 Filter 更合适的判断依据

Filter 可以过滤所有请求,包括静态资源,但它拿不到 Handler 信息,不知道这个请求是映射到 Controller 还是直接读文件。HandlerInterceptor 在 DispatcherServlet 之后执行,能根据 handler 类型、请求路径精细控制放行或拦截。所以权限校验用拦截器,字符编码、CORS 这类跨切面任务才用 Filter。这也是 Java 面试八股文里经常出现的一个比较点。

3.2 用户端和管理端两套拦截规则

先看用户端登录拦截器,我自己在跑这个项目时会封装一个 RedisUtil 来管理 token 对应的用户信息,如果你拿到的源码用 HttpSession,原理一样,只是从 session 取值:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } Object userId = RedisUtil.get("login:token:" + token); if (userId == null) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期\"}"); return false; } request.setAttribute("userId", userId); return true; } }

这段代码的核心逻辑是:先从请求头取 Authorization,取不到直接返回 401;取到了再去 Redis 查一下这个 token 是否有效。login:token:是 key 的前缀,后面拼上 token 值,Redis 里存 userId。如果用户退出或管理员踢人,只要删除这个 key,下一次请求就会立即失效。注意在 preHandle 里返回 false 时,后续 Controller 不会执行,所以不需要再写return super.preHandle()

注册拦截器时,用户端和管理端要分开处理。管理端接口如果和用户端放在相同路径下,要小心规则匹配顺序:

拦截路径放行路径说明
用户端/api/user/**/api/user/login、/api/user/register、/front/**移动端 H5 交互
管理端/api/admin/**/api/admin/login后台管理系统
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/api/user/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/front/**"); registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/api/admin/**") .excludePathPatterns("/api/admin/login"); } }

注意/front/**一定要放行。很多前端页面使用的是 Vue 或原生 HTML,页面本身不需要登录才能加载静态文件,登录只发生在接口调用阶段。如果你把/front/**拦了,访问首页就会出现白屏加一堆 401。

3.3 静态资源放行:css/js 404 的排查方法

回到 zip 里的那些 css 文件。样式丢失最常见的两个原因是:拦截器把/css/**拦了,或者静态资源根本没有放在 classpath 下。排查顺序我一般固定为三步:

先打开浏览器开发者工具,切换到 Network 面板,找到vant.min.css这个请求,看它的 HTTP 状态码。如果是 401,就是被拦截器拦了,在 excludePathPatterns 里补上/css/**/js/**/index.html;如果是 404,用curl -I http://localhost:8080/css/index.css确认资源是否存在,不存在就检查 static 目录位置;如果状态码是 200 但样式依然失效,看 Content-Type,应该是text/css,如果变成application/json,说明请求被 ErrorController 接到了。

registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/css/**", "/js/**", "/index.html", "/api/user/login", "/api/admin/login" );

这种写法适合 demo 级别快速联调。不过生产环境我不建议直接放行所有/css/**,而是尽量把静态资源部署到 Nginx,Java 服务只提供/api/**接口,这样拦截器规则会简单很多。

4. 菜品缓存、购物车与订单提交:从 Redis 到 MySQL 的完整链路

前三章解决了工程启动问题,这一章走业务核心。瑞吉外卖的完整链路是:用户进入页面,按分类加载菜品列表,加入购物车,确认地址后提交订单。每段都有容易踩的细节:缓存序列化、购物车数量累加、事务失效。面试问到的 Redis 缓存穿透、Spring 事务传播,其实都能在这个项目里找到对应场景。

4.1 Spring Cache + Redis 缓存菜品列表时的序列化问题

菜品列表按分类查询,同一个分类下的数据变化不频繁,适合用 @Cacheable 缓存。常见实现是直接在 Service 方法上声明缓存 key:

@Cacheable(cacheNames = "dish", key = "#categoryId", unless = "#result == null || #result.isEmpty()") public List<Dish> listByCategoryId(Long categoryId) { return lambdaQuery().eq(Dish::getCategoryId, categoryId).list(); }

key 用分类主键,比如查询 categoryId=1 时,Redis 里的 key 是dish::1unless表示结果为 null 或空列表时不要缓存,避免空结果把缓存写满。这里要注意,在类内部调用本类的另一个方法时,@Cacheable 不会生效,必须走 Controller -> Service 代理对象,所以不要把同步、缓存注解和业务逻辑混在同一个 private 方法里。

默认的 RedisTemplate 用 JDK 序列化,存进去 value 会出现\xAC\xED之类的二进制前缀,其他语言或新版 Java 反序列化时会报错。在实际项目里我会显式声明一个用 Jackson 序列化的 RedisTemplate:

@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer<Object> jackson = new Jackson2JsonRedisSerializer<>(Object.class); template.setKeySerializer(RedisSerializer.string()); template.setValueSerializer(jackson); return template; }

这段配置让 key 保持字符串,value 存 JSON。配置后,更新菜品时要用 @CacheEvict 清掉对应分类的缓存,否则用户看到的是旧数据:

缓存场景推荐 key失效时机
菜品分类列表category::all后台修改分类
分类下菜品列表dish::{categoryId}新增/修改/删除菜品
套餐列表setmeal::{categoryId}新增/修改/删除套餐

业务中常见的坑是:只加了 @Cacheable,忘了加 @CacheEvict。结果后台改了菜品价格,前端 App 上还是旧价,必须重启服务才更新。这就是“缓存与数据库不一致”的典型现场。

4.2 购物车增加菜品:先查再插入还是直接 save

购物车表 shopping_cart 的插入逻辑不能直接 save。同一个用户重复点击同一个菜品,应该把 number 累加,而不是插入一条新记录。常见写法:

public void add(ShoppingCart cart) { ShoppingCart one = lambdaQuery() .eq(cart.getDishId() != null, ShoppingCart::getDishId, cart.getDishId()) .eq(cart.getSetmealId() != null, ShoppingCart::getSetmealId, cart.getSetmealId()) .eq(ShoppingCart::getUserId, cart.getUserId()) .one(); if (one == null) { cart.setNumber(1); save(cart); } else { one.setNumber(one.getNumber() + 1); updateById(one); } }

参数说明:eq(cart.getDishId() != null, ...)是 MyBatis Plus 的条件构造器,第一个参数为真时才拼接这条 where 条件。如果 dishId 和 setmealId 都为 null,这里会查出该用户购物车所有记录,然后报 TooManyResultsException,所以接口层要强制校验两个字段不能同时为空。另一个容易出问题的点是:前端传过来的 amount 字段不能直接落库,价格应该由后端根据 dish 表或 setmeal 表重新查一次,否则用户改一下请求 Body 就能改价格。

4.3 订单提交:事务边界与状态字段设计

下单接口是整个项目中事务最集中的地方,它要做三件事:生成订单主表记录、把购物车里的内容转成订单明细、清空购物车。三个操作必须在一个事务里,否则可能出现“主表有单、明细为空”的数据不一致。典型实现:

@Transactional(rollbackFor = Exception.class) public Order submit(Order order) { String orderNumber = System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000)); order.setNumber(orderNumber); order.setStatus(1); save(order); List<ShoppingCart> carts = shoppingCartService.listByUserId(order.getUserId()); for (ShoppingCart cart : carts) { OrderDetail detail = new OrderDetail(); detail.setOrderId(order.getId()); detail.setDishId(cart.getDishId()); detail.setSetmealId(cart.getSetmealId()); detail.setNumber(cart.getNumber()); orderDetailService.save(detail); } shoppingCartService.cleanByUserId(order.getUserId()); return order; }

事务注解的rollbackFor = Exception.class很重要,默认情况下 RuntimeException 才回滚,如果代码里捕获了异常并返回错误结果,事务不会触发回滚。常见的失效场景还有:同一个类里 A 方法调用 B 方法,B 上的 @Transactional 不生效,因为代理对象没有经过 Spring 管理。如果你怀疑事务失效,把这两个方法拆到不同 Service 里互相调用,或者用 AopContext 拿当前代理,这是 Java 面试里问烂的题目。

订单状态 status 字段建议单独写成枚举或常量,不要散落魔法值:

status含义常见扩展
1待支付/待接单预留支付回调更新
2已接单商家端操作
3已送达状态推进
4已取消超时或用户取消

下单前最好再判断一下地址是否存在、购物车是否为空,否则会出现脏数据。前端 H5 的 add-order.css 和 address.css 对应的就是这两个步骤的页面。

5. 对前端页面做接口验证:curl 测试瑞吉外卖 API 与 CSS 引用修正

5.1 用 curl 验证登录接口和商品列表接口

后端接口联调不一定要先跑前端。我一般先启动 Spring Boot,然后用 curl 直接打接口,确认数据层和缓存层都对,再打开 H5 页面。先测登录,接口前缀以源码里的 Controller 映射为准,这里按常见结构举例:

curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"phone":"13800000000","password":"123456"}'

返回体里应该有 code 和 token。如果提示用户名密码错误,先看数据库中 user 表的初始数据。拿到 token 后,把它带到菜品列表接口:

curl http://localhost:8080/api/dish/list?categoryId=1 \ -H "Authorization: <token>"

<token>替换成上一步的返回值,能看到 JSON 数组说明接口链路是通的。如果返回 401,就是 token 没传对,或者拦截器里 key 的前缀和你写入 Redis 时不一致;如果返回 500,去看后端日志,重点找 SQLException 和 RedisConnectionFailureException。

5.2 移动端 H5 页面样式丢失时的修复顺序

最后再回到那堆 css 文件。前端页面 index.html 的 head 区域常见这样写:

<link rel="stylesheet" href="/css/vant.min.css"> <link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/add-order.css">

这里建议全部用绝对路径,不要用./css../css。原因在于,index.html 在/index.html时相对路径没问题,但进入/order/confirm.html这类子路径后,相对路径会解析到/order/css/...,直接 404。zip 里的 add-order.css、user.css、address.css 分别对应订单确认、用户中心、地址管理页面,如果你发现只在某些二级页面样式丢失,优先检查是不是相对路径问题。

如果用了 Nginx 托管静态资源,还要确认location /css/是否正确指向 css 目录。改完引用后记得强制刷新浏览器缓存,H5 页面经常因为 Cache-Control 或 ETag 沿用旧 CSS,导致你改了文件却看不到效果。在 Network 面板勾选 Disable cache,或者直接按 Ctrl+F5 强制刷新,再确认这条 CSS 请求的响应状态是不是 200,以及响应体长度是否等于本地文件的大小。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询