想象你打开一个外卖APP,从浏览菜品到吃上热乎饭,整个过程在程序世界里要经过几十个"岗位"的协同配合。下面我们就跟着一份订单的旅程,把"苍穹外卖"这个SpringBoot项目的每个角落都走一遍。
第一章 用户还没来——餐厅开门前的准备工作
餐厅不能空手开业,后厨得先备好锅碗瓢盆和水电气。
pom.xml 就是采购清单。它列明了要用Spring Boot、MySQL驱动、Redis客户端、阿里云OSS等所有"厨具"的版本号。Maven照着这张单子去中央仓库把依赖包全买回来,项目才能编译运行。
application.yml 是总控开关面板。这里写着"数据库在哪儿""Redis在哪儿""服务器端口是多少"。而 application-dev.yml 是开发环境的分控面板(连测试库,日志详细),application-prod.yml 是生产环境的分控面板(连正式库,日志精简)。多环境隔离,开发调试不会影响线上真实数据。
README.md 是新员工入职手册——项目介绍、怎么启动、接口文档链接都在里面。.gitignore 是保密条例,告诉Git哪些本地配置文件、编译产物不要传到代码仓库,避免泄露密码或引发冲突。
数据库SQL脚本(doc/sql目录)就是餐厅的初始菜单数据库——建表语句全在这儿,新环境执行一遍就能生成所有表结构。
第二章 用户打开APP——首页展示阶段
用户掏出手机点开外卖APP,首页需要展示菜品分类、轮播图、热门推荐。这个请求的旅程开始了。
请求先过门卫——拦截器
每次请求进来,第一个遇到的是 LoginInterceptor(登录拦截器)。它负责检查请求头里有没有合法的JWT令牌,就像餐厅门卫检查你有没有会员卡。没令牌或令牌过期?直接返回401,连门都不让你进。
对于登录、注册这些公开接口,门卫会放行。对于需要登录才能看的首页和下单接口,必须验明正身。验身通过后,UserContext(线程上下文)会把当前登录用户的ID存进ThreadLocal——这相当于给这个请求贴上一张"当前顾客是谁"的便签,后续所有处理环节都能随手取用,不用层层传参。
请求到达前台——Controller层
通过门卫后,请求来到 Controller(控制层,相当于前台点菜员)。比如 UserDishController 接收 /api/user/dish/list 请求,它只负责三件事:收参数、调Service、把结果封装成统一格式返回。
业务处理——Service层
Controller转身找到 DishService(菜品服务,相当于厨师长),说"顾客要看某分类下的菜品"。Service接口定义了"能做什么",它的实现类 DishServiceImpl 具体安排怎么做。厨师长决定去数据库取数据,并可能要查Redis缓存提速。
数据获取——Mapper层
Service调用 DishMapper(持久层,相当于切配师傅)。Mapper接口继承了MyBatis-Plus的BaseMapper,自动拥有增删改查能力。如果涉及多表联查或复杂动态SQL,就去 resources/mapper/DishMapper.xml 里写自定义SQL——相当于师傅遇到复杂菜品时需要自己动手切配。
配置保障
MyBatisPlusConfig 确保分页查询正常工作,主键自动生成。
RedisConfig 保证缓存数据序列化格式正确,避免乱码。
GlobalExceptionHandler 随时待命——任何时候发生异常,它都会拦截并返回格式统一的错误信息,不让程序裸奔崩溃。
数据怎么返回给前端——VO
Mapper查回来的数据库实体(Entity)可能包含很多字段(比如菜品创建时间、修改人),但前端只需要展示菜名、价格、图片。于是Service把Entity转换成 DishVO(View Object,视图对象)——相当于后厨把食材做成成品菜端上桌,只给顾客看成品,不给看原料和制作过程。
最后Controller把DishVO装进统一返回封装 Result(包含code、msg、data),JSON格式返回给前端,用户看到了漂亮的菜品列表。
第三章 用户加购下单——核心交易阶段
用户选了几道菜,点击"加入购物车",然后提交订单。
购物车操作
加入购物车请求来到 CartController,调用 CartService。购物车数据通常存Redis(因为频繁读写,且有时效性),RedisUtil 工具类提供 set、get、delete 等简易操作。缓存键前缀统一在 RedisConstant 常量类里定义,比如 "cart:user:123",避免不同业务键名冲突。
下单操作
用户点击"去支付",提交订单。请求进入 OrderController,它接收一个 OrderSubmitDTO(数据传输对象)。注意这个DTO里用了 @NotBlank、@NotNull、@Size 注解——这是参数校验,确保前端传来的地址、联系电话、菜品列表不为空且格式合法。校验不通过,连Service都不调用,直接返回"参数错误"。
校验通过后,Service开始核心业务逻辑:
查询购物车数据,计算总价
检查库存(调用DishMapper查数据库)
生成订单号(唯一)
扣减库存
清空购物车
保存订单到数据库
这些操作涉及多张表,必须保证事务一致性(要么全成功,要么全回滚)。Spring的 @Transactional 注解在这里发挥作用。
异常处理
如果库存不足,Service会主动抛出 BusinessException(自定义业务异常),携带"库存不足"的提示信息。这个异常被全局异常处理器捕获后,返回给前端一个友好的JSON错误,而不是一堆晦涩的堆栈信息。
订单状态管理
订单生成后的状态(待支付、待接单、配送中、已完成、已取消)统一在 OrderConstant 常量类里定义,用数字或枚举表示,避免魔法值散落在代码各处。
定时任务——清理超时订单
用户下了单但一直没支付怎么办?Task 定时任务模块用 @Scheduled 注解每天凌晨扫描订单表,把超过30分钟未支付的订单自动取消并回补库存。这就像餐厅的自动清理机制,不用人工干预。
第四章 支付和实时通知——用户等待阶段
用户支付成功后,希望立刻知道"商家接单了""骑手出发了"。
WebSocket实时推送
这里用到 socket/ws 扩展模块。它建立了一个WebSocket长连接,相当于餐厅和顾客之间拉了一根专用传呼线。OrderService在订单状态变更时,通过WebSocket服务端主动向前端推送消息:"您的订单已接单,预计30分钟送达"。用户不用刷新页面就能实时收到通知,体验更好。
JWT令牌贯穿全程
用户登录成功后,JwtUtil 工具类生成一个JWT令牌,包含用户ID和过期时间(时长由 JwtConstant 配置)。这个令牌后续每次请求都带在Header里,拦截器用JwtUtil解析验证。令牌过期后需要重新登录,保证安全性。
第五章 用户上传头像或评价——文件处理阶段
用户想换个头像,或者给菜品上传评价图片。
请求来到 UserController 的上传接口,Controller调用 AliOSSUtil(阿里云OSS工具类)。这个工具类把前端传过来的MultipartFile上传到阿里云对象存储,返回一个公开访问的图片URL存到数据库。上传所需的AccessKey、Bucket等信息由 OSSConfig 配置类提供。
如果上传过程中网络超时或文件过大,异常会被全局异常处理器捕获,返回"上传失败,请重试"这样的友好提示。
第六章 运营后台管理——商家视角
商家登录后台管理系统(admin端),可以管理菜品、处理订单、查看数据统计。
后台接口和用户接口分离,路径前缀通常是 /api/admin/xxx。后台Controller里同样有参数校验、Service调用、统一返回。不同的是,后台的操作权限更严格——比如只有管理员才能上架/下架菜品。
登录用户上下文同样使用ThreadLocal存储当前操作的管理员ID,用于记录 create_user 和 update_user 字段,做到操作留痕,方便事后审计追责。
第七章 接口文档和测试——质量保障
项目集成 SwaggerConfig,启动后自动扫描所有Controller生成在线API文档。前端同事可以直接在浏览器里调试接口,不用写Mock数据,也不用反复问后端"这个接口返回什么字段"。
test目录 下有Controller和Service的单元测试,使用JUnit + MockMvc模拟HTTP请求。每次代码改动后跑一遍测试,确保核心接口没被改坏,相当于自动化的试吃员。
第八章 打包部署——把餐厅搬到线上
开发完成后,Maven执行 mvn clean package,根据pom.xml配置把项目编译、打包成一个可执行的 JAR包(放在target目录)。这个JAR包包含了所有依赖、配置和编译后的class文件,扔到服务器上用 java -jar 就能启动。
部署到生产环境时,通过 --spring.profiles.active=prod 参数激活 application-prod.yml,自动连接生产数据库和Redis,日志级别调整为INFO,减少不必要的日志输出。
整个项目需要的数据库连接、Redis地址等敏感信息可以通过环境变量注入,避免硬编码在配置文件里,保障生产环境安全。
一张表看懂所有文件夹:
写在最后
从用户点开APP到外卖送到家,一个看似简单的流程,在代码世界里要经过拦截器验身、控制器接单、服务层调度、持久层取数、缓存加速、异常兜底、定时清理、实时推送等几十个环节的精密配合。
这个SpringBoot单体项目虽然"单体",但分层清晰、职责分明。每一层各司其职,每个文件都有自己的使命。理解了这个流程,你就不仅知道了"某个文件是干什么的",更明白了"为什么需要它"——因为用户的一次点击,就是一次横跨整个技术栈的旅行。