Skip to content

REST API 设计 ​

S3-REST必学进阶约 60 分钟JDK 21企业开发基础 · Spring 核心与 MVC

1. 本节定位 ​

此前讲的是"怎么把代码写好",本节讲"怎么把能力暴露给别人用"——也就是接口设计。

接口是前后端之间、服务与服务之间的契约。它一旦发布就难以更改(调用方依赖它),所以设计阶段的准确性比编码阶段更重要。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 REST 的核心思想(资源 + 方法 + 表示);
  • 按规范设计 URL(名词、复数、层级、少用动词);
  • 说出四种 HTTP 方法的幂等性与安全语义;
  • 正确使用状态码,并说清"全部返回 200"为什么是反模式;
  • 设计出统一的分页、排序、过滤参数约定;
  • 说出幂等性设计的两种实现方式;
  • 用检查清单审查一个接口设计。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

接口设计错了,代价很高:

错误代价
URL 命名混乱(/getOrderList vs /orders)每个调用方都要查文档,团队内无法形成直觉
状态码全用 200调用方必须解析 body 才知道成败;监控与网关无法基于状态码做告警与重试
接口没有幂等设计网络重试导致重复下单
分页参数各自为政(page / pageNum / pageIndex)每个接口都要重新看文档

而这些问题在项目初期几乎无成本——只是没人告诉过你"应该怎么设计"。

5. 核心概念 ​

5.1 REST 的核心思想 ​

REST 把一切都看作资源,用URL 定位资源、用方法表达操作、用表示(JSON)传递状态。

text
POST   /orders           创建订单
GET    /orders/123       查询订单 123
GET    /orders?status=PAID&page=1    查询订单列表(带筛选与分页)
PUT    /orders/123       整体替换订单 123
PATCH  /orders/123       局部修改订单 123
DELETE /orders/123       删除订单 123

三个要点:

要点说明
URL 是名词(资源)/orders 而不是 /getOrders
方法是动词(操作)GET / POST / PUT / DELETE
状态码表达结果201 Created 而不是 200 OK + {"success": true}

5.2 URL 设计规范 ​

规范好差
用名词,不用动词GET /ordersGET /getOrderList
用复数表示集合/orders/123/order/123(单复数混用)
层级用嵌套表示从属关系GET /orders/123/itemsGET /orderItems?orderId=123
筛选条件用查询参数/orders?status=PAID/orders/status/PAID
小写 + 连字符/order-items/orderItems、/order_items
不要带文件扩展名/orders/123/orders/123.json

关于"动作类操作"怎么表达(REST 的经典争议点):

需求不推荐推荐
取消订单POST /cancelOrderPOST /orders/123/cancel(子资源 + 动作,务实做法)
支付订单POST /payOrderPOST /orders/123/payments(支付作为子资源)
用户登录POST /loginPOST /sessions(会话作为资源)

说明:纯粹的 REST 不接受"动作 URL",但工程实践中"子资源 + 动作"是被广泛接受的折中——因为它清晰、无歧义。

判断标准:如果一个操作很难用资源表达,说明它可能是"业务动作"——用一个明确的动词路径比硬套 REST 更不容易出错。

5.3 HTTP 方法的语义(本节重点) ​

方法语义安全(不改变数据)幂等(执行多次 = 一次)典型用途
GET读取✅✅查询
POST创建❌❌新建资源
PUT整体替换❌✅用已知 id 覆盖
PATCH局部修改❌⚠️ 取决于实现改一个字段
DELETE删除❌✅删除

三个必须记住的推论:

① GET 绝不能有副作用

text
✗ GET /orders/123/cancel        用 GET 做取消操作

为什么危险:浏览器预取、爬虫、监控探活都会"顺手"发 GET——一个链接被访问就会取消订单。

② PUT 必须幂等,POST 不必

text
PUT /orders/123 { "status": "PAID" }     ← 执行 10 次,结果一样(幂等)
POST /orders { ... }                     ← 执行 10 次,创建 10 个订单

这就是"为什么创建用 POST、更新用 PUT"。

③ POST 的非幂等性必须靠"幂等键"补救

场景:用户点击"支付",网络超时重试——会不会扣两次钱?

幂等键方案(业界标准做法):

http
POST /payments
Idempotency-Key: 7f3a9c2e-...
Content-Type: application/json

{ "orderNo": "ORD-001", "amountInCents": 19900 }
要点说明
客户端生成唯一键通常是一次"业务操作"的 UUID(重试时复用同一个键)
服务端先查后写(同一个事务/唯一约束)键已存在则直接返回上次的结果
键要存下来并设过期时间一般 24 小时
实现基础数据库的唯一约束(《表设计与约束》)

顺带:这也是"为什么支付接口都有 out_trade_no(商户订单号)"——它就是幂等键。

5.4 状态码的正确使用 ​

先看一个反模式:

json
HTTP/1.1 200 OK

{ "code": 404, "message": "订单不存在", "data": null }

"HTTP 200 + body 里的 code" 是很多国内项目的做法,但它有四个问题:

问题说明
网关看不到真实状态Nginx、API 网关、负载均衡都按 HTTP 状态码做统计与策略
监控无法告警404 率、5xx 率是标准监控指标——全 200 时它们全是 0
客户端库误判HTTP 客户端(如 RestTemplate)默认对 4xx/5xx 抛异常——全 200 时它以为都成功了
重试策略失效"5xx 才重试"是通用策略;全 200 时重试逻辑无从下手

正确用法:

状态码含义什么时候用
200 OK成功GET / PUT / PATCH 成功
201 Created已创建POST 成功,并在 Location 头里给出新资源的 URL
204 No Content成功但无返回体DELETE 成功
400 Bad Request请求格式错误JSON 语法错误、缺少必填字段
401 Unauthorized未认证没登录、token 无效
403 Forbidden已认证但无权限登录了但没这个权限
404 Not Found资源不存在查不到、或无权知道它是否存在
409 Conflict冲突唯一约束冲突(用户名已存在)
422 Unprocessable Entity参数校验失败格式对但值不合法(余额不足)
429 Too Many Requests限流请求过于频繁
500 Internal Server Error服务端错误未预期异常
503 Service Unavailable服务不可用依赖挂掉、正在发布

401 与 403 的区别(高频混淆):

text
401 = 你是谁?我不知道        → 客户端应该去登录(或刷新 token)
403 = 我知道你是谁,但你不能  → 客户端不该重试,重试也没用

400 与 422 的取舍:

做法说明
只用 400简单,但"格式错"与"值不合法"混在一起
400 + 422 分开更精确:400 是"请求本身有问题",422 是"参数校验没过"

实践建议:团队内统一即可,但不要全用 200。

5.5 响应体设计 ​

统一响应格式(成功):

json
{
  "data": {
    "orderNo": "ORD-001",
    "amountInCents": 19900,
    "status": "PAID"
  }
}

列表 + 分页:

json
{
  "data": [
    { "orderNo": "ORD-001" },
    { "orderNo": "ORD-002" }
  ],
  "page": {
    "page": 1,
    "size": 20,
    "total": 137,
    "totalPages": 7
  }
}

错误响应(要有机器可读的错误码):

json
{
  "code": "ORDER_NOT_FOUND",
  "message": "订单不存在",
  "traceId": "7f3a9c2e-...",
  "timestamp": "2026-09-21T10:00:00Z",
  "details": [
    { "field": "amountInCents", "message": "金额必须大于 0" }
  ]
}

四个要素的作用:

字段作用
code机器可读——客户端根据它做分支(不要用 message 做判断)
message给人看——可以直接展示给用户(也可以让客户端自己映射文案)
traceId排查用——用户报错时把它给客服,能直接定位日志
details参数校验的逐字段错误(表单场景必需)

一个关键设计原则:

错误码是契约,消息是文案。

  • 错误码:一旦发布就是契约,不能随便改(客户端依赖它);
  • 消息:可以随时改(甚至可以让客户端自己维护多语言文案)。

不要做的事:

json
{ "code": 1, "message": "系统繁忙,请稍后重试" }      // ✗ 所有错误都是 code=1
{ "error": "Exception in thread main java.lang.NullPointerException ..." }   // ✗ 泄露堆栈

第二行尤其危险:泄露堆栈 = 给攻击者提供内部结构信息。

5.6 分页、排序、过滤的参数约定 ​

统一约定(团队内一致比"哪种更好"更重要):

需求参数示例
分页page + size(页码从 1 开始)/orders?page=2&size=20
排序sort=字段,方向/orders?sort=createdAt,desc
多字段排序重复 sort/orders?sort=status,asc&sort=createdAt,desc
过滤直接用字段名/orders?status=PAID&userId=1
范围字段_gte / 字段_lte/orders?createdAt_gte=2026-01-01
全文搜索q 或 keyword/orders?keyword=键盘

两种分页方式的取舍:

方式优点缺点适用
页码分页(page / size)能跳页、能显示总页数深分页慢(LIMIT 100000, 20 要扫描前 10 万行);数据变动时会重复/漏后台管理、数据量小
游标分页(cursor)稳定、高效不能跳页信息流、无限滚动、大数据量

顺带一个实践细节:size 必须有上限(如最大 100)——否则 size=1000000 会打垮数据库 ✓

6. 递进示例 ​

6.1 URL 好坏对比 ​

需求✗ 不好的设计✓ 好的设计
查订单列表GET /getOrderListGET /orders
查某个订单GET /order/query?id=123GET /orders/123
查订单的明细GET /getOrderItems?orderId=123GET /orders/123/items
创建订单POST /order/createPOST /orders
取消订单POST /order/cancel?id=123POST /orders/123/cancel
按状态筛选GET /orders/filter/PAIDGET /orders?status=PAID
分页GET /orders?pageNum=2&pageSize=20GET /orders?page=2&size=20
删除POST /order/delete?id=123DELETE /orders/123

注意最后一行:用 POST 做删除是很多老系统的做法(因为老浏览器不支持 DELETE)——现代前端已经没这个问题了。

6.2 一个完整的接口设计 ​

http
# 创建订单
POST /orders
Content-Type: application/json
Idempotency-Key: 7f3a9c2e-1b2c-...

{ "productId": 1, "quantity": 2 }

HTTP/1.1 201 Created
Location: /orders/ORD-001

{ "data": { "orderNo": "ORD-001", "status": "CREATED", "amountInCents": 39800 } }
http
# 查询订单(不存在)
GET /orders/ORD-999

HTTP/1.1 404 Not Found

{ "code": "ORDER_NOT_FOUND", "message": "订单不存在", "traceId": "..." }
http
# 参数校验失败
POST /orders

{ "productId": 1, "quantity": 0 }

HTTP/1.1 422 Unprocessable Entity

{
  "code": "VALIDATION_FAILED",
  "message": "参数校验失败",
  "details": [ { "field": "quantity", "message": "数量必须大于 0" } ]
}

6.3 反例:异步操作返回 202 ​

很多团队把所有操作都做成同步,导致"批量导入 10 万条"的接口请求挂 5 分钟。

正确做法:

http
POST /imports

HTTP/1.1 202 Accepted
Location: /imports/task-123

{ "taskId": "task-123", "status": "PROCESSING" }

然后客户端轮询 GET /imports/task-123 看进度 ✓ 202 就是为这种场景设计的。

7. 接口设计检查清单 ​

markdown
## URL

- [ ] 用名词(复数)表示资源吗?
- [ ] 层级关系用嵌套表达了吗?(不超过 2 层)
- [ ] 筛选、排序、分页用查询参数了吗?
- [ ] 命名风格统一吗?(小写 + 连字符)

## 方法

- [ ] `GET` 没有副作用吗?
- [ ] 创建用 `POST`、整体替换用 `PUT`、局部修改用 `PATCH`、删除用 `DELETE` 吗?
- [ ] 非幂等的写操作有**幂等键**吗?

## 状态码

- [ ] 创建成功返回 `201` + `Location` 吗?
- [ ] 删除成功返回 `204` 吗?
- [ ] 未认证用 `401`、无权限用 `403` 吗?
- [ ] 参数校验失败与业务规则冲突用了不同的码吗?(`422` / `409`)
- [ ] 有没有"全 200 + body 里带 code"的反模式?

## 响应体

- [ ] 列表接口有统一的分页结构吗?
- [ ] 错误响应有**机器可读的 code** 吗?
- [ ] 有 `traceId` 便于排查吗?
- [ ] 参数校验错误有逐字段的 `details` 吗?
- [ ] **没有泄露堆栈或内部细节**吗?

## 安全与稳定性

- [ ] `size` 有上限吗?
- [ ] 敏感字段(密码、身份证)没有出现在响应里吗?
- [ ] 越权访问有校验吗?(不能只靠前端隐藏按钮)

8. 常见错误 ​

错误 1:URL 里带动词 ​

text
POST /createOrder
GET  /getOrderList
POST /updateOrderStatus

问题:这是 RPC 风格,不是 REST——它的后果是"每个接口的命名都要重新想一遍",团队无法形成直觉。

修复:POST /orders、GET /orders、PATCH /orders/123。

错误 2:全部返回 200 ​

见 5.4 节。这是最影响运维与监控的一类设计问题。

错误 3:用 GET 做写操作 ​

text
GET /orders/123/cancel

危险场景:浏览器预取(Chrome 会预取页面里的链接)、爬虫、监控探活——一个 GET 请求被"顺手"发出,操作就执行了。

错误 4:用 message 做客户端判断 ​

java
// ✗ 客户端根据消息文本判断
if ("订单不存在".equals(response.getMessage())) { ... }

问题:消息一改,客户端就崩。

修复:用 code(ORDER_NOT_FOUND)判断,message 只用于展示。

错误 5:接口返回数据库实体 ​

java
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();     // ✗ 直接返回实体
}

三个问题:

问题说明
泄露敏感字段密码哈希、手机号、内部备注
数据库结构被暴露字段名与表结构绑定,改表就是"破坏 API"
N+1 或懒加载异常序列化时触发额外查询(JPA 的经典坑)

修复:用专门的 DTO / VO(《Record》 很适合做这个)✓

错误 6:没有分页上限 ​

text
GET /orders?size=1000000          # 数据库直接被打垮

修复:服务端强制上限(如 size = Math.min(size, 100))。

9. 动手实验 ​

目标:把一份"混乱的接口设计"改造成规范的。

步骤:

  1. 下面是一份典型的老式接口设计,逐条找出问题:

    #接口
    1GET /getUserList?pageNum=1&pageSize=20
    2POST /createUser
    3POST /deleteUser?id=1
    4GET /user/detail?id=1
    5GET /user/updateStatus?id=1&status=ACTIVE
    6POST /user/sendEmail?id=1
    7所有接口返回 200 + {"code":0,"msg":"成功"}
    8GET /exportUsers(导出 10 万行数据)
  2. 给出改造后的版本(含方法与状态码);

  3. 为第 6 条接口设计"幂等"方案(用户可能重复点击"发送邮件");

  4. 为第 8 条接口设计异步方案(含状态码、返回体、轮询接口);

  5. 写出第 4 条接口的三种错误响应(不存在、无权限、参数错误),含状态码与响应体;

  6. 用自己的话总结:"判断一个接口设计好不好"的两个问题。

验收方式:能说出第 1、3、5 条的改造理由。

扩展任务:为你项目里的一个接口写"接口文档"(含 URL、方法、请求体、响应体、状态码、错误码表、幂等说明)——用检查清单逐条自评。

10. 练习题 ​

基础题

  1. 判断:GET 请求可以带副作用,只要不返回大量数据。(对 / 错)
  2. 填空:创建资源成功应返回 ______;删除成功应返回 ______;已认证但无权限应返回 ______。
  3. 简答:为什么"全部返回 200"是不好的设计?至少说出两个理由。

代码阅读题

java
@GetMapping("/orders/{id}/cancel")
public Result<String> cancel(@PathVariable Long id) {
    orderService.cancel(id);
    return Result.success("取消成功");
}
  1. 这个接口设计有什么问题?(至少两个)
  2. 如果必须有"取消"这个动作,你会怎么设计?

编程题

  1. 设计一套"文章管理"的 REST 接口:
    • 需求:发布文章、查看文章列表(支持按作者/标签筛选、按时间排序、分页)、查看文章详情、修改文章、删除文章、点赞文章;
    • 写出每个接口的方法 + URL + 成功状态码 + 失败状态码;
    • 为"点赞"设计幂等方案(同一个用户重复点赞应该怎样?);
    • 为"文章列表"设计分页参数与响应体;
    • 说明:"点赞"用 POST 还是 PUT?为什么?

综合任务

  1. 设计一套"支付回调"接口并说明设计取舍:
    • 场景:第三方支付平台在支付成功后回调你的服务;
    • 要求:
      • 设计 URL、方法、请求体(含签名校验);
      • 说明为什么这个接口必须幂等(第三方会重试多次);
      • 设计幂等实现(提示:用支付平台的流水号作为幂等键);
      • 设计响应格式(第三方期望什么样的响应表示"我已收到"?);
    • 额外要求:说明"回调接口应该同步处理业务,还是立刻返回再异步处理"?各自的取舍是什么?
分级提示

提示 1(方向):第 4 题的关键是"用 GET 做写操作"与"URL 里带动作"。

提示 2(关键点):第 6 题的"点赞"重复提交会怎样,取决于你怎么设计——取决于业务的真实语义(是"点赞数 +1"还是"标记已赞")。

提示 3(接近答案):第 7 题的回调接口必须立刻返回成功(第三方有超时与重试)——异步处理业务、同步保证幂等记录是常见做法。

11. 参考答案 ​

基础题
  1. 错。GET 绝对不能有副作用——这是 HTTP 规范里的"安全方法"语义。原因是浏览器、爬虫、监控都会主动发 GET,副作用会被"意外触发"。

顺带记住:GET 还必须是幂等的(多次执行结果一样)——所以 GET /orders?page=1 反复调用没问题。

  1. 201 Created(并在 Location 头给出新资源 URL);204 No Content;403 Forbidden。
  2. 至少四个理由:
理由说明
网关与监控失效Nginx、API 网关、监控系统都基于 HTTP 状态码做统计与告警——全 200 时"5xx 率"永远是 0
HTTP 客户端误判RestTemplate、HttpClient 等默认对 4xx/5xx 抛异常——全 200 时它无法区分成功与失败
重试策略无法实施"只重试 5xx"是通用策略;全 200 时无法判断"该不该重试"
缓存与 CDN 行为异常一些缓存策略依赖状态码(如"不缓存 5xx")

唯一的"好处"是"前端处理简单"——但那只是把复杂度从 HTTP 层挪到了业务层,而且代价由运维与监控承担。

代码阅读题(第 4、5 题)

第 4 题:两个问题。

问题一:GET 被用来执行写操作(取消订单)。

危险在哪:GET 是"安全方法",浏览器、爬虫、监控探活都可能主动请求它——一个链接被访问就会取消订单。

问题二:URL 里带动作(/cancel)。

严格 REST 里应该用"方法 + 资源"表达,而不是在 URL 里写动词。

第 5 题:两种可接受的方案:

方案 A(推荐):把"取消"建模成状态变更

http
PATCH /orders/123
Content-Type: application/json

{ "status": "CANCELLED" }

HTTP/1.1 200 OK

优点:纯 REST,且"取消"变成了"状态流转"——天然幂等(重复 PATCH 同一个状态结果一样)。

方案 B:子资源 + 动作(务实)

http
POST /orders/123/cancellations

HTTP/1.1 201 Created
Location: /orders/123/cancellations/1

优点:把"取消"建模为一次"取消记录"——语义清晰,且能记录"谁在什么时候取消的、原因是什么"。

什么时候用 B:当这个动作本身有需要记录的信息时(取消原因、操作人)。

注意方案 A 的一个隐患:PATCH 直接改状态绕过了状态机校验——如果业务上"已发货的订单不能取消",必须在服务端校验(不能只信任客户端传来的 status)。

实践建议:

能用"状态流转"表达的就用它(方案 A);动作本身有业务含义(需要记录、有独立生命周期)时用子资源(方案 B)。

避免 GET /cancel 这类写法——它同时违反了"安全方法"与"URL 用名词"两条原则。

编程题(第 6 题)
markdown
| 需求 | 方法 | URL | 成功 | 失败 |
|------|------|-----|------|------|
| 发布文章 | POST | /articles | 201 + Location | 422(校验失败) |
| 文章列表 | GET | /articles?authorId=1&tag=java&sort=publishedAt,desc&page=1&size=20 | 200 | 400 |
| 文章详情 | GET | /articles/{id} | 200 | 404 |
| 修改文章 | PATCH | /articles/{id} | 200 | 404 / 422 |
| 删除文章 | DELETE | /articles/{id} | 204 | 404 / 403 |
| 点赞 | PUT | /articles/{id}/likes/{userId} | 204 | 404 |
| 取消点赞 | DELETE | /articles/{id}/likes/{userId} | 204 | 404 |
| 点赞数 | GET | /articles/{id}/likes | 200 | 404 |

列表的分页参数与响应体:

http
GET /articles?authorId=1&sort=publishedAt,desc&page=1&size=20

HTTP/1.1 200 OK
json
{
  "data": [
    { "id": 1, "title": "Java 索引笔记", "authorId": 1, "publishedAt": "2026-09-21T10:00:00Z", "likeCount": 12 }
  ],
  "page": { "page": 1, "size": 20, "total": 137, "totalPages": 7 }
}

"点赞"为什么用 PUT 而不是 POST:

方法语义重复调用的结果
POST /articles/1/likes创建一条"点赞记录"创建 2 条(点赞数变 2)✗
PUT /articles/1/likes/{userId}把"用户 X 赞文章 Y"这个资源置为存在仍然是"已点赞"(幂等)✓

关键洞察:PUT 的语义是"用这个 URL 覆盖资源"——URL 里带了 userId,所以"赞两次"就是"往同一个位置写两次"→ 天然幂等 ✓

这也是"URL 设计影响幂等性"的经典案例:同样的业务需求,URL 设计不同,幂等性就不同:

text
POST /articles/1/likes              ← 服务端自己生成 id → 每次都是新资源 → 非幂等
PUT  /articles/1/likes/{userId}     ← URL 里已有唯一标识 → 覆盖写 → 幂等

重复点赞的行为定义(业务上要明确):

设计说明
幂等(推荐)重复点赞不报错、不重复计数——用户无感知
报错(409 Conflict)提示"你已经点过赞了"——但网络重试时会误报

实现层面:用唯一约束保证(UNIQUE (article_id, user_id)),插入冲突时忽略或返回成功 ✓ (《表设计与约束》 的"约束是最后一道防线")。

综合任务(第 7 题)

接口设计:

http
POST /payments/callbacks/wechat
Content-Type: application/json
X-Signature: <第三方签名>

{
  "outTradeNo": "ORD-001",
  "transactionId": "4200001234202609210001",
  "amountInCents": 19900,
  "status": "SUCCESS",
  "paidAt": "2026-09-21T10:00:05Z"
}

响应(第三方期望的格式):

json
{ "code": "SUCCESS", "message": "OK" }

为什么必须幂等:

原因说明
第三方会重试未收到成功响应(超时、网络抖动)就会重试——可能重试很多次
可能重复通知有些平台在"状态变更"时也会再通知一次(如从成功变为退款)
重试没有规律不能假设"只重试 3 次"或"间隔固定"

幂等实现:

sql
-- 用支付平台的流水号作为幂等键(它是全局唯一的)
CREATE TABLE payment_callback (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    transaction_id  VARCHAR(64) NOT NULL UNIQUE,      -- ← 幂等键
    out_trade_no    VARCHAR(64) NOT NULL,
    amount_in_cents BIGINT NOT NULL,
    status          VARCHAR(16) NOT NULL,
    received_at     TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

处理流程:

java
@Transactional
public void handleCallback(CallbackRequest request) {
    // 1. 验签(必须!否则任何人都能伪造回调)
    if (!signatureVerifier.verify(request)) {
        throw new IllegalArgumentException("签名校验失败");
    }

    // 2. 幂等检查:尝试插入,唯一约束冲突说明已处理过
    try {
        callbackRepository.insert(request);           // 依赖 UNIQUE (transaction_id)
    } catch (DuplicateKeyException exception) {
        log.info("重复回调,直接返回成功:{}", request.transactionId());
        return;                                       // ✓ 直接返回成功
    }

    // 3. 首次处理:更新订单状态(在同一个事务里)
    orderService.markPaid(request.outTradeNo(), request.transactionId());
}

同步还是异步处理:

方案做法优点缺点
同步处理(推荐起点)在请求里完成"记录回调 + 更新订单"简单、可靠(数据库事务保证一致)处理慢时可能超时 → 第三方重试(但幂等已保证不会重复扣)
异步处理记录回调后立刻返回,用 MQ 触发后续响应快复杂度上升(消息丢失、顺序、重试)

实践建议:

先做同步 + 幂等——因为幂等已经解决了"重试"这个最大风险。

只有当"处理逻辑确实很慢"(如需要调用多个下游)时,才考虑"记录回调(同步)→ 发消息(同步)→ 业务处理(异步)"的两段式。

但要注意:异步方案下,"记录回调"与"业务处理"不在同一个事务——必须保证消息不丢(本地消息表 + 定时补偿,或事务性消息)。

最后一个安全提醒:

必须做说明
验签回调 URL 是公开的,不验签等于任何人都能伪造"支付成功"
金额校验校验回调金额与订单金额一致(防止篡改)
IP 白名单(可选)只接受第三方平台的出口 IP
不在响应里泄露内部信息失败时返回通用错误即可,细节记日志

12. 掌握标准 ​

  • [ ] 能按规范设计 URL(名词、复数、层级、少用动词);
  • [ ] 能说出四种 HTTP 方法的幂等性与安全性,并知道 GET 为什么不能有副作用;
  • [ ] 能正确使用常见状态码,并说清"全 200"的四个问题;
  • [ ] 能设计出统一的响应体(含分页、错误码、traceId);
  • [ ] 知道 401 与 403、400 与 422 的区别;
  • [ ] 会为写操作设计幂等键,并知道它依赖数据库唯一约束;
  • [ ] 知道"接口不该直接返回数据库实体"。

13. 扩展资源 ​

14. 下一步学习 ​

Spring 核心三节到此完成。接下来可以:

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引