

前言:由于前几天忙着开始学习算法以及学校的一些事情(旷课被老师逮到了),因此关于苍穹外卖的项目总结就搁置了几天,从今天开始项目的总结,由于我是第一次写这种项目,我会把在学习过程的所有踩坑点都罗列出来,适合新手入门以及初学者的总结。

苍穹外卖是一套完整的线上外卖点餐解决方案,覆盖用户端、商家管理端两大核心端口,兼顾订单交易、支付结算、数据统计、实时通知等全流程业务,是贴合企业实际开发场景的前后端分离实战项目。
从整体架构来看,项目分为三大板块:业务功能模块、后端技术体系、数据存储与部署模块,各板块分工明确、衔接紧密,既保证了业务流程的顺畅运行,也兼顾了系统的稳定性、并发能力和可维护性。
为什么先说项目的环境搭建,因为这些是项目的运行基础,如何导入源代码等等,其次就是让我们从宏观来看这个项目,从整体把握,明确需求。 开发环境: 是开发人员日常开发、调试代码的环境,核心目标是便捷开发、快速测试,无需追求高并发和高可用,重点保障开发效率。部署步骤贴合本地开发场景:首先搭建本地开发环境,安装JDK(推荐1.8版本)、MySQL(5.7或8.0版本)、Redis、Maven等基础依赖,配置环境变量;其次导入项目源码,修改配置文件(数据库连接、Redis地址、OSS配置等),确保本地服务能正常启动;最后启动前端项目(小程序、管理后台),对接后端接口,完成本地开发调试。 测试环境 是测试人员验证功能、排查Bug的环境,环境配置需尽量贴近生产环境,确保测试结果的准确性,避免“开发环境正常、生产环境异常”的问题。部署步骤:搭建测试服务器(可采用单机或简易集群),安装与生产环境一致版本的依赖(JDK、MySQL、Redis等);部署后端服务,通过Maven打包成jar包,上传至测试服务器,配置启动脚本;部署前端项目(小程序体验版、管理后台测试版),对接测试环境接口;导入测试数据(模拟用户、菜品、订单等数据),供测试人员开展功能测试、压力测试、兼容性测试。 生产环境 是项目正式对外提供服务的环境,核心要求是高可用、高并发、数据安全,部署流程相对复杂,需做好全方位的保障。部署步骤:首先搭建生产服务器集群(避免单机故障导致服务中断),配置负载均衡(如Nginx),实现请求分发;其次部署后端服务,采用多实例部署,通过Docker容器化部署更便捷(统一环境、简化部署),配置服务监控(如SpringBoot Admin),实时监控服务运行状态;部署前端项目,将小程序正式上线、管理后台部署至Nginx,配置静态资源缓存,提升访问速度;数据库采用主从复制,实现数据备份,防止数据丢失;Redis采用集群部署,保证缓存服务的稳定性,同时做好数据持久化配置;最后配置防火墙、安全组,限制非法访问,保障系统安全,完成上线前的最终测试,确认无误后正式对外提供服务。

在这里主要就是三个类,Controller,Service,Mapper,这三个层面相互作用实现的。 Cotroller层分析: Controller不负责业务逻辑的实现,它主要是接收前端的参数,本质是:前端通过不同方式把参数传给后端,SpringMVC通过注解自动解析,封装成java对象供业务使用。
前端传参的方式,主要有三种: 路径参数:直接写在 URL 路径中,属于网址一部分,用于传递 ID。 查询参数:放在 URL ? 后面,用于分页、筛选、查询。 JSON 参数:不放在网址里,放在请求体中,用于登录、新增、修改等敏感或复杂数据提交。 在前后端分离项目中,GET 请求通常使用路径参数或查询参数,参数会拼接在 URL 中,可见且长度有限;而 POST、PUT 等提交数据的请求,会将参数放在 请求体(Request Body) 中,以 JSON 格式传递,不会暴露在 URL 地址栏,安全性更高,也适合传递复杂对象,如登录信息、新增菜品信息、订单信息等。
路径参数 / 占位符
场景:根据 id 查询、删除 注解:@PathVariable
逻辑:把 URL 里的 {id} 取出来,赋值给方法参数 id。
@DeleteMapping("/dish/{id}")
public Result delete(@PathVariable Long id) {
}URL 查询参数(?key=value)
场景:分页、条件查询注解:@RequestParam(可省略)
逻辑:自动获取 URL 问号后面的参数,按名称匹配,赋值给变量或对象。
这里是通过Spring自动封装实现的
@GetMapping("/employee/page")
public Result page(EmployeePageQueryDTO queryDTO) {
}JSON 格式参数(最重要、最通用)
场景:登录、新增、修改、下单等注解:@RequestBody
逻辑:
dto.getXxx() 获取@PostMapping("/login")
public Result login(@RequestBody EmployeeLoginDTO dto) {
}SpringMVC 接收参数的完整通用流程 前端发起请求(GET/POST/PUT/DELETE) 请求到达 DispatcherServlet 前端控制器 根据映射找到对应 Controller 方法
@PathVariable → 解析路径@RequestParam → 解析 URL 参数@RequestBody → 解析 JSON 到对象通用的规则是:
@PathVariable@RequestBody项目中四层数据的封装 Entity(实体类)
DTO(数据传输对象)
@RequestBody 使用@RequestBody 解析思想:解耦前端不需要知道数据库表结构后端也不会把实体类(Entity)直接暴露给前端 VO(视图对象)
Result(统一返回结果)
总结: 在苍穹外卖项目中,通过 DTO、VO、Entity、Result 四层数据封装,实现了前后端交互的标准化与安全性。Entity 负责与数据库交互,保证数据持久层结构清晰;DTO 用于接收前端请求参数,实现按需传参、避免冗余;VO 用于封装响应数据,屏蔽敏感信息、精简返回内容;Result 作为统一返回对象,确保所有接口格式一致,降低前端处理成本,同时提升系统安全性、可维护性与协作效率。
我们怎么知道这些参数的格式呢,这时我们就需要从接口文档中去查询
接口文档:
苍穹外卖使用的是 Knife4j,它是基于 Swagger 封装的增强版接口文档工具,只要项目启动,就能自动生成在线接口文档,不用自己手写。
接口文档在哪里?怎么打开?
1. 先保证这些都启动
2. 访问地址(直接浏览器打开)
plaintext
http://localhost:8080/doc.htmlhttp://localhost:8088/doc.html打开这个地址,看到接口管理页面,就是项目接口文档
每个接口会显示:
/admin/employee/login3. 点开一个接口,能看到什么?(最重要)
点开任意接口后,会显示三块内容:
① 请求参数(前端传给后端)
如果是 Query 参数(?key=value),会显示参数名、是否必填
如果是 JSON 参数(Body 请求体),会显示完整结构比如登录接口会显示:
json
{
"username": "string",
"password": "string"
}这就是前面说的:DTO 长什么样、前端要传什么。
② 响应参数(后端返回给前端)
会显示:
比如登录接口会返回:
json
{
"code": 200,
"msg": "success",
"data": {
"id": 1,
"username": "admin",
"name": "管理员",
"token": "xxxxxxxx"
}
}这就是 Result 统一封装 + VO 返回对象。
③ 在线调试
可以直接在页面上输入参数,点 “发送”,就能调用接口,不用 Postman,非常方便。
接口文档和我们前面学的知识怎么对应?
我们可以这样理解:
总结:
苍穹外卖项目通过 Knife4j 自动生成接口文档,访问地址为 /doc.html。文档中清晰展示了每个接口的请求方式、请求参数、响应格式与在线调试功能,便于前后端对接与接口测试,同时直观体现了 DTO 参数接收、VO 数据返回、Result 统一封装等项目设计思想。
Service层的实现: Service 层 = 业务逻辑层它是项目的核心大脑,专门处理业务规则、流程控制、数据组装,不负责接收请求、不负责操作数据库,只专注 “业务该怎么做”。
Service 层主要做哪些事
为什么要有 Service 层?不能直接 Controller 写?
@Transactional 即可控制事务Service 层常用核心技术
总结: Service 层是项目的业务核心,负责处理复杂业务逻辑、数据校验、事务控制与数据转换,实现了业务与控制层、数据层的解耦,让项目结构更清晰、更易于维护和扩展,是企业级开发中标准且必备的分层设计。
作用:标记这是一个控制器,并且所有方法直接返回 JSON,不用页面跳转。
等价于:@Controller + @ResponseBody
用在哪:所有 Controller 类上面
@RestController
@RequestMapping("/admin/employee")
public class EmployeeController {
}作用:给Controller 或接口方法绑定访问路径(URL 地址)。
常用衍生注解:
@GetMapping 查询@PostMapping 新增、提交@PutMapping 修改@DeleteMapping 删除@PostMapping("/login")
public Result login(@RequestBody EmployeeLoginDTO dto) {
}作用:标记这是业务逻辑层,交给 Spring 管理。
用在哪:ServiceImpl 实现类上
@Service
public class EmployeeServiceImpl implements EmployeeService {
}作用:标记这是 MyBatis 的数据访问层,用来操作数据库。
用在哪:Mapper 接口上
@Mapper
public interface EmployeeMapper {
}作用:自动装配,由 Spring 自动创建对象并注入,不用自己 new。
用在哪:Controller 注入 ServiceService 注入 Mapper
@Autowired
作用:自动装配,由 Spring 自动创建对象并注入,不用自己 new。
用在哪:Controller 注入 ServiceService 注入 Mapper作用:接收前端传来的 JSON 格式参数,封装到 DTO 对象。
用在哪:Controller 方法参数(很重要,容易遗漏)
public Result login(@RequestBody EmployeeLoginDTO dto)作用:接收 路径参数,比如 /dish/123 里的 123。
@DeleteMapping("/dish/{id}")
public Result delete(@PathVariable Long id)作用:开启事务,方法内多次数据库操作要么都成功,要么都回滚。
用在哪:Service 层复杂业务方法上
@Transactional
public void submitOrder(OrdersSubmitDTO dto) {
}作用:Lombok 自动生成:getter、setter、toString、equals、hashCode
用在哪:DTO、VO、Entity 类上
@Data
public class EmployeeLoginDTO {
}作用:自动创建日志对象 log,用来打印日志。
@Slf4j
@Service
public class EmployeeServiceImpl {
}
log.info("员工登录:{}", username);作用:全局异常处理,统一捕获异常并返回友好提示。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public Result exceptionHandler(Exception ex) {
return Result.error(ex.getMessage());
}
}在苍穹外卖项目中,通过一系列 Spring 及第三方注解实现了分层开发与自动化管理:@RestController 负责接收前端请求----@Service 处理业务逻辑-----@Mapper 操作数据库----@Autowired 实现依赖注入---@Transactional 保证事务安全--- 配合 @RequestBody----@PathVariable 接收参数----@Data、@Slf4j 简化代码,以及全局异常处理,让整个项目结构清晰、开发高效、易于维护。
一、Mapper 层是干嘛的 Mapper 层也叫 DAO 层、数据访问层。它的任务只有一个:和数据库打交道,专门做增删改查(CRUD)。 它不关心业务、不处理请求,只负责:从数据库查数据、把数据写入数据库。
select * from ...
insert into ...
update ... set
delete from ...
它只做数据库操作,不做任何业务判断。
二、在项目结构中的位置
Controller 层(接收请求)
↓
Service 层(业务逻辑)
↓
Mapper 层(操作数据库)
↓
MySQL 数据库三、Mapper 层用什么技术 项目中使用:
@Mapper 注解
苍穹外卖里两种都常见:
@Select
XML 里
四、Mapper 层长什么样?
1. 接口类(用 @Mapper 标记)
java
运行
@Mapper
public interface EmployeeMapper {
// 根据用户名查询员工
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
// 新增员工
void insert(Employee employee);
}!!!@Mapper:让 MyBatis 生成代理对象,交给 Spring 管理!!!常见的复杂sql,初学者对这些可能比较头疼
动态条件查询(<if> 标签最常用)
根据前端传的参数动态拼接 WHERE 条件。
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>动态更新(<set> + <if>)
只更新前端传了的字段,没传的不动。
<update id="update">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>多表联查(一对一、一对多)
菜品 + 分类联查、订单 + 订单明细联查最常见。
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>批量操作(<foreach>)
批量删除、批量插入、批量状态修改。
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>分页查询
配合 PageHelper 或手写分页
<select id="pageQuery" resultType="com.sky.entity.Orders">
select * from orders
<where>
<if test="status != null">and status = #{status}</if>
</where>
order by order_time desc
</select>嵌套结果(一对多)
订单主表 + 订单明细表联合查询,返回一个 VO 对象。
<resultMap id="OrderWithDetailsMap" type="com.sky.vo.OrderVO">
<id column="order_id" property="id"/>
<result column="order_number" property="orderNumber"/>
<collection property="orderDetails" ofType="com.sky.entity.OrderDetail">
<id column="detail_id" property="id"/>
<result column="name" property="name"/>
<result column="number" property="number"/>
</collection>
</resultMap>
<select id="getOrderWithDetails" resultMap="OrderWithDetailsMap">
select o.*, od.*
from orders o
left join order_detail od on o.id = od.order_id
where o.id = #{id}
</select><select>:查询
<insert>:新增
<update>:修改
<delete>:删除
<where>:智能拼接条件,自动去掉多余 and
<set>:动态更新,自动去掉多余逗号
<if>:条件判断
<foreach>:循环,批量操作
<resultMap>:结果映射,多表、一对多
<sql> + <include>:抽取重复 SQL 片段复用
在苍穹外卖项目中,Mapper XML 文件主要用于编写复杂 SQL 语句,包括动态条件查询、动态更新、多表联查、批量操作与一对多嵌套查询等。通过 <where>、<if>、<set>、<foreach>、<resultMap> 等标签,实现灵活、可维护的数据访问逻辑,尤其适用于分页查询、条件搜索、订单详情组装等复杂业务场景,使数据层结构清晰且易于扩展。

小程序端整体定位
小程序端技术栈
wx.request / 封装统一 requestwx.setStorageSync 保存 token、用户信息关于具体的相关操作,可以查看前几天的文章。 苍穹外卖用户端基于微信小程序开发,面向普通消费者提供完整的外卖点餐服务。主要包括微信授权登录、首页菜品展示、购物车、地址管理、订单提交、微信支付及订单查询等核心功能。小程序通过统一封装的网络请求与后端交互,携带 JWT 令牌进行身份认证,接口遵循 RESTful 规范,返回统一格式数据。整体采用模块化开发,页面结构清晰,交互流程流畅,实现了从点餐到支付再到订单跟踪的完整闭环。

SpringBoot —— 自动配置、简化开发的核心 SpringBoot 的作用:
@SpringBootApplication 启动类一键运行关键特点:
在项目里体现:
SkyTakeoutApplicationapplication.yml 统一配置@Service、@RestController 被 Spring 管理SpringMVC —— 处理 HTTP 请求的 Web 框架 面试常问: Spring 是一个通用的企业级应用框架,核心是 IOC 容器和 AOP,负责管理项目中所有 Bean 的生命周期、依赖关系、事务等。SpringMVC 是 Spring 框架的一个 Web 模块,基于 Spring 实现,专门用于处理 HTTP 请求、控制器映射、参数接收与响应返回。两者是父子容器关系:Spring 是父容器,管理 Service、Mapper 等业务组件;SpringMVC 是子容器,管理 Controller、拦截器等 Web 组件。SpringMVC 依赖 Spring 运行,二者共同构成了 Java Web 开发的经典架构。
地位:前后端交互的入口 SpringMVC 负责:
@RequestBody、@RequestParam、@PathVariable核心组件:
DispatcherServlet 前端控制器(统一入口)Controller 处理器在项目里体现:
@RestController@GetMapping、@PostMappingResult 拦截器是 SpringMVC 框架提供的扩展组件,依附于 MVC 体系运行,作用是在请求进入 Controller 前后进行统一拦截与处理。在项目中,拦截器常用于 JWT 登录校验、接口权限控制、日志记录等场景。通过实现 WebMvcConfigurer 接口将拦截器注册到 MVC 容器,使其自动加入请求处理链路,与 SpringMVC 协同工作。
MyBatis —— 持久层框架,操作数据库 地位:连接 MySQL 的桥梁 MyBatis 负责:
为什么mapper是一个接口不需要实现类?
@Mapper 被 MyBatis 扫描@Autowired 直接注入使用在项目里体现:
@Mapper 接口<if>、<where>、<set>、<foreach>Spring Cache 是 Spring 提供的声明式缓存框架,作用:减少数据库查询,提高接口速度。
不用自己操作 Redis,直接用注解就能缓存数据。
核心注解(项目里最常用)
1. @Cacheable
触发查询:先查缓存,没有再查数据库,并自动存入缓存常用于:查询接口
@Cacheable(value = "dish", key = "#categoryId")
public List<Dish> getByCategoryId(Long categoryId) {
return dishMapper.selectByCategoryId(categoryId);
}@CacheEvict
清理缓存新增、修改、删除数据时,把旧缓存删掉,保证数据一致
@CacheEvict(value = "dish", allEntries = true)
public void save(DishDTO dishDTO) {
}@CachePut
更新缓存(很少用)
底层用什么存?
Spring Cache 只是规范,真正存储一般用:
Spring Cache 让缓存注解化,不用手动写 Redis 操作,查询自动缓存,更新自动清理,既简单又高效。
Spring 自带的 轻量级定时任务框架,作用:让程序在指定时间自动执行某段代码。
外卖项目里最经典用途:取消超时未支付订单
1. 启动类加开启定时任务
@EnableScheduling2. 写定时任务类 + @Scheduled
@Component
public class OrderTask {
// 每分钟执行一次
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder() {
// 查询超时订单 → 取消订单 → 恢复库存
}
}cron 表达式简单理解
秒 分 时 日 月 周
例子:
0 0 12 * * ? → 每天 12 点执行0 0/5 * * * ? → 每 5 分钟0 * * * * ? → 每分钟执行一次外卖项目里的作用
总结:
@Cacheable、@CacheEvict 实现自动缓存与清理,结合 Redis 大幅降低数据库压力,提升接口响应速度。
@Scheduled 配置 cron 表达式实现定时执行,常用于订单超时取消、订单统计、数据清理等自动化任务。

一、项目为什么要用 Redis?
二、Redis 在项目中的四大核心用途
1️⃣ 缓存菜品、分类数据(提高查询速度)
这是 Redis 最主要用途。
核心注解:
@Cacheable(value = "dishCache", key = "#categoryId")@CacheEvict(value = "dishCache", allEntries = true)存储微信小程序购物车
购物车不存 MySQL,只存在 Redis。
cart_用户id核心操作:
opsForValue().set()opsForValue().get()delete()存储验证码(手机验证码)
验证码类型_手机号
特点:
缓存订单、状态、分布式锁(扩展场景)
部分版本中用于:
在苍穹外卖项目中,Redis 作为高性能内存数据库,承担了缓存、临时存储、高并发支撑等核心任务。主要用于菜品数据缓存,大幅减轻数据库压力并提升首页加载速度;通过 Redis 存储微信小程序购物车,实现快速读写与数据共享;同时用于存储短信验证码并设置自动过期,保证安全性。结合 Spring Cache 实现声明式缓存,通过注解完成缓存读写与清理,简化开发。Redis 的使用显著提升了系统并发能力与响应速度,是外卖项目高并发场景下的关键中间件。
WebSocket 是一种全双工、长连接的网络通信协议。
一句话:HTTP 是发短信,WebSocket 是打电话。
为什么外卖项目必须用 WebSocket?
场景:
用户小程序下单支付成功 → 商家后台要立刻收到语音播报:您有新的订单
如果用 HTTP:
用 WebSocket:
苍穹外卖里 WebSocket 到底做什么?
项目里只有一个核心场景:
订单来了 → 实时推送给商家后台
额外扩展场景(部分版本):
真实业务流程(最重要)
用户小程序下单
↓
订单入库(MySQL)
↓
支付成功
↓
Service 层调用:
WebSocketServer.sendToUser(商家ID, "新订单来了")
↓
服务器主动推送消息
↓
商家后台网页收到消息
↓
自动播放语音提示1. 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>2配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}3. 核心服务类
@ServerEndpoint("/ws/{userId}")
@Component
public class WebSocketServer {
private static Map<Long, Session> sessionMap = new ConcurrentHashMap<>();
// 建立连接
@OnOpen
public void onOpen(Session session, @PathParam("userId") Long userId) {
sessionMap.put(userId, session);
}
// 收到消息
@OnMessage
public void onMessage(String message) {}
// 关闭连接
@OnClose
public void onClose(@PathParam("userId") Long userId) {
sessionMap.remove(userId);
}
// 发送消息给指定用户
public static void sendToUser(Long userId, String message) {
Session session = sessionMap.get(userId);
if (session != null) {
session.getBasicRemote().sendText(message);
}
}
}WebSocket 和 HTTP 的区别(面试常问)
HTTP | WebSocket | |
|---|---|---|
连接 | 短连接,一次请求就断 | 长连接,一直保持 |
通信方向 | 前端→服务器 | 双向随意发 |
实时性 | 差,需要轮询 | 极高,实时推送 |
开销 | 高 | 低 |
适用场景 | 接口请求、查询 | 消息推送、实时通知 |
总结:
WebScket是苍穹外卖项目中的实现实时消息推送的技术,基于长连接实现双向通信,解决了传统HTTP无法主动推送的问题。在项目中主要用于用户下单后,实时向商家后台推送新订单通知,并配合语音播报提升接单效率。通过建立持久连接,服务器可主动向前端发送消息,具有低延迟、高实时性、低资源消耗的特点,是外卖订单系统必不可少的通信技术。
OSS = 阿里云对象存储服务简单说:专门存在网上的 “图片 / 文件网盘”用来存图片、文件、视频等,不占项目服务器硬盘。
项目里有大量图片:
如果存在项目本地:
所以用 阿里云 OSS 专门存图片。
OSS 在项目里到底干嘛
只有一个核心业务:
图片上传 + 图片访问
流程:
使用 OSS 的完整步骤(项目真实流程)
1. 阿里云开通 OSS
2. 引入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>配置 yml
sky:
oss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAIxxxxxx
access-key-secret: xxxxxxxx
bucket-name: sky-takeout工具类:
@Component
public class AliOssUtil {
@Value("${sky.oss.endpoint}")
private String endpoint;
@Value("${sky.oss.access-key-id}")
private String accessKeyId;
@Value("${sky.oss.access-key-secret}")
private String accessKeySecret;
@Value("${sky.oss.bucket-name}")
private String bucketName;
public String upload(String fileName, InputStream inputStream) throws Exception {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
ossClient.shutdown();
return url;
}
}Controller接受上传:
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws Exception {
String url = aliOssUtil.upload(file.getOriginalFilename(), file.getInputStream());
return Result.success(url);
}保存 URL 到数据库
菜品表 dish 中的 image 字段存的就是 OSS 地址。
一张图看懂上传流程
前端选择图片
↓
Controller 接收 MultipartFile
↓
调用 AliOssUtil 上传到 OSS
↓
OSS 返回图片 URL
↓
URL 存入 MySQL
↓
小程序/后台直接访问 URL 显示图片为什么图片存在 OSS,不存数据库
总结:
阿里云 OSS 是苍穹外卖项目中用于图片存储的核心第三方服务,主要负责菜品图片、套餐图片、用户头像的上传与访问。通过 OSS 存储图片,避免了图片占用服务器磁盘空间,解决了分布式部署下图片无法共享的问题,同时借助阿里云的 CDN 加速,提升图片加载速度。后端通过封装工具类实现图片上传,并将返回的 URL 存入数据库,前端直接通过 URL 加载图片,整个流程简洁、高效、稳定,是外卖项目中标准的文件存储方案。

JWT = JSON Web Token一种轻量级、可自包含、防篡改的令牌格式,用来做登录认证。
特点:
三段式字符串,用 . 分隔:
aaaaa.bbbbb.ccccc
头部.载荷.签名1. 头部(Header)
2. 载荷(Payload)——最重要
放要传递的信息:
苍穹外卖里核心就是存:userId
3. 签名(Signature)
服务端用密钥签名,防止篡改。
JWT 完整流程(苍穹外卖真实流程)
1. 员工/用户输入账号密码 → 登录
2. 校验通过 → 生成 JWT,把 userId 放进载荷
3. 返回 token 给前端
4. 前端存在本地,每次请求头带上:Authorization: Bearer token
5. 拦截器拦截请求 → 解析 token → 拿到 userId
6. 把 userId 存入 ThreadLocal,方便后面 Controller/Service 获取
7. 接口直接取用当前登录人 ID四、生成 JWT(登录时)
工具类里写一个方法,把 userId 放进去:登录成功后返回给前端。
public static String generateToken(Long userId) {
return Jwts.builder()
.setClaim("userId", userId) // 把用户ID存进去
.setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000))
.signWith(SignatureAlgorithm.HS256, SIGN_KEY)
.compact();
}拦截器解析 JWT,拿到 userId
前端请求 → 拦截器 preHandle:
Claims claims = JwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());ThreadLocal = 线程本地变量,同一个请求线程内随处可取值,不用层层传参。
一个请求从头到尾是同一个线程:
都在一个线程里,所以可以:
苍穹外卖里的写法:
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}拦截器:
BaseContext.setCurrentId(userId);Controller / Service 中直接获取当前登录人 ID
任何地方,直接一句:
Long currentId = BaseContext.getCurrentId();比如:
不用传参、不用注解、随处可拿。
请求结束一定要 remove!
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
BaseContext.remove();
}整套逻辑串起来
用户登录
↓
生成 JWT,存入 userId
↓
前端携带 token 访问接口
↓
拦截器解析 token → 拿到 userId
↓
存入 ThreadLocal
↓
Controller/Service 直接 BaseContext.getCurrentId() 获取
↓
请求结束,清除 ThreadLocal为什么要用 JWT + ThreadLocal
总结:
JWT 是一种轻量级的无状态认证令牌,在苍穹外卖项目中用于员工与用户的登录认证。服务端在登录成功后生成 JWT,并将用户 ID 存入载荷;前端每次请求在请求头携带令牌,拦截器负责解析令牌获取用户 ID,并通过 ThreadLocal 线程本地变量进行存储,使得后续 Controller、Service 层可以随时随地获取当前登录人 ID,避免了层层传递参数。最后在请求完成后清除 ThreadLocal 数据,保证线程安全与内存安全。整套方案简洁、安全、适合分布式前后端分离架构。
Knife4j = 美化 + 增强版的 Swagger作用只有一个:自动生成接口文档,让前端、测试直接看接口、测接口
不用你手写文档,也不用 Postman 到处测。
它在项目里干嘛
@RestController 接口全部展示出来
为什么要用它
项目里怎么用
1. 引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>配置类:
@Configuration
@EnableKnife4j
public class Knife4jConfig {
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(...)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
}
}启动项目,访问地址
http://localhost:8080/doc.html常用注解(写在接口上)
1. @Api 加在 Controller 上
给模块起名字
@Api(tags = "员工管理接口")
@RestController
@RequestMapping("/admin/employee")2. @ApiOperation 加在方法上
说明接口功能
@ApiOperation("员工登录接口")
@PostMapping("/login")3. @ApiModel / @ApiModelProperty 加在 DTO/VO 上
给字段加注释
@Data
@ApiModel("员工登录DTO")
public class EmployeeLoginDTO {
@ApiModelProperty("用户名")
private String username;
}和 Swagger 区别
所以苍穹外卖用的是 Knife4j。
Knife4j 是基于 Swagger 增强的接口文档生成工具,能够自动扫描项目中的 Controller 接口,生成可视化、可在线调试的 API 文档。通过简单注解即可描述接口功能与参数含义,极大提升前后端协作效率,方便开发调试与接口测试,是苍穹外卖项目中前后端对接的重要工具。
AOP = 面向切面编程简单理解:不修改原来的代码,就能给方法统一加功能。
比如:
这些与业务无关,但每个方法都需要的功能,就叫横切逻辑。AOP 就是把它们抽出来,做成一个 “切面”。
AOP 核心术语
@Before 之前
@After 之后
@Around 前后都加(最强大)
@AfterReturning 返回后
@AfterThrowing 抛异常后
苍穹外卖里 AOP 用在哪
简单示例:接口日志切面
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("execution(* com.sky.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
// 执行前
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
// 执行后
long end = System.currentTimeMillis();
log.info("耗时:{}ms", end - start);
return result;
}
}@Aspect
@Component
@Slf4j
public class LogAspect { // 单独的类
// 切点:给哪些方法增强
@Pointcut("execution(* com.sky.service.*.*(..))")
public void pt(){}
// 环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// ... 日志逻辑 ...
}
}保证多个数据库操作同时成功 or 同时失败。
典型场景:下单 → 扣库存 → 生成订单 → 扣减余额任何一步失败,全部回滚。
Spring 事务底层就是 AOP
@Transactional
本质 = AOP 环绕通知执行流程:
3. 苍穹外卖必用事务的场景
这些必须保证要么全成,要么全失败。
4. 基本使用
@Service
public class OrderServiceImpl implements OrderService {
@Override
@Transactional // 开启事务
public OrderVO submitOrder(OrdersSubmitDTO dto) {
// 1. 新增订单
orderMapper.insert(order);
// 2. 新增订单明细
orderDetailMapper.batchInsert(details);
// 3. 清空购物车
cartMapper.deleteByUserId(userId);
}
}任意一步抛异常,所有 SQL 全部回滚
事务常用属性
rollbackFor = Exception.class所有异常都回滚(推荐加上)
propagation 传播机制(控制多个事务方法嵌套)
timeout 超时时间
AOP 和事务是什么关系
Spring 事务就是用 AOP 动态代理实现的:给加了 @Transactional 的方法套一层 “开启、提交、回滚” 的增强逻辑。给方法前后加功能。
总结:
AOP 即面向切面编程,是 Spring 核心特性之一,用于在不修改业务代码的前提下,对方法进行统一增强,如日志记录、权限校验、事务管理等。Spring 声明式事务基于 AOP 实现,通过 @Transactional 注解即可为方法开启事务,保证多个数据库操作的原子性:操作全部成功则提交,出现异常则自动回滚。在苍穹外卖项目中,事务主要应用于订单提交、订单支付、取消订单等核心业务,确保数据一致性与业务安全性;AOP 则广泛用于日志记录、接口监控等场景,提升代码复用性与可维护性。
Lombok 是一个 Java 插件工具,作用只有一个:帮你自动生成代码,不用手写 getter/setter/toString/ 构造器…
以前写实体类要写一堆:
private Long id;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }用了 Lombok,一个注解代替所有。
写在实体类 / Service / Controller 上面最常用在:
@Data
public class User {
private Long id;
private String name;
}最常用注解
@Data(最重要、最常用)
自动生成:
实体类 DTO、VO 全部都用它。
@Data
public class DishDTO {
}@Slf4j
自动生成日志对象:
private static final Logger log = LoggerFactory.getLogger(当前类.class);写在 Controller、Service、切面、任务类 上:
@Slf4j
@Service
public class OrderService {
}然后直接用:
log.info("订单创建成功");@NoArgsConstructor
无参构造器
@AllArgsConstructor
全参构造器
@Builder
建造者模式,优雅创建对象
User user = User.builder()
.id(1L)
.name("张三")
.build();它是怎么生效的
为什么项目要用 Lombok
一句话总结
Lombok 是一款通过注解简化 Java 代码的工具库,在苍穹外卖项目中广泛应用于实体类、DTO、VO 以及业务类。通过 @Data 自动生成 getter、setter、toString 等方法,@Slf4j 快速注入日志对象,大幅减少样板代码,让开发更简洁高效,是现代 SpringBoot 项目标配工具。

关于这一部分,我感觉没什么好说的,能正确写好sql语句就可以了,还有一个就是我学的时候有点搞不清的主键id和正常id
主键 ID vs 普通 ID:核心区别 + 用法全解
主键 id和正常 id,本质是:一个是数据库核心主键(唯一、不可改、索引),一个只是业务里叫 “id” 的普通字段
先一句话分清
id、user_id(主键)、pk_id
order_no、user_code、biz_id
核心区别对比表
特性 | 主键 ID(数据库主键) | 普通 ID / 业务 ID |
|---|---|---|
数量 | 一张表只能有 1 个 | 一张表可以有多个 |
唯一性 | 数据库强制唯一 | 可重复(需手动控制) |
非空 | 强制非空 | 可以为空 |
能否修改 | 严禁修改 | 可以随意修改 |
索引 | 自带唯一索引(查询快) | 无索引(需手动加) |
作用 | 数据库内部使用、关联表 | 业务展示、对外接口、用户看 |
暴露风险 | 不能暴露给前端 / 外网 | 专门用来暴露给外部 |
最常见的实际场景
1. 用户表(经典例子)
sql
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT, -- 【主键ID】数据库主键,自增、唯一、不对外暴露
user_code VARCHAR(32) UNIQUE, -- 【普通ID】业务编号,给用户看、给接口用
username VARCHAR(50),
phone VARCHAR(20)
);id = 10086 → 数据库内部用,绝对不能让前端 / 外网看到
user_code = U20250403001 → 业务 ID,暴露给用户、接口、第三方
2. 订单表
order_id (主键,自增) → 数据库内部关联、查询
order_no = 2025040310001 → 用户看到的订单号、客服查询用
为什么要分开
1. 安全(最关键)
2. 解耦
3. 规范
五、简单总结记忆法
总结
Redis缓存设计表:

具体的操作上面有讲解。 项目难点:

大家可以对照着看看,有什么不懂的具体去复习一下,这里再补充一下Nginx反向代理,其实也不敢多说,容易违规。 在 苍穹外卖 项目中,Nginx 反向代理是整个系统的流量入口与核心网关,主要解决前后端分离、跨域、请求转发、动静分离、负载均衡等问题。 一、核心作用(项目场景)
html 目录。
反向代理转发 API 请求(解决跨域)
http://localhost/api/employee/loginhttp://localhost:8080/admin/employee/login
按路径区分双端(管理端 / 用户端)
/api/xxx → 后端管理服务(/admin/xxx)/user/xxx → 后端用户服务(/user/xxx)负载均衡(集群扩展)
upstream 定义多台后端服务器最后,真的是燃尽了,如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!