ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

瑞吉外卖源码解析:Spring Boot + Redis + MyBatis Plus 实战指南

瑞吉外卖源码解析:Spring Boot + Redis + MyBatis Plus 实战指南 简介瑞吉外卖作为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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependenciesstarter-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 doesnt exist。导入脚本后改 application.yml注意这几个参数spring: datasource: url: jdbc:mysql://localhost:3306/ruiji?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0url 里必须带 serverTimezone否则高版本 MySQL 驱动会抛 java.sql.SQLExceptionuseSSLfalse 不是必须但能省掉本地环境的一堆 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 relstylesheet href/css/index.css link relstylesheet 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;charsetUTF-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/**而是尽量把静态资源部署到 NginxJava 服务只提供/api/**接口这样拦截器规则会简单很多。4. 菜品缓存、购物车与订单提交从 Redis 到 MySQL 的完整链路前三章解决了工程启动问题这一章走业务核心。瑞吉外卖的完整链路是用户进入页面按分类加载菜品列表加入购物车确认地址后提交订单。每段都有容易踩的细节缓存序列化、购物车数量累加、事务失效。面试问到的 Redis 缓存穿透、Spring 事务传播其实都能在这个项目里找到对应场景。4.1 Spring Cache Redis 缓存菜品列表时的序列化问题菜品列表按分类查询同一个分类下的数据变化不频繁适合用 Cacheable 缓存。常见实现是直接在 Service 方法上声明缓存 keyCacheable(cacheNames dish, key #categoryId, unless #result null || #result.isEmpty()) public ListDish listByCategoryId(Long categoryId) { return lambdaQuery().eq(Dish::getCategoryId, categoryId).list(); }key 用分类主键比如查询 categoryId1 时Redis 里的 key 是dish::1。unless表示结果为 null 或空列表时不要缓存避免空结果把缓存写满。这里要注意在类内部调用本类的另一个方法时Cacheable 不会生效必须走 Controller - Service 代理对象所以不要把同步、缓存注解和业务逻辑混在同一个 private 方法里。默认的 RedisTemplate 用 JDK 序列化存进去 value 会出现\xAC\xED之类的二进制前缀其他语言或新版 Java 反序列化时会报错。在实际项目里我会显式声明一个用 Jackson 序列化的 RedisTemplateBean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializerObject 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); ListShoppingCart 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?categoryId1 \ -H Authorization: token把token替换成上一步的返回值能看到 JSON 数组说明接口链路是通的。如果返回 401就是 token 没传对或者拦截器里 key 的前缀和你写入 Redis 时不一致如果返回 500去看后端日志重点找 SQLException 和 RedisConnectionFailureException。5.2 移动端 H5 页面样式丢失时的修复顺序最后再回到那堆 css 文件。前端页面 index.html 的 head 区域常见这样写link relstylesheet href/css/vant.min.css link relstylesheet href/css/main.css link relstylesheet 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或者直接按 CtrlF5 强制刷新再确认这条 CSS 请求的响应状态是不是 200以及响应体长度是否等于本地文件的大小。本文还有配套的精品资源点击获取
返回列表