首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【苍穹外卖实战总结--五万字全面讲解,适合复盘】已燃尽TAT

【苍穹外卖实战总结--五万字全面讲解,适合复盘】已燃尽TAT

作者头像
北极的代码
发布2026-04-22 15:58:33
发布2026-04-22 15:58:33
1570
举报

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


第一部分:项目总览

苍穹外卖是一套完整的线上外卖点餐解决方案,覆盖用户端、商家管理端两大核心端口,兼顾订单交易、支付结算、数据统计、实时通知等全流程业务,是贴合企业实际开发场景的前后端分离实战项目。

从整体架构来看,项目分为三大板块:业务功能模块、后端技术体系、数据存储与部署模块,各板块分工明确、衔接紧密,既保证了业务流程的顺畅运行,也兼顾了系统的稳定性、并发能力和可维护性。


项目的环境搭建:

为什么先说项目的环境搭建,因为这些是项目的运行基础,如何导入源代码等等,其次就是让我们从宏观来看这个项目,从整体把握,明确需求。 开发环境: 是开发人员日常开发、调试代码的环境,核心目标是便捷开发、快速测试,无需追求高并发和高可用,重点保障开发效率。部署步骤贴合本地开发场景:首先搭建本地开发环境,安装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 地址栏,安全性更高,也适合传递复杂对象,如登录信息、新增菜品信息、订单信息等。


Controller对应的三种接收格式(坑点!容易漏掉):

路径参数 / 占位符

场景:根据 id 查询、删除 注解@PathVariable

逻辑:把 URL 里的 {id} 取出来,赋值给方法参数 id。

代码语言:javascript
复制
@DeleteMapping("/dish/{id}")
public Result delete(@PathVariable Long id) {
}

URL 查询参数(?key=value)

场景:分页、条件查询注解@RequestParam(可省略)

逻辑:自动获取 URL 问号后面的参数,按名称匹配,赋值给变量或对象。

这里是通过Spring自动封装实现的

代码语言:javascript
复制
@GetMapping("/employee/page")
public Result page(EmployeePageQueryDTO queryDTO) {
}

JSON 格式参数(最重要、最通用)

场景:登录、新增、修改、下单等注解@RequestBody

逻辑

  • 前端传 JSON
  • SpringMVC 用消息转换器解析
  • 按字段名匹配,自动封装到 DTO 实体类
  • 后端直接 dto.getXxx() 获取
代码语言:javascript
复制
@PostMapping("/login")
public Result login(@RequestBody EmployeeLoginDTO dto) {
}

SpringMVC 接收参数的完整通用流程 前端发起请求(GET/POST/PUT/DELETE) 请求到达 DispatcherServlet 前端控制器 根据映射找到对应 Controller 方法

  1. SpringMVC 根据参数上的注解:
    • @PathVariable → 解析路径
    • @RequestParam → 解析 URL 参数
    • @RequestBody → 解析 JSON 到对象
  2. 把解析后的值赋给方法形参
  3. Controller 拿到参数,调用 Service 执行业务
  4. 最后返回统一结果 Result

通用的规则是:

  • 简单参数、分页参数 → 直接写参数 / 用 DTO 接收,不用注解
  • 路径里的 id@PathVariable
  • JSON 对象(新增、修改、登录)必须用 @RequestBody
  • 参数名要和前端传的 key 一致,否则接收不到(查询参数)!!
  • 所有复杂业务,统一用 DTO 接收,结构清晰、安全、规范

项目中四层数据的封装 Entity(实体类)

  • 全称:实体对象
  • 位置:与数据库表一一对应
  • 作用封装表中所有字段,用于操作数据库
  • 特点
    • 字段最全
    • 包含敏感字段(如密码)
    • 只在后端内部使用,不直接返回前端

DTO(数据传输对象)

  • 全称:Data Transfer Object
  • 流向前端 → 后端
  • 作用接收前端传来的参数
  • 特点
    • 按需定义,字段只多不少
    • 不与数据库表完全对应
    • 配合 @RequestBody 使用
  • 前端传 JSON
  • SpringMVC 通过 @RequestBody 解析
  • 根据 key 名匹配,自动赋值到 DTO 的成员变量
  • Controller 拿到 DTO,传给 Service
  • Service 从 DTO 里取值,再封装到 Entity 操作数据库

思想:解耦前端不需要知道数据库表结构后端也不会把实体类(Entity)直接暴露给前端 VO(视图对象)

  • 全称:View Object
  • 流向后端 → 前端
  • 作用封装要返回给前端的数据
  • 特点
    • 只返回前端需要的字段
    • 屏蔽敏感信息(密码、盐值等)
    • 结构灵活,可组合多表数据

Result(统一返回结果)

  • 作用:给前端的最终包裹
  • 特点
    • 所有接口返回格式完全统一
    • 包含 code、msg、data、success
    • 配合全局异常处理一起使用

总结: 在苍穹外卖项目中,通过 DTO、VO、Entity、Result 四层数据封装,实现了前后端交互的标准化与安全性。Entity 负责与数据库交互,保证数据持久层结构清晰;DTO 用于接收前端请求参数,实现按需传参、避免冗余;VO 用于封装响应数据,屏蔽敏感信息、精简返回内容;Result 作为统一返回对象,确保所有接口格式一致,降低前端处理成本,同时提升系统安全性、可维护性与协作效率。


我们怎么知道这些参数的格式呢,这时我们就需要从接口文档中去查询

接口文档

苍穹外卖使用的是 Knife4j,它是基于 Swagger 封装的增强版接口文档工具,只要项目启动,就能自动生成在线接口文档,不用自己手写。

接口文档在哪里?怎么打开?

1. 先保证这些都启动

  • MySQL 服务启动
  • Redis 服务启动
  • 后端 Sky-takeout 项目正常启动成功

2. 访问地址(直接浏览器打开)

plaintext

代码语言:javascript
复制
http://localhost:8080/doc.html
  • 如果项目端口不是 8080,就换成你自己的端口例如:http://localhost:8088/doc.html

打开这个地址,看到接口管理页面,就是项目接口文档

每个接口会显示:

  • 请求方式:GET / POST / PUT / DELETE
  • 接口地址:如 /admin/employee/login
  • 接口说明:干什么用的

3. 点开一个接口,能看到什么?(最重要)

点开任意接口后,会显示三块内容:

① 请求参数(前端传给后端)

如果是 Query 参数(?key=value),会显示参数名、是否必填

如果是 JSON 参数(Body 请求体),会显示完整结构比如登录接口会显示:

json

代码语言:javascript
复制
{
  "username": "string",
  "password": "string"
}

这就是前面说的:DTO 长什么样、前端要传什么

② 响应参数(后端返回给前端)

会显示:

  • 返回码 code
  • 提示信息 msg
  • 返回数据 data 结构(也就是 VO 结构)

比如登录接口会返回:

json

代码语言:javascript
复制
{
  "code": 200,
  "msg": "success",
  "data": {
    "id": 1,
    "username": "admin",
    "name": "管理员",
    "token": "xxxxxxxx"
  }
}

这就是 Result 统一封装 + VO 返回对象

③ 在线调试

可以直接在页面上输入参数,点 “发送”,就能调用接口,不用 Postman,非常方便。

接口文档和我们前面学的知识怎么对应?

我们可以这样理解:

  1. 接口地址 → 对应 Controller 里的 @GetMapping / @PostMapping
  2. 请求参数 → 对应 DTO 封装
  3. 请求体 JSON → 对应 @RequestBody 接收
  4. 响应结构 → 对应 VO + Result 统一封装
  5. 需要登录的接口 → 对应 JWT 拦截器、员工登录

总结:

苍穹外卖项目通过 Knife4j 自动生成接口文档,访问地址为 /doc.html。文档中清晰展示了每个接口的请求方式、请求参数、响应格式与在线调试功能,便于前后端对接与接口测试,同时直观体现了 DTO 参数接收、VO 数据返回、Result 统一封装等项目设计思想。


Service层的实现: Service 层 = 业务逻辑层它是项目的核心大脑,专门处理业务规则、流程控制、数据组装,不负责接收请求、不负责操作数据库,只专注 “业务该怎么做”。

Service 层主要做哪些事

  1. 接收 Controller 传入的 DTO
  2. 做业务校验
    • 数据是否为空
    • 状态是否合法
    • 权限是否足够
    • 业务规则是否满足
  3. 调用 Mapper 操作数据库
  4. 处理复杂逻辑
    • 多表操作
    • 事务控制
    • 数据转换(DTO → Entity,Entity → VO)
  5. 返回 VO 或数据给 Controller

为什么要有 Service 层?不能直接 Controller 写?

  1. 职责单一Controller 只负责接收和返回,不写复杂业务
  2. 代码复用多个 Controller 可以调用同一个 Service
  3. 便于维护业务改需求只改 Service,不动 Controller
  4. 方便事务管理Service 上加 @Transactional 即可控制事务
  5. 分层清晰,符合企业开发规范

Service 层常用核心技术

  1. @Transactional 事务控制下单、扣库存、支付等多表操作必须加事务
  2. 数据转换DTO → EntityEntity → VO
  3. 异常抛出业务错误抛自定义异常,交给全局异常处理
  4. 调用第三方服务微信支付、OSS、短信、WebSocket 等

总结: Service 层是项目的业务核心,负责处理复杂业务逻辑、数据校验、事务控制与数据转换,实现了业务与控制层、数据层的解耦,让项目结构更清晰、更易于维护和扩展,是企业级开发中标准且必备的分层设计。


关于项目中的相关注解:
@RestController

作用:标记这是一个控制器,并且所有方法直接返回 JSON,不用页面跳转。

等价于:@Controller + @ResponseBody

用在哪:所有 Controller 类上面

代码语言:javascript
复制
@RestController
@RequestMapping("/admin/employee")
public class EmployeeController {
}
@RequestMapping

作用:Controller 或接口方法绑定访问路径(URL 地址)。

  • 通用注解
  • 可加在类上路径前缀
  • 可指定任意 method

常用衍生注解:

  • @GetMapping 查询
  • @PostMapping 新增、提交
  • @PutMapping 修改
  • @DeleteMapping 删除
  • 只能加在方法上
  • 语义明确,一看就知道是查询 / 新增 / 修改 / 删除
  • 代码更简洁
  • 符合 RESTful 风格
代码语言:javascript
复制
@PostMapping("/login")
public Result login(@RequestBody EmployeeLoginDTO dto) {
}
@Service

作用:标记这是业务逻辑层交给 Spring 管理。

用在哪:ServiceImpl 实现类上

代码语言:javascript
复制
@Service
public class EmployeeServiceImpl implements EmployeeService {
}
@Mapper

作用:标记这是 MyBatis 的数据访问层,用来操作数据库。

用在哪:Mapper 接口上

代码语言:javascript
复制
@Mapper
public interface EmployeeMapper {
}
@Autowired

作用:自动装配,由 Spring 自动创建对象并注入,不用自己 new。

用在哪:Controller 注入 ServiceService 注入 Mapper

代码语言:javascript
复制
@Autowired
作用:自动装配,由 Spring 自动创建对象并注入,不用自己 new。
用在哪:Controller 注入 ServiceService 注入 Mapper
@RequestBody

作用:接收前端传来的 JSON 格式参数,封装到 DTO 对象。

用在哪:Controller 方法参数(很重要,容易遗漏)

代码语言:javascript
复制
public Result login(@RequestBody EmployeeLoginDTO dto)
@PathVariable

作用:接收 路径参数,比如 /dish/123 里的 123。

代码语言:javascript
复制
@DeleteMapping("/dish/{id}")
public Result delete(@PathVariable Long id)
@Transactional

作用:开启事务,方法内多次数据库操作要么都成功,要么都回滚。

用在哪:Service 层复杂业务方法上

代码语言:javascript
复制
@Transactional
public void submitOrder(OrdersSubmitDTO dto) {
}
@Data

作用:Lombok 自动生成:getter、setter、toString、equals、hashCode

用在哪:DTO、VO、Entity 类上

代码语言:javascript
复制
@Data
public class EmployeeLoginDTO {
}
@Slf4j

作用:自动创建日志对象 log,用来打印日志。

代码语言:javascript
复制
@Slf4j
@Service
public class EmployeeServiceImpl {
}
log.info("员工登录:{}", username);
@RestControllerAdvice + @ExceptionHandler

作用:全局异常处理,统一捕获异常并返回友好提示。

代码语言:javascript
复制
@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 层是干嘛的 Mapper 层也叫 DAO 层、数据访问层。它的任务只有一个:和数据库打交道,专门做增删改查(CRUD)。 它不关心业务、不处理请求,只负责:从数据库查数据、把数据写入数据库。

  1. 查询数据select * from ...
  2. 新增数据insert into ...
  3. 修改数据update ... set
  4. 删除数据delete from ...
  5. 分页、统计、多表关联

它只做数据库操作,不做任何业务判断。


二、在项目结构中的位置

代码语言:javascript
复制
Controller 层(接收请求)
    ↓
Service 层(业务逻辑)
    ↓
Mapper 层(操作数据库)
    ↓
MySQL 数据库
  • Controller:接客
  • Service:干活
  • Mapper:管数据库

三、Mapper 层用什么技术 项目中使用:

  • MyBatis / MyBatis-Plus
  • 接口 + @Mapper 注解
  • 要么写 XML 映射文件,要么用 注解 SQL

苍穹外卖里两种都常见:

  • 简单 SQL:用注解 @Select
  • 复杂 SQL:写在 XML

四、Mapper 层长什么样?

1. 接口类(用 @Mapper 标记)

java

运行

代码语言:javascript
复制
@Mapper
public interface EmployeeMapper {

    // 根据用户名查询员工
    @Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);

    // 新增员工
    void insert(Employee employee);
}
  • !!!@Mapper:让 MyBatis 生成代理对象,交给 Spring 管理!!!
  • 方法名随意,见名知意
  • 参数一般是实体类或简单类型

XML 映射文件(复杂 SQL 写这里)

常见的复杂sql,初学者对这些可能比较头疼

动态条件查询(<if> 标签最常用)

根据前端传的参数动态拼接 WHERE 条件

代码语言:javascript
复制
<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>

只更新前端传了的字段,没传的不动。

代码语言:javascript
复制
<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>

多表联查(一对一、一对多)

菜品 + 分类联查、订单 + 订单明细联查最常见。

代码语言:javascript
复制
<delete id="deleteByIds">
    delete from dish where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

批量操作(<foreach>

批量删除、批量插入、批量状态修改。

代码语言:javascript
复制
<delete id="deleteByIds">
    delete from dish where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

分页查询

配合 PageHelper 或手写分页

代码语言:javascript
复制
<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 对象。

代码语言:javascript
复制
<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> 等标签,实现灵活、可维护的数据访问逻辑,尤其适用于分页查询、条件搜索、订单详情组装等复杂业务场景,使数据层结构清晰且易于扩展。

用户端小程序开发:

小程序端整体定位

  • 用户端(微信小程序):给普通顾客使用
  • 功能:点餐、加购物车、下单、支付、查看订单、地址管理
  • 管理后台是两个独立端,共用同一个后端接口

小程序端技术栈

  • 框架:微信原生小程序(WXML + WXSS + JS + JSON)
  • 网络请求:wx.request / 封装统一 request
  • 登录:微信授权登录(code → openid)
  • 支付:微信支付
  • 缓存:wx.setStorageSync 保存 token、用户信息

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

后端技术栈:


SpringBoot —— 自动配置、简化开发的核心 SpringBoot 的作用:

  • 简化 SSM 繁杂的 XML 配置
  • 自动配置 Tomcat、Spring、MyBatis、Redis 等组件
  • 通过 @SpringBootApplication 启动类一键运行
  • 管理所有 Bean(Controller、Service、Mapper)
  • 整合事务、日志、AOP 等功能

关键特点:

  • 约定大于配置
  • 起步依赖(starter),引入依赖即集成组件
  • 内置 Tomcat,无需单独部署服务器

在项目里体现:

  • 启动类 SkyTakeoutApplication
  • application.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 开发的经典架构。

  • Spring 是核心容器,管理所有 Bean。
  • SpringMVC 是 Spring 的 Web 模块,处理 Web 请求。
  • SpringMVC 基于 Spring 实现,属于 Spring 的一部分
  • SpringBoot快速开发脚手架,自动配置、简化整合,让 Spring+SpringMVC 开发变得极简单。

地位:前后端交互的入口 SpringMVC 负责:

  • 接收前端 HTTP 请求
  • 根据 URL 映射到对应 Controller 方法
  • 自动解析参数:@RequestBody@RequestParam@PathVariable
  • 统一响应格式,返回 JSON 数据
  • 拦截器、跨域、文件上传等 Web 功能

核心组件:

  • DispatcherServlet 前端控制器(统一入口)
  • Controller 处理器
  • 视图解析器(本项目只用 JSON,不用页面)

在项目里体现:

  • 各种 @RestController
  • @GetMapping@PostMapping
  • 统一结果封装 Result
  • JWT 登录拦截器

拦截器是 SpringMVC 框架提供的扩展组件,依附于 MVC 体系运行,作用是在请求进入 Controller 前后进行统一拦截与处理。在项目中,拦截器常用于 JWT 登录校验、接口权限控制、日志记录等场景。通过实现 WebMvcConfigurer 接口将拦截器注册到 MVC 容器,使其自动加入请求处理链路,与 SpringMVC 协同工作。

MyBatis —— 持久层框架,操作数据库 地位:连接 MySQL 的桥梁 MyBatis 负责:

  • 简化 JDBC 代码,不用写连接、关闭
  • 提供 XML 或注解方式编写 SQL
  • 自动实现数据库记录 ↔ Java 对象映射
  • 支持动态 SQL、多表查询、批量操作

为什么mapper是一个接口不需要实现类?

  • 我们只写 Mapper 接口 + SQL(注解或 XML)
  • 启动项目,@Mapper 被 MyBatis 扫描
  • MyBatis 动态生成代理实现类
  • 代理对象被放入 Spring 容器
  • Service 中通过 @Autowired 直接注入使用
  • 调用方法 → 代理执行 SQL → 返回结果

在项目里体现:

  • @Mapper 接口
  • Mapper XML 映射文件
  • 动态 SQL:<if><where><set><foreach>
  • 多表联查、分页查询

Spring Cache(Spring 缓存)

Spring Cache 是 Spring 提供的声明式缓存框架,作用:减少数据库查询,提高接口速度

不用自己操作 Redis,直接用注解就能缓存数据。


核心注解(项目里最常用)

1. @Cacheable

触发查询:先查缓存,没有再查数据库,并自动存入缓存常用于:查询接口

代码语言:javascript
复制
@Cacheable(value = "dish", key = "#categoryId")
public List<Dish> getByCategoryId(Long categoryId) {
    return dishMapper.selectByCategoryId(categoryId);
}

@CacheEvict

清理缓存新增、修改、删除数据时,把旧缓存删掉,保证数据一致

代码语言:javascript
复制
@CacheEvict(value = "dish", allEntries = true)
public void save(DishDTO dishDTO) {
}

@CachePut

更新缓存(很少用)

底层用什么存?

Spring Cache 只是规范,真正存储一般用:

  • Redis(苍穹外卖就是)
  • 或 Caffeine、内存等

Spring Cache 让缓存注解化,不用手动写 Redis 操作,查询自动缓存,更新自动清理,既简单又高效。

Spring Task(Spring 定时任务)

Spring 自带的 轻量级定时任务框架,作用:让程序在指定时间自动执行某段代码

外卖项目里最经典用途:取消超时未支付订单


1. 启动类加开启定时任务

代码语言:javascript
复制
@EnableScheduling

2. 写定时任务类 + @Scheduled

代码语言:javascript
复制
@Component
public class OrderTask {

    // 每分钟执行一次
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeoutOrder() {
        // 查询超时订单 → 取消订单 → 恢复库存
    }
}

cron 表达式简单理解

秒 分 时 日 月 周

例子:

  • 0 0 12 * * ? → 每天 12 点执行
  • 0 0/5 * * * ? → 每 5 分钟
  • 0 * * * * ? → 每分钟执行一次

外卖项目里的作用

  1. 每分钟检查超时订单超过 15 分钟未支付 → 自动取消
  2. 每天凌晨统计订单
  3. 自动清理无用缓存

总结:

  • Spring CacheSpring 提供的声明式缓存注解,通过 @Cacheable@CacheEvict 实现自动缓存与清理,结合 Redis 大幅降低数据库压力,提升接口响应速度。
  • Spring TaskSpring 内置轻量级定时任务工具,通过 @Scheduled 配置 cron 表达式实现定时执行,常用于订单超时取消、订单统计、数据清理等自动化任务。
苍穹外卖的中间件和第三方:

苍穹外卖 —— Redis 使用详解

一、项目为什么要用 Redis?

  1. 高并发支撑小程序首页、菜品列表请求量大,用 MySQL 压力太大
  2. 性能提升Redis 纯内存读写,比数据库快几十~上百倍
  3. 临时数据存储购物车、验证码、缓存都适合存在 Redis
  4. 过期自动删除验证码、超时订单标记可自动失效
  5. 分布式支持多服务器部署也能共享数据

二、Redis 在项目中的四大核心用途

1️⃣ 缓存菜品、分类数据(提高查询速度)

这是 Redis 最主要用途

  • 缓存对象:菜品 Dish、分类 Category
  • 业务场景:
    • 用户端小程序首页加载菜品列表
    • 分类切换展示对应菜品
  • 实现方式:
    • 结合 Spring Cache 注解实现
    • 查询走缓存,更新删除缓存

核心注解:

代码语言:javascript
复制
@Cacheable(value = "dishCache", key = "#categoryId")
代码语言:javascript
复制
@CacheEvict(value = "dishCache", allEntries = true)

存储微信小程序购物车

购物车不存 MySQL,只存在 Redis

  • key 设计:cart_用户id
  • value:存储购物车中多个菜品的 JSON 字符串
  • 为什么放 Redis:
    • 购物车是临时数据
    • 要求高并发、快速读写
    • 不需要长期持久化

核心操作:

  • 加入购物车:opsForValue().set()
  • 查看购物车:opsForValue().get()
  • 下单后清空购物车:delete()

存储验证码(手机验证码)

  • key:验证码类型_手机号
  • value:验证码数字
  • 设置 过期时间 5 分钟
  • 登录时校验 Redis 中的验证码

特点:

  • 不用存库
  • 自动过期,安全省心

缓存订单、状态、分布式锁(扩展场景)

部分版本中用于:

  • 订单状态缓存
  • 防止重复提交、重复支付
  • 简单分布式锁控制

在苍穹外卖项目中,Redis 作为高性能内存数据库,承担了缓存、临时存储、高并发支撑等核心任务。主要用于菜品数据缓存,大幅减轻数据库压力并提升首页加载速度;通过 Redis 存储微信小程序购物车,实现快速读写与数据共享;同时用于存储短信验证码并设置自动过期,保证安全性。结合 Spring Cache 实现声明式缓存,通过注解完成缓存读写与清理,简化开发。Redis 的使用显著提升了系统并发能力与响应速度,是外卖项目高并发场景下的关键中间件。


WebSocket

WebSocket 是一种全双工、长连接的网络通信协议。

  • 浏览器 ↔ 服务器一直保持连接
  • 服务器可以主动给前端发消息
  • 不像 HTTP 只能前端请求、服务器应答

一句话:HTTP 是发短信,WebSocket 是打电话。


为什么外卖项目必须用 WebSocket?

场景:

用户小程序下单支付成功 → 商家后台要立刻收到语音播报:您有新的订单

如果用 HTTP:

  • 前端只能每隔几秒轮询一次
  • 延迟高、浪费服务器资源、体验差

用 WebSocket:

  • 订单一创建,服务器主动推送给商家
  • 实时、秒级响应、流畅、省资源

苍穹外卖里 WebSocket 到底做什么?

项目里只有一个核心场景

订单来了 → 实时推送给商家后台

  1. 用户在小程序下单并支付成功
  2. 后端收到支付回调 / 订单提交
  3. 后端通过 WebSocket 主动发送消息
  4. 商家后台网页收到消息
  5. 播放语音:“您有新的订单,请及时处理”

额外扩展场景(部分版本):

  • 商家接单 → 推送给用户小程序
  • 订单完成 → 推送状态更新
  • 催单提醒

真实业务流程(最重要)

代码语言:javascript
复制
用户小程序下单
     ↓
订单入库(MySQL)
     ↓
支付成功
     ↓
Service 层调用:
WebSocketServer.sendToUser(商家ID, "新订单来了")
     ↓
服务器主动推送消息
     ↓
商家后台网页收到消息
     ↓
自动播放语音提示

1. 依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2配置类

代码语言:javascript
复制
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3. 核心服务类

代码语言:javascript
复制
@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

项目里有大量图片:

  • 菜品图片
  • 套餐图片
  • 用户头像

如果存在项目本地

  1. 图片占服务器硬盘空间
  2. 分布式部署时,图片无法共享
  3. 加载慢、不安全、不方便管理

所以用 阿里云 OSS 专门存图片。


OSS 在项目里到底干嘛

只有一个核心业务:

图片上传 + 图片访问

流程:

  1. 管理员在后台上传菜品图片
  2. 前端把图片传给后端
  3. 后端把图片上传到阿里云 OSS
  4. OSS 返回一个 URL 地址
  5. 后端把 URL 存入 MySQL
  6. 小程序 / 后台展示图片时,直接加载这个 URL

使用 OSS 的完整步骤(项目真实流程)

1. 阿里云开通 OSS

  • 注册阿里云
  • 开通 OSS 服务
  • 创建 Bucket(存储空间)
  • 获取 4 个关键信息:
    • endpoint
    • accessKeyId
    • accessKeySecret
    • bucketName

2. 引入依赖

代码语言:javascript
复制
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>

配置 yml

代码语言:javascript
复制
sky:
  oss:
    endpoint: oss-cn-beijing.aliyuncs.com
    access-key-id: LTAIxxxxxx
    access-key-secret: xxxxxxxx
    bucket-name: sky-takeout

工具类:

代码语言:javascript
复制
@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接受上传:

代码语言:javascript
复制
@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 地址。

一张图看懂上传流程

代码语言:javascript
复制
前端选择图片
     ↓
Controller 接收 MultipartFile
     ↓
调用 AliOssUtil 上传到 OSS
     ↓
OSS 返回图片 URL
     ↓
URL 存入 MySQL
     ↓
小程序/后台直接访问 URL 显示图片

为什么图片存在 OSS,不存数据库

  • 数据库不适合存大文件
  • 图片会让数据库暴增、查询变慢
  • OSS 成本低、访问快、自带防盗链
  • 支持 CDN 加速,小程序加载更流畅

总结:

阿里云 OSS 是苍穹外卖项目中用于图片存储的核心第三方服务,主要负责菜品图片、套餐图片、用户头像的上传与访问。通过 OSS 存储图片,避免了图片占用服务器磁盘空间,解决了分布式部署下图片无法共享的问题,同时借助阿里云的 CDN 加速,提升图片加载速度。后端通过封装工具类实现图片上传,并将返回的 URL 存入数据库,前端直接通过 URL 加载图片,整个流程简洁、高效、稳定,是外卖项目中标准的文件存储方案。

安全类&工具类:

JWT具体讲解:

JWT = JSON Web Token一种轻量级、可自包含、防篡改的令牌格式,用来做登录认证

特点:

  • 服务端不用存 Session
  • 令牌里可以存用户 ID、用户名等信息
  • 有签名,不能篡改
  • 适合分布式、前后端分离、小程序项目

JWT 长什么样

三段式字符串,用 . 分隔:

代码语言:javascript
复制
aaaaa.bbbbb.ccccc
头部.载荷.签名

1. 头部(Header)

  • 加密算法(一般 HS256)
  • 类型 JWT

2. 载荷(Payload)——最重要

放要传递的信息:

  • userId(用户 ID / 员工 ID)
  • username
  • 过期时间

苍穹外卖里核心就是存:userId

3. 签名(Signature)

服务端用密钥签名,防止篡改。

JWT 完整流程(苍穹外卖真实流程)

代码语言:javascript
复制
1. 员工/用户输入账号密码 → 登录
2. 校验通过 → 生成 JWT,把 userId 放进载荷
3. 返回 token 给前端
4. 前端存在本地,每次请求头带上:Authorization: Bearer token
5. 拦截器拦截请求 → 解析 token → 拿到 userId
6. 把 userId 存入 ThreadLocal,方便后面 Controller/Service 获取
7. 接口直接取用当前登录人 ID

四、生成 JWT(登录时)

工具类里写一个方法,把 userId 放进去:登录成功后返回给前端。

代码语言:javascript
复制
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

  1. 从请求头获取 token
  2. 解析 token
  3. 从载荷里 取出 userId
代码语言:javascript
复制
Claims claims = JwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());

ThreadLocal 是什么

ThreadLocal = 线程本地变量,同一个请求线程内随处可取值,不用层层传参。

一个请求从头到尾是同一个线程:

  • 拦截器
  • Controller
  • Service
  • Mapper

都在一个线程里,所以可以:

  • 拦截器存 userId
  • 后面任何地方直接拿

苍穹外卖里的写法:

代码语言:javascript
复制
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();
    }
}

拦截器:

代码语言:javascript
复制
BaseContext.setCurrentId(userId);

Controller / Service 中直接获取当前登录人 ID

任何地方,直接一句:

代码语言:javascript
复制
Long currentId = BaseContext.getCurrentId();

比如:

  • 创建订单时记录用户 ID
  • 新增菜品时记录操作人
  • 个人中心获取当前用户信息

不用传参、不用注解、随处可拿

请求结束一定要 remove!

代码语言:javascript
复制
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    BaseContext.remove();
}

整套逻辑串起来

代码语言:javascript
复制
用户登录
   ↓
生成 JWT,存入 userId
   ↓
前端携带 token 访问接口
   ↓
拦截器解析 token → 拿到 userId
   ↓
存入 ThreadLocal
   ↓
Controller/Service 直接 BaseContext.getCurrentId() 获取
   ↓
请求结束,清除 ThreadLocal

为什么要用 JWT + ThreadLocal

  1. 无状态,不依赖 Session,分布式随便部署
  2. 安全,不能篡改
  3. 方便,整个请求链路随处可拿用户 ID
  4. 解耦,不用每个接口都带 userId 参数

总结:

JWT 是一种轻量级的无状态认证令牌,在苍穹外卖项目中用于员工与用户的登录认证。服务端在登录成功后生成 JWT,并将用户 ID 存入载荷;前端每次请求在请求头携带令牌,拦截器负责解析令牌获取用户 ID,并通过 ThreadLocal 线程本地变量进行存储,使得后续 Controller、Service 层可以随时随地获取当前登录人 ID,避免了层层传递参数。最后在请求完成后清除 ThreadLocal 数据,保证线程安全与内存安全。整套方案简洁、安全、适合分布式前后端分离架构。

Knife4j(swagger)详解

Knife4j = 美化 + 增强版的 Swagger作用只有一个:自动生成接口文档,让前端、测试直接看接口、测接口

不用你手写文档,也不用 Postman 到处测。


它在项目里干嘛

  1. 自动生成接口文档把所有 @RestController 接口全部展示出来
  2. 在线调试接口浏览器里直接发请求,不用开 Postman
  3. 展示参数说明哪个字段必填、什么含义、示例值
  4. 分模块显示管理后台接口、用户端接口分开

为什么要用它

  • 前后端分离,前端要接口文档
  • 测试人员要测接口
  • 自己开发调试方便
  • 接口一多,手写文档根本维护不过来

项目里怎么用

1. 引入依赖

代码语言:javascript
复制
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

配置类:

代码语言:javascript
复制
@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();
    }
}

启动项目,访问地址

代码语言:javascript
复制
http://localhost:8080/doc.html

常用注解(写在接口上)

1. @Api 加在 Controller 上

给模块起名字

代码语言:javascript
复制
@Api(tags = "员工管理接口")
@RestController
@RequestMapping("/admin/employee")

2. @ApiOperation 加在方法上

说明接口功能

代码语言:javascript
复制
@ApiOperation("员工登录接口")
@PostMapping("/login")

3. @ApiModel / @ApiModelProperty 加在 DTO/VO 上

给字段加注释

代码语言:javascript
复制
@Data
@ApiModel("员工登录DTO")
public class EmployeeLoginDTO {

    @ApiModelProperty("用户名")
    private String username;
}

和 Swagger 区别

  • Swagger 是官方原版,界面丑
  • Knife4j 是国人增强版,界面好看、使用更爽

所以苍穹外卖用的是 Knife4j

Knife4j 是基于 Swagger 增强的接口文档生成工具,能够自动扫描项目中的 Controller 接口,生成可视化、可在线调试的 API 文档。通过简单注解即可描述接口功能与参数含义,极大提升前后端协作效率,方便开发调试与接口测试,是苍穹外卖项目中前后端对接的重要工具。


AOP 切面编程

AOP = 面向切面编程简单理解:不修改原来的代码,就能给方法统一加功能。

比如:

  • 统一日志
  • 统一事务
  • 统一权限校验
  • 统一异常处理
  • 接口耗时统计

这些与业务无关,但每个方法都需要的功能,就叫横切逻辑。AOP 就是把它们抽出来,做成一个 “切面”。

AOP 核心术语

  • 切面(Aspect):要加的功能类(日志、事务)
  • 通知(Advice):什么时候加
    • @Before 之前
    • @After 之后
    • @Around 前后都加(最强大)
    • @AfterReturning 返回后
    • @AfterThrowing 抛异常后
  • 切点(Pointcut):给哪些方法加(匹配规则)
  • 连接点(JoinPoint):具体被增强的方法

苍穹外卖里 AOP 用在哪

  1. 全局日志记录记录哪个接口被调用、参数、耗时
  2. 事务管理Spring 事务底层就是 AOP
  3. 接口权限校验
  4. 数据权限过滤

简单示例:接口日志切面

代码语言:javascript
复制
@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;
    }
}
代码语言:javascript
复制
@Aspect
@Component
@Slf4j
public class LogAspect {  // 单独的类

    // 切点:给哪些方法增强
    @Pointcut("execution(* com.sky.service.*.*(..))")
    public void pt(){}

    // 环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // ... 日志逻辑 ...
    }
}

事务管理(Spring 声明式事务)

保证多个数据库操作同时成功 or 同时失败。

典型场景:下单 → 扣库存 → 生成订单 → 扣减余额任何一步失败,全部回滚。

Spring 事务底层就是 AOP

代码语言:javascript
复制
@Transactional
本质 = AOP 环绕通知

执行流程:

  1. 进入方法 → 开启事务
  2. 执行 SQL
  3. 无异常 → 提交
  4. 有异常 → 回滚

3. 苍穹外卖必用事务的场景

  • 提交订单(订单表 + 订单明细表 + 购物车删除)
  • 支付成功(更新订单 + 通知商家)
  • 批量起售 / 停售菜品
  • 取消订单(恢复库存 + 退款)

这些必须保证要么全成,要么全失败

4. 基本使用

代码语言:javascript
复制
@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 和事务是什么关系

  • AOP 是一种思想、一种技术
  • 事务是 AOP 的一个经典应用

Spring 事务就是用 AOP 动态代理实现的给加了 @Transactional 的方法套一层 “开启、提交、回滚” 的增强逻辑。给方法前后加功能。

总结:

AOP 即面向切面编程,是 Spring 核心特性之一,用于在不修改业务代码的前提下,对方法进行统一增强,如日志记录、权限校验、事务管理等。Spring 声明式事务基于 AOP 实现,通过 @Transactional 注解即可为方法开启事务,保证多个数据库操作的原子性:操作全部成功则提交,出现异常则自动回滚。在苍穹外卖项目中,事务主要应用于订单提交、订单支付、取消订单等核心业务,确保数据一致性与业务安全性;AOP 则广泛用于日志记录、接口监控等场景,提升代码复用性与可维护性。


Lombok 详细讲解

Lombok 是一个 Java 插件工具,作用只有一个:帮你自动生成代码,不用手写 getter/setter/toString/ 构造器…

以前写实体类要写一堆:

代码语言:javascript
复制
private Long id;

public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

用了 Lombok,一个注解代替所有

写在实体类 / Service / Controller 上面最常用在:

  • Entity
  • DTO
  • VO
  • pojo
代码语言:javascript
复制
@Data
public class User {
    private Long id;
    private String name;
}

最常用注解

@Data(最重要、最常用)

自动生成:

  • getter / setter
  • toString
  • equals / hashCode
  • 无参构造

实体类 DTO、VO 全部都用它。

代码语言:javascript
复制
@Data
public class DishDTO {
}

@Slf4j

自动生成日志对象:

代码语言:javascript
复制
private static final Logger log = LoggerFactory.getLogger(当前类.class);

写在 Controller、Service、切面、任务类 上:

代码语言:javascript
复制
@Slf4j
@Service
public class OrderService {
}

然后直接用:

代码语言:javascript
复制
log.info("订单创建成功");

@NoArgsConstructor

无参构造器

@AllArgsConstructor

全参构造器


@Builder

建造者模式,优雅创建对象

代码语言:javascript
复制
User user = User.builder()
                .id(1L)
                .name("张三")
                .build();

它是怎么生效的

  • 编译时(.java → .class)自动生成代码
  • 你看不到源码,但 class 文件里真的有
  • 不影响运行、不降低性能

为什么项目要用 Lombok

  • 代码极度简洁
  • 实体类少写几百行代码
  • 改字段不用改 getter/setter
  • 提高开发效率,减少出错

一句话总结

Lombok 是一款通过注解简化 Java 代码的工具库,在苍穹外卖项目中广泛应用于实体类、DTO、VO 以及业务类。通过 @Data 自动生成 getter、setter、toString 等方法,@Slf4j 快速注入日志对象,大幅减少样板代码,让开发更简洁高效,是现代 SpringBoot 项目标配工具。

MYSQL表:

关于这一部分,我感觉没什么好说的,能正确写好sql语句就可以了,还有一个就是我学的时候有点搞不清的主键id和正常id

主键 ID vs 普通 ID:核心区别 + 用法全解

主键 id正常 id,本质是:一个是数据库核心主键(唯一、不可改、索引),一个只是业务里叫 “id” 的普通字段


先一句话分清

  1. 主键 ID(主键)
    • 数据库强制要求的唯一标识
    • 一张表只能有一个
    • 作用:定位数据、关联表、保证数据不重复
    • 通常命名:iduser_id(主键)、pk_id
  2. 正常 ID(业务 ID)
    • 只是业务上的编号,和数据库主键无关
    • 一张表可以有 N 个
    • 作用:给用户看、对外暴露、业务逻辑使用
    • 通常命名:order_nouser_codebiz_id

核心区别对比表

特性

主键 ID(数据库主键)

普通 ID / 业务 ID

数量

一张表只能有 1 个

一张表可以有多个

唯一性

数据库强制唯一

可重复(需手动控制)

非空

强制非空

可以为空

能否修改

严禁修改

可以随意修改

索引

自带唯一索引(查询快)

无索引(需手动加)

作用

数据库内部使用、关联表

业务展示、对外接口、用户看

暴露风险

不能暴露给前端 / 外网

专门用来暴露给外部


最常见的实际场景

1. 用户表(经典例子)

sql

代码语言:javascript
复制
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. 订单表

  • 主键 ID:order_id (主键,自增) → 数据库内部关联、查询
  • 业务 ID:order_no = 2025040310001 → 用户看到的订单号、客服查询用

为什么要分开

1. 安全(最关键)

  • 主键 ID 是连续自增的,如果暴露出去,别人能猜到你有多少数据、爬取你的信息。
  • 业务 ID 是随机 / 无规则的,无法被猜测,更安全。

2. 解耦

  • 主键 ID 只服务数据库
  • 业务 ID 只服务业务逻辑
  • 以后业务规则变了,改业务 ID,完全不影响数据库结构

3. 规范

  • 主键:稳定、不变、唯一
  • 业务 ID:可修改、可自定义、可重复(如果业务允许)

五、简单总结记忆法

  • 主键 ID = 数据库的身份证(唯一、终身不变、内部用)
  • 普通 ID = 你的工号 / 学号(业务用、对外展示、可改)

总结

  1. 主键 ID:数据库唯一标识,1 张表 1 个,不可改、不暴露、自带索引。
  2. 普通 ID:业务编号,随便用,可改、可暴露、无强制约束。
  3. 最佳实践:永远用主键 ID 做内部关联,用业务 ID 做对外展示

Redis缓存设计表:

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

大家可以对照着看看,有什么不懂的具体去复习一下,这里再补充一下Nginx反向代理,其实也不敢多说,容易违规。 在 苍穹外卖 项目中,Nginx 反向代理是整个系统的流量入口与核心网关,主要解决前后端分离、跨域、请求转发、动静分离、负载均衡等问题。 一、核心作用(项目场景)

  1. 部署前端静态资源(动静分离)
    • 管理端(Vue 页面)直接放在 Nginx 的 html 目录。
    • Nginx 直接返回 HTML/CSS/JS/ 图片,不经过 Tomcat,速度极快

反向代理转发 API 请求(解决跨域)

  • 前端请求:http://localhost/api/employee/login
  • Nginx 转发到后端:http://localhost:8080/admin/employee/login
  • 浏览器只认 Nginx(80 端口),不存在跨域

按路径区分双端(管理端 / 用户端)

  • /api/xxx → 后端管理服务(/admin/xxx
  • /user/xxx → 后端用户服务(/user/xxx

负载均衡(集群扩展)

  • upstream 定义多台后端服务器
  • Nginx 按轮询 / 权重分发请求
  • 支持高并发、避免单点故障
结语:

最后,真的是燃尽了,如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一部分:项目总览
    • 项目的环境搭建:
  • 业务功能:
    • 关于业务功能实现的流程:
      • 关于项目中的相关注解:
      • Mapper 层讲解
      • XML 映射文件(复杂 SQL 写这里)
  • 用户端小程序开发:
  • 后端技术栈:
    • Spring Cache(Spring 缓存)
    • Spring Task(Spring 定时任务)
    • 苍穹外卖的中间件和第三方:
      • 苍穹外卖 —— Redis 使用详解
    • WebSocket
    • OSS 是什么
    • 苍穹外卖为什么要用 OSS
    • 安全类&工具类:
      • JWT具体讲解:
      • JWT 长什么样
    • ThreadLocal 是什么
    • Knife4j(swagger)详解
    • AOP 切面编程
    • 事务管理(Spring 声明式事务)
    • Lombok 详细讲解
    • MYSQL表:
    • 结语:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档