幂等性指南
Quick Reference
What使用幂等键防止重复收款
Why网络故障和重试绝不能导致对客户重复扣款
Reading Time8 分钟
Difficulty中级
Prerequisites可运行的 API 集成
在支付处理中,非幂等的重试是一个财务错误:
- 您的服务器发送收款请求
- 收款成功,但由于网络超时导致响应丢失
- 您的服务器重试——没有幂等键,A55 会创建第二笔收款
- 客户被重复扣款
幂等键解决了这个问题:如果 A55 收到重复的键,它会返回原始响应而不是创建新的收款。这使重试变得安全——无论您发送多少次请求,客户只会被收取一次费用。
工作原理
在每个 POST 请求中发送 Idempotency-Key 头部:
POST /api/v1/bank/wallet/charge/ HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
A55 存储该键及其关联的响应。如果在过期窗口内再次发送相同的键:
- 相同载荷 → 返回原始响应(HTTP 200/201)
- 不同载荷 → 返回 HTTP
409 Conflict
键格式
使用 UUID v4 作为幂等键:
550e8400-e29b-41d4-a716-446655440000
要求:
- 每个业务操作唯一(而非每次重试尝试)
- 只生成一次,在同一操作的所有重试尝试中重复使用
- 最少 16 个字符,最多 128 个字符
支持的端点
所有 POST 端点支持幂等性:
| 端点 | 用途 |
|---|---|
POST /api/v1/bank/wallet/ | 创建钱包 |
POST /api/v1/bank/wallet/charge/ | 创建收款 |
POST /api/v1/bank/wallet/charge/refund/ | 退款 |
POST /api/v1/bank/wallet/charge/capture/ | 扣款 |
POST /api/v1/bank/wallet/charge/cancel/ | 取消 |
GET、PUT 和 DELETE 请求天然具有幂等性,无需此头部。
重复请求行为
| 场景 | 结果 |
|---|---|
| 相同键 + 相同载荷 | 返回原始响应 |
| 相同键 + 不同载荷 | HTTP 409 Conflict |
| 过期后使用相同键(24 小时) | 视为新请求 |
| 未提供键 | 每次请求创建新资源 |
键过期
幂等键在 24 小时后过期。过期后,相同的键可以用于新请求。设计您的重试策略时,确保在此窗口内完成所有重试。
代码示例
- cURL
- Python
- Node.js
# 为此操作生成一个幂等键(仅一次)
IDEMPOTENCY_KEY=$(uuidgen)
# 首次尝试
curl -s -X POST "https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/" \
-H "Authorization: Bearer ${A55_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d '{
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": { "number": "4111111111111111", "holder_name": "TEST", "expiration_month": "12", "expiration_year": "2030", "cvv": "123" },
"payer": { "name": "张伟", "email": "zhangwei@example.com", "document": "12345678909", "document_type": "CPF" }
}'
# 使用相同的密钥重试——返回相同的响应,不会重复收费
curl -s -X POST "https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/" \
-H "Authorization: Bearer ${A55_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d '{
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": { "number": "4111111111111111", "holder_name": "TEST", "expiration_month": "12", "expiration_year": "2030", "cvv": "123" },
"payer": { "name": "张伟", "email": "zhangwei@example.com", "document": "12345678909", "document_type": "CPF" }
}'
import os
import uuid
import time
import random
import requests
def create_charge_idempotent(charge_data: dict, max_retries: int = 3):
idempotency_key = str(uuid.uuid4())
headers = {
"Authorization": f"Bearer {os.environ['A55_ACCESS_TOKEN']}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
for attempt in range(max_retries + 1):
try:
response = requests.post(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
headers=headers,
json=charge_data,
)
if response.status_code == 409:
raise ValueError(f"幂等性冲突:{response.json()}")
if response.status_code < 500:
return response.json()
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
except requests.ConnectionError:
if attempt == max_retries:
raise
time.sleep(2 ** attempt)
raise Exception("已超过最大重试次数")
charge = create_charge_idempotent({
"amount": "100.00",
"currency": "BRL",
"type_charge": "credit_card",
"card": {
"number": "4111111111111111",
"holder_name": "TEST",
"expiration_month": "12",
"expiration_year": "2030",
"cvv": "123",
},
"payer": {
"name": "张伟",
"email": "zhangwei@example.com",
"document": "12345678909",
"document_type": "CPF",
},
})
const crypto = require("crypto");
async function createChargeIdempotent(chargeData, maxRetries = 3) {
const idempotencyKey = crypto.randomUUID();
const headers = {
Authorization: `Bearer ${process.env.A55_ACCESS_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/",
{ method: "POST", headers, body: JSON.stringify(chargeData) }
);
if (res.status === 409) {
throw new Error(`幂等性冲突:${await res.text()}`);
}
if (res.status < 500) {
return await res.json();
}
const delay = (2 ** attempt + Math.random()) * 1000;
await new Promise(r => setTimeout(r, delay));
} catch (err) {
if (err.message.includes("Idempotency")) throw err;
if (attempt === maxRetries) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
throw new Error("已超过最大重试次数");
}
createChargeIdempotent({
amount: "100.00",
currency: "BRL",
type_charge: "credit_card",
card: {
number: "4111111111111111",
holder_name: "TEST",
expiration_month: "12",
expiration_year: "2030",
cvv: "123",
},
payer: {
name: "张伟",
email: "zhangwei@example.com",
document: "12345678909",
document_type: "CPF",
},
});
切勿为每次重试生成新键
幂等键必须每个业务操作只生成一次。如果您为每次重试尝试生成新键,将完全失去防重复保护。