OpenAPI 与第三方接口对接规范
OpenAPI 3.0 / 3.1 规范
什么是 OpenAPI
OpenAPI 规范(原名 Swagger Specification)是 Linux 基金会旗下的 RESTful API 描述标准,使用 JSON 或 YAML 格式定义 API 的端点、参数、请求/响应结构、认证方式等。OpenAPI 3.0 于 2017 年发布,3.1 于 2021 年发布,新增了对 JSON Schema 2020-12 的完整支持。
OpenAPI 3.0 vs 3.1
| 对比维度 | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| JSON Schema 版本 | 自定义子集(近似 Draft-07) | 完整支持 JSON Schema 2020-12 |
| 根字段标识 | openapi: 3.0.x | openapi: 3.1.x |
| 示例字段 | example(单值) | examples(数组)+ example 并存 |
| Webhook 支持 | 不支持 | 原生支持 webhooks 字段 |
| 路径模板 | {param} | 兼容 {param},推荐 {param} |
| $schema 字段 | 不支持 | 允许在文档中声明 JSON Schema 版本 |
| null 类型 | 通过 nullable: true 实现 | 直接使用 type: null |
YAML 基础结构
openapi: 3.0.3
info:
title: 用户中心 API
description: 提供用户注册、登录、信息管理等基础能力
version: 1.0.0
contact:
name: 后端团队
email: backend@example.com
url: https://api.example.com/support
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
servers:
- url: https://api.example.com/v1
description: 生产环境
- url: https://staging-api.example.com/v1
description: 预发布环境
- url: http://localhost:8080/api/v1
description: 本地开发
paths:
/users:
get:
summary: 分页查询用户列表
operationId: listUsers
tags:
- 用户管理
parameters:
- name: page
in: query
description: 页码,从 1 开始
required: false
schema:
type: integer
minimum: 1
default: 1
- name: size
in: query
description: 每页条数,最大 100
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/PageResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: 创建用户
operationId: createUser
tags:
- 用户管理
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: 创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: 用户已存在
components:
schemas:
ApiResponse:
type: object
properties:
code:
type: integer
description: 业务状态码
example: 200
message:
type: string
description: 提示信息
example: 操作成功
data:
type: object
description: 响应数据
nullable: true
required:
- code
- message
PageResponse:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
type: object
properties:
list:
type: array
description: 数据列表
items:
$ref: '#/components/schemas/UserVO'
total:
type: integer
description: 总记录数
example: 100
page:
type: integer
description: 当前页码
example: 1
size:
type: integer
description: 每页条数
example: 20
pages:
type: integer
description: 总页数
example: 5
required:
- list
- total
- page
- size
IdResponse:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
type: integer
description: 新创建记录的 ID
example: 1001
CreateUserRequest:
type: object
description: 创建用户请求体
properties:
username:
type: string
description: 用户名
minLength: 4
maxLength: 32
pattern: '^[a-zA-Z0-9_]+$'
example: zhangsan
email:
type: string
format: email
description: 邮箱地址
example: zhangsan@example.com
phone:
type: string
description: 手机号
pattern: '^1[3-9]\d{9}$'
example: '13800138000'
password:
type: string
description: 密码(至少 8 位)
minLength: 8
maxLength: 64
writeOnly: true
required:
- username
- password
UserVO:
type: object
description: 用户视图对象
properties:
id:
type: integer
description: 用户 ID
example: 1001
username:
type: string
description: 用户名
example: zhangsan
email:
type: string
description: 邮箱
example: zhangsan@example.com
phone:
type: string
description: 手机号
example: '13800138000'
status:
type: string
description: 用户状态
enum:
- ACTIVE
- INACTIVE
- LOCKED
example: ACTIVE
createdAt:
type: string
format: date-time
description: 创建时间
example: '2026-07-09T10:30:00+08:00'
updatedAt:
type: string
format: date-time
description: 更新时间
example: '2026-07-09T10:30:00+08:00'
responses:
BadRequest:
description: 请求参数错误
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
code:
example: 400
message:
example: 请求参数校验失败
Unauthorized:
description: 未授权
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
code:
example: 401
message:
example: 未登录或 Token 已过期
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 使用 JWT Token 进行身份认证,格式: Bearer {token}
security:
- BearerAuth: []路径定义规范
路径命名规则:
| 元素 | 规范 | 示例 |
|---|---|---|
| 资源路径 | 名词复数,小写,连字符分隔 | /users, /order-items |
| 子资源 | 嵌套路径表达从属关系 | /users/{userId}/orders |
| 操作路径 | 避免动词,用 HTTP 方法表达操作 | POST /users(创建), DELETE /users/{id}(删除) |
| 版本号 | 统一前缀 | /v1/users, /v2/users |
| 查询操作 | 使用专用的 /search 端点 | POST /orders/search |
路径参数定义示例:
paths:
/users/{userId}/orders/{orderId}:
get:
parameters:
- name: userId
in: path
required: true
description: 用户 ID
schema:
type: integer
example: 1001
- name: orderId
in: path
required: true
description: 订单 ID
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
responses:
'200':
description: 成功请求体(requestBody)定义
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 名称
items:
type: array
description: 订单项
minItems: 1
items:
type: object
properties:
productId:
type: integer
quantity:
type: integer
minimum: 1
required:
- productId
- quantity
example:
name: 测试订单
items:
- productId: 101
quantity: 2
application/xml:
schema:
$ref: '#/components/schemas/CreateOrderRequest'响应定义
responses:
'200':
description: 操作成功
headers:
X-Request-Id:
schema:
type: string
format: uuid
description: 请求追踪 ID
X-RateLimit-Remaining:
schema:
type: integer
description: 剩余请求配额
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'4XX':
description: 客户端错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'参数类型
OpenAPI 3.0 支持四种参数位置:
| 参数位置 | 说明 | 适用场景 |
|---|---|---|
in: path | URL 路径参数 | 资源标识符,如 /users/{id} |
in: query | URL 查询参数 | 筛选、排序、分页条件 |
in: header | HTTP 请求头 | 认证 Token、请求 ID、内容类型 |
in: cookie | Cookie 值 | 会话标识、跟踪 ID |
参数定义完整示例:
parameters:
- name: X-Request-Id
in: header
description: 请求追踪 ID,用于链路追踪和问题排查
required: false
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
- name: fields
in: query
description: 指定返回字段,多个字段用逗号分隔
required: false
schema:
type: string
example: id,name,email
- name: sort
in: query
description: '排序字段,格式: field1,-field2(负号表示降序)'
required: false
schema:
type: string
example: -createdAt,idAPI 规范设计
RESTful 命名规范
资源命名
# 正确:名词复数形式
GET /api/v1/users # 查询用户列表
POST /api/v1/users # 创建用户
GET /api/v1/users/{id} # 查询单个用户
PUT /api/v1/users/{id} # 全量更新用户
PATCH /api/v1/users/{id} # 部分更新用户
DELETE /api/v1/users/{id} # 删除用户
# 子资源
GET /api/v1/users/{id}/orders # 查询用户订单列表
# 操作类接口(RPC 风格,有限使用)
POST /api/v1/orders/{id}/cancel # 取消订单
POST /api/v1/orders/{id}/refund # 发起退款
# 错误:使用动词
GET /api/v1/getUserList # 错误
POST /api/v1/createUser # 错误
POST /api/v1/deleteUser # 错误版本号策略
推荐在 URL 路径中嵌入版本号,保持向后兼容:
| 策略 | 示例 | 说明 |
|---|---|---|
| URL 路径版本 | /api/v1/users | 最直观,易于路由 |
| 请求头版本 | Accept: application/vnd.example.v1+json | 更灵活,URL 干净 |
| 参数版本 | /api/users?version=1 | 不推荐,缓存困难 |
HTTP 动词语义
| 动词 | 语义 | 幂等 | 安全 | 请求体 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 无 |
| POST | 创建资源 / 触发操作 | 否 | 否 | 有 |
| PUT | 全量替换资源 | 是 | 否 | 有 |
| PATCH | 部分更新资源 | 否 | 否 | 有 |
| DELETE | 删除资源 | 是 | 否 | 可选 |
请求响应格式
统一响应结构
所有 API 响应遵循统一的 JSON 结构:
{
"code": 200,
"message": "操作成功",
"data": {},
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1780444800000
}@Data
public class ApiResult<T> implements Serializable {
private Integer code;
private String message;
private T data;
private String traceId;
private Long timestamp;
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
result.setTraceId(TraceContext.getTraceId());
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> ApiResult<T> error(Integer code, String message) {
ApiResult<T> result = new ApiResult<>();
result.setCode(code);
result.setMessage(message);
result.setTraceId(TraceContext.getTraceId());
result.setTimestamp(System.currentTimeMillis());
return result;
}
}请求头规范
| 请求头 | 必填 | 说明 |
|---|---|---|
Content-Type | 是 | application/json;charset=utf-8 |
Accept | 是 | application/json |
Authorization | 鉴权时必填 | Bearer {token} 或 AK/SK 签名 |
X-Request-Id | 推荐 | 请求追踪 ID,UUID 格式 |
X-Client-Version | 推荐 | 客户端版本号,用于兼容性判断 |
Accept-Language | 否 | 国际化语言,zh-CN / en-US |
分页规范
基于页码的分页(适合前台列表)
// 请求
GET /api/v1/users?page=1&size=20&sort=-createdAt
// 响应
{
"code": 200,
"message": "操作成功",
"data": {
"list": [],
"page": 1,
"size": 20,
"pages": 50,
"total": 1000,
"hasMore": true
}
}@Data
public class PageResult<T> {
private List<T> list;
private Integer page;
private Integer size;
private Integer pages;
private Long total;
private Boolean hasMore;
public PageResult(List<T> list, Long total, Integer page, Integer size) {
this.list = list;
this.total = total;
this.page = page;
this.size = size;
this.pages = (int) Math.ceil((double) total / size);
this.hasMore = page < this.pages;
}
}分页请求参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
page | Integer | 1 | 页码,从 1 开始 |
size | Integer | 20 | 每页条数,最大值 100 |
sort | String | 可选 | 排序字段,如 -createdAt,id |
基于游标的分页(适合大数据量、实时数据)
// 请求
GET /api/v1/feed?cursor=MTY1NDMyMDAwMA&size=20
// 响应
{
"code": 200,
"message": "操作成功",
"data": {
"list": [],
"cursor": "MTY1NDMyMDEwMA",
"hasMore": true,
"size": 20
}
}游标分页规则:
cursor使用 Base64 编码的排序字段值(如时间戳、ID)- 首次请求不传
cursor,服务端返回第一页 - 服务端返回
cursor字段,客户端下次请求时携带 - 游标分页适合增量加载场景(Feed 流、消息列表)
- 不适合跳转指定页面的场景
错误码设计
全局错误码
| 错误码 | 含义 | HTTP 状态码 |
|---|---|---|
| 200 | 操作成功 | 200 |
| 400 | 请求参数校验失败 | 400 |
| 401 | 未认证(未登录或 Token 无效) | 401 |
| 403 | 无权限(禁止访问) | 403 |
| 404 | 请求的资源不存在 | 404 |
| 405 | 请求方法不允许 | 405 |
| 408 | 请求超时 | 408 |
| 409 | 资源冲突(如重复创建) | 409 |
| 415 | 不支持的媒体类型 | 415 |
| 429 | 请求过于频繁,触发限流 | 429 |
| 500 | 服务器内部错误 | 500 |
| 502 | 网关错误 | 502 |
| 503 | 服务暂不可用 | 503 |
| 504 | 网关超时 | 504 |
业务错误码
业务错误码按模块划分,使用 6 位数字编码:
XX YY ZZ
| | |
| | +-- 具体错误序号(01-99)
| +----- 模块编号(01-99)
+-------- 系统/业务标识(10=业务, 20=系统)示例:
| 错误码 | 含义 |
|---|---|
| 100101 | 用户不存在 |
| 100102 | 用户名已存在 |
| 100103 | 用户已被锁定 |
| 100201 | 邮箱格式不正确 |
| 100202 | 手机号已注册 |
| 200101 | 订单不存在 |
| 200102 | 订单状态不允许取消 |
| 200103 | 订单已超时关闭 |
| 300101 | 支付金额不一致 |
| 300102 | 支付已超时 |
public enum ErrorCode {
// 全局错误
SUCCESS(200, "操作成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录或 Token 无效"),
FORBIDDEN(403, "无权限访问"),
NOT_FOUND(404, "资源不存在"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
// 用户模块(10)
USER_NOT_FOUND(100101, "用户不存在"),
USERNAME_ALREADY_EXISTS(100102, "用户名已存在"),
USER_LOCKED(100103, "用户已被锁定"),
PASSWORD_ERROR(100104, "密码错误"),
// 订单模块(20)
ORDER_NOT_FOUND(200101, "订单不存在"),
ORDER_CANNOT_CANCEL(200102, "订单状态不允许取消"),
ORDER_TIMEOUT(200103, "订单已超时关闭");
private final int code;
private final String message;
}HTTP 状态码与业务码配合
- HTTP 状态码用于表达传输层状态(协议级别)
- 业务码用于表达业务逻辑状态(应用级别)
- 正常情况下返回 HTTP 200 + 业务 code=200
- 发生业务异常时返回 HTTP 200 + 业务 code=具体错误码
- 发生系统级异常时返回 HTTP 5xx + 业务 code=对应错误码
// 业务异常(HTTP 200,但业务 code 非 200)
HTTP/1.1 200 OK
{
"code": 100101,
"message": "用户不存在",
"data": null,
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1780444800000
}
// 系统异常(HTTP 500)
HTTP/1.1 500 Internal Server Error
{
"code": 500,
"message": "服务器内部错误,请稍后重试",
"data": null,
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1780444800000
}字段命名规范
JSON 字段命名
| 规范 | 规则 | 示例 |
|---|---|---|
| 命名风格 | 小驼峰(camelCase) | userId, createdAt, orderItems |
| 缩写 | 保持全小写,不全部大写 | orderId(非 orderID), htmlContent |
| 布尔类型 | 以 is/has/can/should 开头 | isActive, hasPermission, canDelete |
| 日期时间 | 统一使用 ISO 8601 格式 | 2026-07-09T10:30:00+08:00 |
| 枚举值 | 全大写 + 下划线 | ACTIVE, IN_PROGRESS, WAIT_PAYMENT |
| 金额 | 统一以分为单位(Long 类型) | amountInCents: 1000(表示 10 元) |
数据库字段与 API 字段映射
数据库 snake_case <-> API camelCase
user_id <-> userId
created_at <-> createdAt
order_status <-> orderStatus第三方接口对接最佳实践
接口文档管理
文档获取方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Swagger / OpenAPI JSON | 服务提供方暴露 /v3/api-docs 端点 | 动态获取最新的接口定义 |
| Knife4j UI | 增强版的 Swagger UI,提供在线调试 | 开发联调阶段 |
| YApi | 国产接口管理平台,支持文档托管 | 团队协作,统一的文档入口 |
| 离线 OpenAPI YAML/JSON | 通过文件方式交付 OpenAPI 规范 | 外部系统对接 |
| Markdown 文档 | 人工编写的接口说明文档 | 简单的对接场景 |
YApi 使用规范
# YApi 项目配置建议
project:
name: 第三方对接-XX系统
basePath: /api/v1
tag:
- 用户接口
- 订单接口
- 通知接口
env:
- name: 开发环境
domain: http://dev-api.example.com
- name: 测试环境
domain: http://test-api.example.com
- name: 生产环境
domain: https://api.example.com文档管理流程:
- 合作伙伴提供 OpenAPI JSON/YAML 文件
- 导入到内部 YApi 或 Swagger UI 进行预览
- 团队评审接口设计合理性
- 生成 Mock 数据用于并行开发
- 联调阶段使用在线调试功能验证
签名验签机制
AK/SK 签名机制
AK(Access Key):身份标识,相当于用户名 SK(Secret Key):签名密钥,严格保密,不得在网络中明文传输
签名流程:
客户端 服务端
| |
| 1. 构造规范请求(CanonicalRequest) |
| 2. 使用 SK 对规范请求进行 HMAC-SHA256 签名 |
| 3. 拼接 Authorization 请求头 |
|-------------------------------------------->|
| | 4. 根据 AK 查询对应的 SK
| | 5. 使用同样的算法计算签名
| | 6. 比较签名是否一致
| 7. 返回响应数据 |
|<--------------------------------------------|签名计算步骤(Java 实现):
public class SignUtil {
private static final String ALGORITHM = "HMAC-SHA256";
private static final String HEADER_AUTH = "Authorization";
private static final String HEADER_TIMESTAMP = "X-Timestamp";
private static final String HEADER_NONCE = "X-Nonce";
private static final String HEADER_AK = "X-Access-Key";
private static final String HEADER_SIGNED_HEADERS = "X-Signed-Headers";
/**
* 生成签名
*
* @param ak Access Key
* @param sk Secret Key
* @param method HTTP 方法 (GET/POST/PUT/DELETE)
* @param path 请求路径 (如 /api/v1/users)
* @param queryString 查询字符串 (如 page=1&size=20)
* @param headers 需要签名的心跳头
* @param body 请求体内容
* @param timestamp 当前时间戳(毫秒)
* @param nonce 随机字符串
* @return 签名值
*/
public static String generateSignature(
String ak, String sk, String method, String path,
String queryString, Map<String, String> headers,
String body, long timestamp, String nonce) throws Exception {
// 1. 构建规范请求
String canonicalRequest = buildCanonicalRequest(method, path, queryString, headers, body);
// 2. 构建签名字符串
String stringToSign = timestamp + "\n" + nonce + "\n" + canonicalRequest;
// 3. 计算 HMAC-SHA256
Mac mac = Mac.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(sk.getBytes(StandardCharsets.UTF_8), ALGORITHM);
mac.init(keySpec);
byte[] signBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signBytes);
}
private static String buildCanonicalRequest(
String method, String path, String queryString,
Map<String, String> headers, String body) {
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase()).append("\n");
sb.append(path).append("\n");
sb.append(queryString != null ? queryString : "").append("\n");
// 对参与签名的请求头排序后拼接
if (headers != null) {
headers.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry ->
sb.append(entry.getKey().toLowerCase())
.append(":")
.append(entry.getValue().trim())
.append("\n"));
}
// 如果 body 为空,使用空字符串
String bodyContent = (body != null && !body.isEmpty()) ? body : "";
// 对 body 进行 SHA256 摘要
String bodyHash = DigestUtils.sha256Hex(bodyContent);
sb.append(bodyHash);
return sb.toString();
}
}请求头组装:
public class SignClient {
private final String ak;
private final String sk;
public HttpHeaders buildSignedHeaders(String method, String path,
String queryString, String body) {
long timestamp = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString().replace("-", "");
Map<String, String> headers = new LinkedHashMap<>();
headers.put("X-Timestamp", String.valueOf(timestamp));
headers.put("X-Nonce", nonce);
headers.put("X-Access-Key", ak);
headers.put("Content-Type", "application/json;charset=utf-8");
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::add);
try {
String signature = SignUtil.generateSignature(
ak, sk, method, path, queryString, headers, body, timestamp, nonce);
httpHeaders.add("Authorization", "SHA256-RSA " + signature);
} catch (Exception e) {
throw new RuntimeException("签名生成失败", e);
}
return httpHeaders;
}
}服务端验签:
@Component
public class SignAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private AccessKeyService accessKeyService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("SHA256-RSA ")) {
throw new AuthenticationException("缺少或无效的 Authorization 头");
}
String signature = authHeader.substring("SHA256-RSA ".length());
String ak = request.getHeader("X-Access-Key");
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
// 1. 校验时间戳是否在允许范围内(5 分钟)
long ts = Long.parseLong(timestamp);
if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
throw new AuthenticationException("请求已过期,请检查系统时间");
}
// 2. 校验 Nonce 是否已被使用(防重放)
if (nonceService.isNonceUsed(nonce)) {
throw new AuthenticationException("Nonce 已使用,疑似重放攻击");
}
// 3. 根据 AK 查询 SK
SecretKey secretKey = accessKeyService.getSecretKeyByAccessKey(ak);
if (secretKey == null) {
throw new AuthenticationException("无效的 Access Key");
}
// 4. 重新计算签名
String body = getRequestBody(request);
String queryString = request.getQueryString();
Map<String, String> signedHeaders = extractSignedHeaders(request);
try {
String expectedSign = SignUtil.generateSignature(
ak, secretKey.getSecretKey(),
request.getMethod(), request.getRequestURI(),
queryString, signedHeaders, body, ts, nonce);
if (!expectedSign.equals(signature)) {
throw new AuthenticationException("签名验证失败");
}
} catch (Exception e) {
throw new AuthenticationException("签名验证异常", e);
}
// 5. 标记 Nonce 已使用
nonceService.markNonceUsed(nonce, ts);
chain.doFilter(request, response);
}
}签名字段说明
| 请求头 | 必填 | 说明 |
|---|---|---|
Authorization | 是 | 签名值,格式 SHA256-RSA {signature} |
X-Access-Key | 是 | 分配给调用方的 Access Key |
X-Timestamp | 是 | 当前毫秒级时间戳,服务端校验 5 分钟内有效 |
X-Nonce | 是 | 随机字符串(如 UUID),一次使用,防重放攻击 |
X-Signed-Headers | 否 | 参与签名的请求头列表,多个用分号分隔 |
安全建议
- SK 存储在安全的密钥管理服务中(如 Vault、KMS),不在代码中硬编码
- 定期轮换 AK/SK,建议每 90 天更换一次
- 提供多个 AK/SK 用于轮换过渡期
- 监控 AK/SK 的异常使用行为(短时间内大量 401)
- 敏感接口增加额外的 IP 白名单校验
- Nonce 有效期与时间戳窗口一致,过期后清理
重试机制
指数退避(Exponential Backoff)
public class RetryTemplate {
private static final Logger log = LoggerFactory.getLogger(RetryTemplate.class);
private final int maxRetries;
private final long initialDelayMs;
private final long maxDelayMs;
private final double jitterFactor;
/**
* @param maxRetries 最大重试次数
* @param initialDelayMs 初始延迟(毫秒)
* @param maxDelayMs 最大延迟(毫秒)
* @param jitterFactor 抖动因子(0.0 ~ 1.0)
*/
public RetryTemplate(int maxRetries, long initialDelayMs,
long maxDelayMs, double jitterFactor) {
this.maxRetries = maxRetries;
this.initialDelayMs = initialDelayMs;
this.maxDelayMs = maxDelayMs;
this.jitterFactor = jitterFactor;
}
/**
* 执行带重试的操作
*
* @param callable 需要重试的操作
* @param <T> 返回值类型
* @return 操作结果
*/
public <T> T execute(RetryCallable<T> callable) {
Throwable lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
return callable.call(attempt);
} catch (Throwable e) {
lastException = e;
if (attempt >= maxRetries) {
log.error("所有重试均已耗尽,共重试 {} 次", attempt, e);
break;
}
if (!callable.isRetryable(e)) {
log.warn("异常不可重试,停止重试", e);
break;
}
long delay = computeDelay(attempt);
log.warn("第 {} 次重试,等待 {}ms,异常: {}", attempt + 1, delay, e.getMessage());
try {
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
}
}
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
throw new RuntimeException("重试耗尽", lastException);
}
/**
* 计算延迟时间:指数退避 + 抖动
* delay = min(initialDelay * 2^attempt, maxDelay)
* jitter = delay * (0.5 - random * jitterFactor)
*/
private long computeDelay(int attempt) {
double delay = initialDelayMs * Math.pow(2, attempt);
delay = Math.min(delay, maxDelayMs);
// 添加抖动(jitter),避免惊群效应
double jitter = delay * jitterFactor * (ThreadLocalRandom.current().nextDouble() - 0.5);
delay = delay + jitter;
return (long) Math.max(delay, 0);
}
public interface RetryCallable<T> {
T call(int attempt) throws Throwable;
default boolean isRetryable(Throwable e) {
// 默认只在网络异常和 5xx 时重试
if (e instanceof java.net.ConnectException) return true;
if (e instanceof java.net.SocketTimeoutException) return true;
if (e instanceof java.net.UnknownHostException) return true;
if (e instanceof RetryableException) return true;
return false;
}
}
public static class RetryableException extends RuntimeException {
public RetryableException(String message) {
super(message);
}
public RetryableException(String message, Throwable cause) {
super(message, cause);
}
}
}重试配置示例:
# application.yml
third-party:
retry:
max-retries: 3
initial-delay-ms: 1000
max-delay-ms: 30000
jitter-factor: 0.2
retryable-http-codes:
- 429 # Too Many Requests
- 500 # Internal Server Error
- 502 # Bad Gateway
- 503 # Service Unavailable
- 504 # Gateway Timeout使用示例:
@Service
public class PaymentService {
private final RetryTemplate retryTemplate = new RetryTemplate(3, 1000, 30000, 0.2);
public PaymentResult queryPayment(String orderNo) {
return retryTemplate.execute(attempt -> {
HttpHeaders headers = signClient.buildSignedHeaders(
"GET", "/api/v1/payments/" + orderNo, null, null);
ResponseEntity<String> response = restTemplate.exchange(
"https://third-party.com/api/v1/payments/{orderNo}",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class,
orderNo);
return objectMapper.readValue(response.getBody(), PaymentResult.class);
});
}
}重试注意事项
- 幂等性保证:重试的操作必须是幂等的,否则可能导致数据重复
- 退避策略选择:
- 普通错误间隔 1s -> 2s -> 4s -> 8s
- 限流错误间隔更长,配合
Retry-After响应头
- 最大重试次数:通常设置为 3 次,敏感操作不超过 5 次
- 抖动:避免多个客户端同时重试造成服务器压力尖峰
- 非幂等操作:POST 创建类接口不要自动重试,除非有去重机制
Spring Retry 集成
@Service
public class ExternalApiService {
@Retryable(
value = {RetryableException.class},
maxAttempts = 3,
backoff = @Backoff(
delay = 1000,
multiplier = 2.0,
maxDelay = 30000,
random = true // 添加随机抖动
)
)
public String callExternalApi(String param) {
// 调用第三方接口
return restTemplate.postForObject("https://api.example.com/service", param, String.class);
}
@Recover
public String recover(RetryableException e, String param) {
// 所有重试耗尽后的兜底处理
log.error("外部接口调用失败,参数: {}", param, e);
return fallbackResult;
}
}熔断降级
Sentinel 配置
# application.yml
spring:
cloud:
sentinel:
enabled: true
transport:
dashboard: localhost:8080
datasource:
ds1:
file:
file: classpath:sentinel-rules.json
data-type: json
rule-type: flow// sentinel-rules.json - 熔断降级规则
{
"degradeRules": [
{
"resource": "queryThirdPartyPayment",
"grade": 0,
"count": 500,
"timeWindow": 10,
"minRequestAmount": 5,
"statIntervalMs": 1000
}
]
}参数说明:
| 参数 | 说明 |
|---|---|
resource | 资源名,通常为方法签名或路径 |
grade | 熔断策略:0=RT,1=异常比例,2=异常数 |
count | 阈值(RT 为毫秒,异常比例为 0.0~1.0) |
timeWindow | 熔断持续时间(秒),过后进入半开状态 |
minRequestAmount | 触发熔断的最小请求数 |
statIntervalMs | 统计时间窗口(毫秒) |
@Component
public class ThirdPartyClient {
private static final Logger log = LoggerFactory.getLogger(ThirdPartyClient.class);
@SentinelResource(
value = "queryThirdPartyPayment",
fallback = "queryPaymentFallback",
blockHandler = "queryPaymentBlock"
)
public PaymentResult queryPayment(String orderNo) {
// 调用第三方支付查询接口
return doQuery(orderNo);
}
/**
* 熔断降级后的兜底方法
* 注意:参数列表需额外添加 BlockException 参数
*/
public PaymentResult queryPaymentBlock(String orderNo, BlockException e) {
log.warn("支付查询被熔断降级, orderNo: {}", orderNo, e);
return PaymentResult.degraded(orderNo);
}
/**
* 业务异常降级
*/
public PaymentResult queryPaymentFallback(String orderNo, Throwable e) {
log.error("支付查询异常, orderNo: {}", orderNo, e);
return PaymentResult.failed(orderNo, "查询异常");
}
}熔断降级策略
| 策略 | 触发条件 | 恢复机制 |
|---|---|---|
| RT 慢调用 | 请求响应时间超过阈值(如 3s) | 熔断窗口结束后允许少量请求探测 |
| 异常比例 | 异常数占总请求比例超过阈值(如 50%) | 同上 |
| 异常数 | 时间窗口内异常数超过阈值(如 10 次) | 同上 |
降级响应设计
// 熔断降级时的响应格式
{
"code": 5000,
"message": "服务暂不可用,请稍后重试",
"degraded": true,
"fallbackData": {}
}数据同步方案
方案对比
| 方案 | 实时性 | 复杂度 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 回调(Callback) | 高 | 中 | 中 | 支付结果通知、审核状态变更 |
| 轮询(Polling) | 低 | 低 | 高 | 订单状态同步、对账 |
| Webhook | 高 | 中 | 中 | 事件驱动的数据同步 |
| 消息队列 | 高 | 高 | 高 | 跨系统数据同步 |
| 定时批量同步 | 低 | 低 | 高 | 主数据同步(商品、用户) |
回调通知
# 回调地址配置
third-party:
callback:
payment:
url: https://our-system.com/api/callback/payment
retry-count: 3
retry-interval: 30s
timeout: 5000ms
signature-required: true@RestController
@RequestMapping("/api/callback")
public class CallbackController {
@PostMapping("/payment")
public ResponseEntity<String> handlePaymentCallback(
@RequestBody PaymentCallbackDTO callback,
@RequestHeader("X-Signature") String signature) {
// 1. 验签
if (!signService.verify(callback, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("签名验证失败");
}
// 2. 幂等处理(使用第三方交易号去重)
boolean processed = idempotentService.tryProcess(callback.getTransactionId());
if (!processed) {
return ResponseEntity.ok("SUCCESS"); // 已处理过,直接返回成功
}
// 3. 业务处理
try {
paymentService.handlePaymentCallback(callback);
return ResponseEntity.ok("SUCCESS");
} catch (Exception e) {
log.error("回调处理失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("FAILED");
}
}
}回调接口要求:
| 要求 | 说明 |
|---|---|
| 响应格式 | 固定响应字符串 SUCCESS / FAILED(非 JSON) |
| 幂等性 | 相同回调可能重复推送,需根据交易号去重 |
| 超时 | 回调接口应在 5 秒内返回响应 |
| 签名 | 回调请求必须携带签名用于验证 |
| 安全 | 回调地址应配置 IP 白名单,过滤非信任来源 |
轮询同步
@Component
public class PollingSyncTask {
@Scheduled(fixedDelay = 30000) // 每 30 秒轮询一次
public void syncPaymentStatus() {
List<String> pendingOrders = orderService.getPendingSyncOrders(50);
for (String orderNo : pendingOrders) {
try {
PaymentResult result = paymentClient.queryPayment(orderNo);
if (result.isFinalStatus()) {
orderService.updatePaymentStatus(orderNo, result.getStatus());
}
} catch (Exception e) {
log.warn("轮询同步失败, orderNo: {}", orderNo, e);
}
}
}
}Webhook
@Component
public class WebhookDispatcher {
private static final Logger log = LoggerFactory.getLogger(WebhookDispatcher.class);
private final RetryTemplate retryTemplate = new RetryTemplate(3, 5000, 60000, 0.1);
public void dispatch(WebhookEvent event) {
List<WebhookSubscription> subscriptions = subscriptionService
.findByEventType(event.getEventType());
for (WebhookSubscription sub : subscriptions) {
dispatcher.execute(() -> {
try {
String payload = objectMapper.writeValueAsString(event);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Webhook-Id", event.getEventId());
headers.set("X-Webhook-Signature",
signService.sign(payload, sub.getSecret()));
headers.set("X-Webhook-Timestamp",
String.valueOf(System.currentTimeMillis()));
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
ResponseEntity<String> response = restTemplate.exchange(
sub.getCallbackUrl(),
HttpMethod.POST,
entity,
String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RetryTemplate.RetryableException(
"Webhook 响应异常: " + response.getStatusCode());
}
// 记录推送成功
webhookLogService.recordSuccess(sub.getId(), event.getEventId());
} catch (Exception e) {
log.error("Webhook 推送失败, url: {}, event: {}",
sub.getCallbackUrl(), event.getEventId(), e);
// 记录推送失败
webhookLogService.recordFailure(sub.getId(), event.getEventId(), e.getMessage());
}
});
}
}
}Webhook 事件格式:
{
"eventId": "evt_20260709123000_001",
"eventType": "order.paid",
"occurredAt": "2026-07-09T12:30:00+08:00",
"data": {
"orderNo": "ORD20260709123000001",
"paymentNo": "PAY20260709123000001",
"amount": 10000,
"paidAt": "2026-07-09T12:30:00+08:00"
}
}Webhook 重试策略:
| 失败次数 | 延迟时间 | 重试窗口 |
|---|---|---|
| 第 1 次 | 30 秒 | 30 秒内 |
| 第 2 次 | 5 分钟 | 35 分钟内 |
| 第 3 次 | 30 分钟 | 1 小时内 |
| 第 4 次 | 2 小时 | 3 小时内 |
| 第 5 次 | 6 小时 | 9 小时内 |
| 第 6 次 | 24 小时 | 33 小时内(最终放弃) |
超时设置
超时参数规范
third-party:
timeout:
connect-timeout: 5000ms # 连接超时
read-timeout: 30000ms # 读取超时
write-timeout: 10000ms # 写入超时
connection-request-timeout: 1000ms # 从连接池获取连接的超时HTTP 客户端配置
RestTemplate 配置:
@Configuration
public class RestTemplateConfig {
@Value("${third-party.timeout.connect-timeout:5000}")
private int connectTimeout;
@Value("${third-party.timeout.read-timeout:30000}")
private int readTimeout;
@Bean
public RestTemplate restTemplate() {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setSocketTimeout(readTimeout)
.setConnectionRequestTimeout(1000)
.build();
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(50);
connectionManager.setValidateAfterInactivity(30000);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(connectTimeout);
factory.setReadTimeout(readTimeout);
factory.setConnectionRequestTimeout(1000);
RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
return restTemplate;
}
}WebClient 配置(Spring WebFlux):
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(30))
.addHandlerLast(new WriteTimeoutHandler(10)))
.connectionProvider(ConnectionProvider.builder("third-party")
.maxConnections(200)
.maxIdleTime(Duration.ofSeconds(30))
.maxLifeTime(Duration.ofMinutes(10))
.pendingAcquireTimeout(Duration.ofSeconds(5))
.build());
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(config -> config
.defaultCodecs()
.maxInMemorySize(2 * 1024 * 1024))
.build();
}
}超时建议值
| 接口类型 | 连接超时 | 读取超时 | 说明 |
|---|---|---|---|
| 内部服务(内网) | 2s | 10s | 内网延迟低 |
| 支付接口 | 5s | 30s | 支付网关处理较慢 |
| 通知推送 | 3s | 10s | 推送量大的场景 |
| 文件上传 | 10s | 120s | 大文件上传需更长读取超时 |
| 数据查询 | 5s | 30s | 复杂查询可能耗时较长 |
鉴权方式
API Key
最简单的鉴权方式,通过 Header 或 Query 传递 Key。
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: 在请求头中传递 API Key
security:
- ApiKeyAuth: []// 服务端验证
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private ApiKeyService apiKeyService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey == null) {
apiKey = request.getParameter("api_key");
}
if (apiKey == null || !apiKeyService.isValid(apiKey)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"message\":\"无效的 API Key\"}");
return;
}
chain.doFilter(request, response);
}
}适用场景: 内部服务间调用、开放平台公共接口 安全级别: 低(建议配合 IP 白名单使用)
JWT(JSON Web Token)
无状态鉴权方式,Token 中包含用户身份信息和声明。
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 在 Authorization 请求头中传递 Bearer Token
security:
- BearerAuth: []public class JwtTokenProvider {
private static final long ACCESS_TOKEN_EXPIRATION = 3600000; // 1 小时
private static final long REFRESH_TOKEN_EXPIRATION = 2592000000; // 30 天
private final String secretKey;
public JwtTokenProvider(String secretKey) {
this.secretKey = secretKey;
}
public String generateAccessToken(Long userId, String username, Set<String> roles) {
Date now = new Date();
Date expiry = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION);
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("username", username)
.claim("roles", roles)
.issuedAt(now)
.expiration(expiry)
.signWith(getSigningKey())
.compact();
}
public String generateRefreshToken(Long userId) {
Date now = new Date();
Date expiry = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION);
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("type", "refresh")
.issuedAt(now)
.expiration(expiry)
.signWith(getSigningKey())
.compact();
}
public Claims validateToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
}
}JWT 结构:
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {
"sub": "1001",
"username": "zhangsan",
"roles": ["ADMIN", "MANAGER"],
"iat": 1780444800,
"exp": 1780448400
}
Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret)适用场景: 用户登录态管理、前后端分离架构 安全级别: 中
OAuth2 Client Credentials
适用于服务间调用的 OAuth2 授权模式。
components:
securitySchemes:
OAuth2Client:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://auth.example.com/oauth/token
scopes:
read: 读取资源权限
write: 写入资源权限
refreshUrl: https://auth.example.com/oauth/token
security:
- OAuth2Client: [read, write]@Component
public class OAuth2Client {
private final WebClient webClient;
private final String tokenUrl;
private final String clientId;
private final String clientSecret;
private AccessToken cachedToken;
@PostConstruct
public void init() {
// 启动时预获取 Token
refreshToken();
}
public synchronized String getAccessToken() {
if (cachedToken == null || cachedToken.isExpired()) {
refreshToken();
}
return cachedToken.getToken();
}
private void refreshToken() {
try {
Map<String, String> body = new HashMap<>();
body.put("grant_type", "client_credentials");
body.put("client_id", clientId);
body.put("client_secret", clientSecret);
body.put("scope", "read write");
TokenResponse response = webClient.post()
.uri(tokenUrl)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(body))
.retrieve()
.bodyToMono(TokenResponse.class)
.block(Duration.ofSeconds(10));
cachedToken = new AccessToken(
response.getAccessToken(),
System.currentTimeMillis() + response.getExpiresIn() * 1000);
} catch (Exception e) {
log.error("获取 OAuth2 Token 失败", e);
throw new RuntimeException("鉴权失败", e);
}
}
public HttpHeaders buildAuthHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getAccessToken());
return headers;
}
}适用场景: 微服务间调用、第三方系统对接、无用户参与的自动化场景 安全级别: 高
AK/SK 签名
参考上一章节"签名验签机制"中的完整说明。
四种鉴权方式对比:
| 鉴权方式 | 安全级别 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| API Key | 低 | 低 | 内部服务、非敏感数据 |
| JWT | 中 | 中 | 用户认证、前后端分离 |
| OAuth2 Client Credentials | 高 | 中 | 服务间鉴权 |
| AK/SK 签名 | 高 | 高 | 第三方接口对接、开放平台 |
对接检查清单文档模板
以下模板用于第三方接口对接项目,在项目启动时填写,联合评审后在对接过程中持续更新。
---
项目名称: [第三方系统名称] 接口对接
文档版本: 1.0.0
创建日期: 2026-07-09
负责人: [姓名]
评审人: [姓名]
状态: [待评审 / 开发中 / 联调中 / 已上线]
---
## 一、基础信息
| 项目 | 内容 |
|------|------|
| 第三方系统名称 | |
| 对接负责人 | |
| 接口文档地址 | |
| 对接环境 | 开发 / 测试 / 生产 |
| 预计上线时间 | |
| 联系人(对方) | |
| 联系人(我方) | |
## 二、环境信息
### 开发环境
| 参数 | 值 |
|------|-----|
| API 基础地址 | |
| AK / Client ID | |
| SK / Client Secret | |
| 白名单 IP | |
| 回调地址 | |
### 测试环境
| 参数 | 值 |
|------|-----|
| API 基础地址 | |
| AK / Client ID | |
| SK / Client Secret | |
| 白名单 IP | |
| 回调地址 | |
### 生产环境
| 参数 | 值 |
|------|-----|
| API 基础地址 | |
| AK / Client ID | |
| SK / Client Secret | |
| 白名单 IP | |
| 回调地址 | |
## 三、接口清单
| 序号 | 接口名称 | 请求方法 | 路径 | 是否必接 | 状态 |
|------|---------|---------|------|---------|------|
| 1 | 查询用户信息 | GET | /api/v1/users/{id} | 必接 | 待开发 |
| 2 | 创建订单 | POST | /api/v1/orders | 必接 | 待开发 |
| 3 | 订单回调通知 | POST | 回调我方 | 必接 | 待开发 |
| 4 | 查询订单状态 | GET | /api/v1/orders/{id} | 选接 | 待评估 |
## 四、鉴权方式
| 检查项 | 说明 | 是否完成 |
|--------|------|---------|
| 鉴权方式 | API Key / JWT / OAuth2 / AK-SK | |
| 密钥获取方式 | 对方提供 / 我方申请 | |
| 密钥轮换机制 | 有 / 无,周期 | |
| 白名单配置 | 已配置 / 无需配置 | |
| Token 有效期 | 时长 | |
| Token 刷新机制 | 自动刷新 / 手动刷新 | |
## 五、接口规范检查
| 检查项 | 说明 | 通过 |
|--------|------|------|
| 请求头 Content-Type | application/json | |
| 请求头 Accept | application/json | |
| 统一响应格式 | code / message / data | |
| 错误码文档 | 提供错误码列表及含义 | |
| 分页参数格式 | page / size / total | |
| 日期时间格式 | ISO 8601 | |
| 金额单位 | 分(无小数点) | |
| 枚举值定义 | 提供完整枚举列表 | |
## 六、签名验签
| 检查项 | 说明 | 是否完成 |
|--------|------|---------|
| 签名算法 | HMAC-SHA256 / RSA-SHA256 | |
| 时间戳校验 | 服务端校验时间窗口(分钟) | |
| Nonce 防重放 | 已实现 / 不需要 | |
| 签名示例 | 对方提供正确签名示例 | |
| 我方签名实现 | 单元测试通过 | |
| 验签失败处理 | 记录日志 + 告警 | |
## 七、超时与重试
| 检查项 | 配置值 | 是否完成 |
|--------|--------|---------|
| 连接超时 | 毫秒 | |
| 读取超时 | 毫秒 | |
| 重试次数 | 次 | |
| 重试间隔 | 毫秒 | |
| 重试策略 | 固定间隔 / 指数退避 | |
| 幂等性保证 | 已实现 / 不需要(查询类) | |
## 八、熔断降级
| 检查项 | 配置值 | 是否完成 |
|--------|--------|---------|
| 熔断阈值 | RT / 异常比例 / 异常数 | |
| 熔断窗口 | 秒 | |
| 降级策略 | 返回兜底数据 / 返回错误 | |
| 降级响应格式 | 已定义 | |
| 熔断监控 | 已接入 Sentinel Dashboard | |
## 九、数据同步
| 检查项 | 是否完成 | 备注 |
|--------|---------|------|
| 回调接口已开发 | | |
| 回调签名已验证 | | |
| 回调幂等已实现 | | |
| 轮询定时任务已配置 | | |
| Webhook 订阅已配置 | | |
| 数据一致性校验方案 | | |
| 对账机制已实现 | | |
| 失败记录与重跑机制 | | |
## 十、安全审计
| 检查项 | 是否完成 | 备注 |
|--------|---------|------|
| 敏感数据加密传输(HTTPS) | | |
| 请求日志记录(含 traceId) | | |
| 敏感参数脱敏(密码、Token) | | |
| 调用频率限制 | | |
| IP 白名单配置 | | |
| AK/SK 不在代码中硬编码 | | |
| 密钥存储在配置中心或 KMS | | |
## 十一、监控告警
| 检查项 | 配置值 | 是否完成 |
|--------|--------|---------|
| 接口调用量监控 | | |
| 接口成功率告警(< 99%) | | |
| 接口响应时间告警(> 3s) | | |
| 熔断触发告警 | | |
| 认证失败告警 | | |
| 回调失败告警 | | |
| 日志已接入 ELK/Loki | | |
## 十二、联调检查清单
```mermaid
flowchart LR
A[接口定义评审] --> B[Mock 数据准备]
B --> C[单接口调试]
C --> D[业务流程串接]
D --> E[异常场景验证]
E --> F[性能压测]
F --> G[上线前Review]| 阶段 | 检查项 | 完成 |
|---|---|---|
| 接口定义评审 | 所有接口定义与对方确认 | |
| Interface Definition Review | Confirm all API definitions with provider | |
| Mock 数据准备 | 搭建 Mock Server 或使用 YApi Mock | |
| 单接口调试 | 每个接口单独调通 | |
| 业务流程串接 | 完整业务链路走通 | |
| 异常场景验证 | 网络超时、参数错误、鉴权失败、重复请求 | |
| 性能压测 | 确认接口能承载预期 QPS | |
| 上线前 Review | 检查清单逐项确认 |
十三、上线回滚方案
| 检查项 | 说明 | 是否完成 |
|---|---|---|
| 灰度发布 | 先切小部分流量验证 | |
| 手工补偿脚本 | 数据不一致时人工修复 | |
| 回滚开关 | 可快速切换至旧版逻辑 | |
| 监控观察期 | 上线后观察 30 分钟 | |
| 应急联系人 | 我方 + 对方联系方式 |
评审记录:
| 版本 | 评审人 | 评审日期 | 评审结论 | 备注 |
|---|---|---|---|---|
| 1.0.0 | 通过 / 有条件通过 / 不通过 |
使用说明:
1. 复制模板到对接项目文档目录
2. 逐项填写和检查
3. 评审会上逐项确认
4. 随着对接进展持续更新状态
5. 上线前所有检查项需标记为"已完成"