
3分钟看懂国际支付源码,拒绝官方文档长篇大论
官方文档往往厚达数百页,API 列表密密麻麻,新人一看就头晕,根本抓不住核心逻辑。很多开发者在对接国际支付时,陷入“看文档 - 写代码 - 报错 - 再查文档”的死循环,效率极低。其实,剥离掉营销话术和冗余配置,国际支付的核心链路非常清晰,只需要通过图解原理拆解底层数据流,就能在 10 分钟内建立完整认知。
本文将结合 stripe-node 等 NPM/PyPI 官方包 的实际源码逻辑,带你穿透表象,直击支付网关的心脏。我们不讲空洞的理论,只讲代码里跑通的真相,帮你把复杂系统变成可控的黑盒。
入口定位:请求是如何进入支付大脑的
在深入源码之前,必须先明确一个概念:国际支付系统不是一个单体应用,而是一个分布式的状态机。当你点击“支付”按钮时,前端发起的 POST /createPaymentIntent 请求,实际上只是整个链路的冰山一角。
以业界标准的 stripe-node SDK 为例,其入口函数通常位于 StripeClient 类中。这个类充当了“指挥官”的角色,它负责鉴权、序列化请求、处理重试以及解析响应。
// 源码片段 1:StripeClient 核心请求处理逻辑 (简化版)
// 文件路径: lib/StripeClient.js (伪代码结构,基于真实 SDK 逻辑)
class StripeClient {
constructor(apiKey) {
this._apiKey = apiKey; // 存储私钥,用于 HMAC 签名验证
this._baseURL = 'https://api.stripe.com/v1'; // 官方网关地址
}
async request(method, path, params) {
// 1. 构建完整 URL
const url = `${this._baseURL}${path}`;
// 2. 准备请求头,Authorization 是核心
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${this._apiKey}`
};
// 3. 参数序列化,注意:Stripe 后端要求表单格式而非 JSON
const body = this._serialize(params);
try {
// 4. 发起 HTTP 请求,这里通常包裹了 fetch 或 axios
const response = await fetch(url, {
method,
headers,
body,
});
// 5. 解析响应 JSON
const data = await response.json();
// 6. 错误拦截:非 2xx 状态码抛出特定异常
if (!response.ok) {
throw new StripeError(data.error.message, response.status);
}
return data;
} catch (error) {
// 7. 网络层重试逻辑(指数退避策略)
if (error instanceof NetworkError this._retryCount 3) {
return this._retryRequest(method, path, params);
}
throw error;
}
}
}
逐行解析:
constructor: 初始化时绑定 API Key,这是身份验证的基石。
request: 这是所有 SDK 调用的统一出口。注意 Content-Type 是 form-urlencoded,这是 Stripe 早期为了兼容各种后端语言做出的妥协,也是很多初学者容易踩的坑(以为要传 JSON)。
_serialize: 将嵌套对象扁平化。例如 { card: { number: '4242...' } } 会变成 card[number]=4242...。
retry: 支付系统必须高可用,网络抖动是常态,SDK 内置的重试机制保证了最终一致性。
理解了这个入口,你就知道,所谓的“调用支付接口”,本质上就是一个带有鉴权头的 HTTP POST 请求,返回的是一个包含状态信息的 JSON 对象。
核心片段:PaymentIntent 的状态流转
国际支付最核心的概念是 PaymentIntent(支付意图)。它不仅仅是一个订单,更是一个状态容器,记录了从“创建”到“成功/失败”的全过程。
在源码层面,PaymentIntent 的创建与更新是分步进行的。让我们看看当用户输入卡号后,后端代码是如何驱动状态变化的。
# 源码片段 2:PaymentIntent 状态机处理 (Python 示例,逻辑同构)
# 依赖包: stripe-python (PyPI 官方包)
import stripe
def process_payment(customer_id, amount, currency='usd'):
处理支付流程的核心函数
# 1. 创建 PaymentIntent
# 注意: capture_method='automatic' 表示扣款成功后自动捕获资金
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
customer=customer_id,
automatic_payment_methods={'enabled': True},
metadata={'order_id': 'ORD_12345'}
)
# 2. 获取 Client Secret,用于前端唤起收银台
# 这是一个一次性令牌,前端用它来与 Stripe.js 交互
client_secret = intent['client_secret']
# 3. 模拟前端支付回调 (Webhook)
# 实际生产中,这是通过 Webhook 事件触发的异步处理
# 假设前端支付成功,Stripe 会发送 payment_intent.succeeded 事件
if intent['status'] == 'requires_payment_method':
# 状态 1: 需要用户提供支付方式
# 此时资金未冻结,仅建立了意向
print(等待前端提交卡号信息...)
elif intent['status'] == 'requires_confirmation':
# 状态 2: 支付方式已提交,等待银行授权
# 此时可能触发 3DS 验证
print(正在与发卡行通信,验证 3DS...)
elif intent['status'] == 'requires_capture':
# 状态 3: 授权成功,资金已冻结,等待商户捕获
# 适用于预付卡或需要延迟结算的场景
print(授权成功,正在捕获资金...)
# 手动捕获资金
stripe.PaymentIntent.confirm(intent['id'])
elif intent['status'] == 'succeeded':
# 状态 4: 支付完成,资金已入账
print(支付成功!更新本地订单状态...)
return {'success': True, 'transaction_id': intent['id']}
else:
# 状态 5: 失败
print(支付失败: , intent['last_payment_error'])
return {'success': False}
return {'success': False, 'client_secret': client_secret}
逐行解析:
PaymentIntent.create: 这一步只是“预约”。此时没有任何资金移动,只是告诉 Stripe:“我要收这么多钱,给这个客户”。
client_secret: 这是一个安全设计。前端不需要知道 API Key,只需要拿着这个 secret 去和 Stripe 的 JS SDK 交互,由浏览器端完成卡号收集。
status 字段: 这是整个系统的灵魂。requires_payment_method 到 succeeded 的每一步流转,都对应着银行侧的一次交互。
metadata: 用于关联本地业务数据。当 Webhook 回调时,你通过这个字段找回你的本地订单 ID。
图解原理:
想象一个漏斗:
顶部:用户点击支付,创建 Intent(漏斗口)。
中部:卡号传输,3DS 验证(漏斗颈,最容易卡住的地方)。
底部:银行授权,资金捕获(漏斗底,出水口)。
如果中间任何环节断开(如用户关闭浏览器、银行拒绝),状态就会停留在 requires_confirmation 或变为 canceled,这就是为什么你需要处理 Webhook 来同步最终状态的原因。
设计思想:为什么是异步 Webhook 而不是同步返回?
很多初学者疑惑:既然 confirm 之后有状态,为什么还要监听 Webhook?直接同步返回结果不行吗?
答案在于解耦与最终一致性。
网络不可靠:支付请求涉及多个第三方(你的服务器、Stripe 服务器、发卡行、收单行)。任何一个节点超时,同步响应都可能丢失。
耗时差异:支付授权可能瞬间完成,也可能需要 30 秒进行 3DS 挑战。如果同步等待,用户体验极差。
状态同步:Webhook 是 Stripe 主动推送的“真相”。无论前端页面是否关闭,无论网络是否中断,Stripe 都会确保 Webhook 事件最终送达你的服务器。
源码中的 Webhook 验证逻辑:
// 源码片段 3:Webhook 签名验证 (安全核心)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) = {
let event;
try {
// 1. 验证签名,防止伪造请求
// 必须使用原始 body (raw),不能是 JSON 解析后的对象
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
// 签名验证失败,直接拒绝
console.log(`Webhook signature verification failed.`);
res.sendStatus(400);
return;
}
// 2. 事件路由
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
// 执行本地业务逻辑:发货、更新数据库
handlePaymentSuccess(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedIntent = event.data.object;
// 执行失败逻辑:记录日志、通知用户
handlePaymentFailure(failedIntent);
break;
}
// 3. 快速响应,避免 Stripe 认为你超时并重试
res.json({ received: true });
});
设计精髓:
express.raw: 必须使用原始字节流进行签名验证。如果先 JSON.parse,哈希值就会改变,导致验证失败。这是最经典的坑。
幂等性: Webhook 可能会重试。你的 handlePaymentSuccess 必须设计成幂等的(即执行多次效果相同),避免重复发货。
快速 ACK: 验证通过后立即返回 200,耗时操作放入消息队列(如 Redis/RabbitMQ)异步处理。
手写简化版:构建一个迷你支付网关
为了彻底吃透原理,我们手写一个极简版的支付网关,模拟 Stripe 的核心流程。
场景: 用户购买一个 $10.00 的商品。
1. 定义数据模型
# models.py
from enum import Enum
class PaymentStatus(Enum):
REQUIRES_PAYMENT_METHOD = requires_payment_method
REQUIRES_CAPTURE = requires_capture
SUCCEEDED = succeeded
FAILED = failed
class PaymentIntent:
def __init__(self, amount, currency, customer_id):
self.id = fpi_{generate_uuid()}
self.amount = amount
self.currency = currency
self.customer_id = customer_id
self.status = PaymentStatus.REQUIRES_PAYMENT_METHOD.value
self.last_error = None
2. 模拟支付处理引擎
# engine.py
import random
from models import PaymentIntent, PaymentStatus
class PaymentEngine:
def create_intent(self, amount, currency, customer_id):
return PaymentIntent(amount, currency, customer_id)
def confirm_payment(self, intent_id, card_token):
intent = self.get_intent(intent_id)
# 1. 状态检查:是否已经成功或失败?
if intent.status in [PaymentStatus.SUCCEEDED.value, PaymentStatus.FAILED.value]:
raise Exception(Payment already processed)
# 2. 模拟银行交互 (3DS Verification)
# 假设 10% 概率失败
if random.random() 0.1:
intent.status = PaymentStatus.FAILED.value
intent.last_error = Card declined by issuer
return intent
# 3. 模拟授权成功
intent.status = PaymentStatus.REQUIRES_CAPTURE.value
# 4. 模拟自动捕获 (Automatic Capture)
intent.status = PaymentStatus.SUCCEEDED.value
return intent
3. 服务层 (API)
# api.py
from flask import Flask, request, jsonify
from engine import PaymentEngine
app = Flask(__name__)
engine = PaymentEngine()
@app.route('/create', methods=['POST'])
def create_payment():
data = request.json
intent = engine.create_intent(
amount=data['amount'],
currency=data['currency'],
customer_id=data['customer_id']
)
return jsonify({
'id': intent.id,
'client_secret': f{intent.id}_secret_{random_string()},
'status': intent.status
})
@app.route('/confirm', methods=['POST'])
def confirm_payment():
data = request.json
try:
intent = engine.confirm_payment(
intent_id=data['id'],
card_token=data['card_token']
)
# 模拟 Webhook 触发 (实际中是异步的)
if intent.status == succeeded:
trigger_webhook('payment_intent.succeeded', intent)
return jsonify({'status': intent.status})
except Exception as e:
return jsonify({'error': str(e)}), 400
这个简化版揭示了什么?
状态机是核心:所有逻辑都围绕 status 的流转。
Token 化:前端提交的是 card_token,而不是原始卡号。这是 PCI-DSS 合规的关键。
异步通知:trigger_webhook 解耦了支付处理与业务落地。
应用场景与避坑指南
理解了源码和设计思想,在实际项目中你会遇到以下典型场景:
多币种结算:
痛点:汇率波动导致金额不一致。
解法:在创建 PaymentIntent 时指定 currency,但金额需经过汇率转换。建议在本地数据库存储原始币种和金额,避免二次转换误差。
退款流程:
原理:退款也是一个状态机。Refund 对象关联 PaymentIntent。
代码:stripe.Refund.create({'payment_intent': intent_id, 'amount': refund_amount})。
注意:部分退款需记录剩余可退金额,防止超退。
Webhook 丢失处理:
痛点:网络故障导致 Webhook 未送达。
解法:实现对账机制。每天定时任务,拉取 Stripe 后台的交易列表,与本地数据库比对。对于状态不一致的记录,手动触发补偿逻辑。
日志与排查:
技巧:记录 Stripe-Request-Id 响应头。当出现问题时,拿着这个 ID 去 Stripe Dashboard 查询,能精确定位到每一次 API 调用的请求体和响应体。
避坑清单:
❌ 不要在前端存储 API Secret Key。
❌ 不要依赖同步 HTTP 响应作为支付成功的唯一依据。
❌ 不要忽略 Webhook 的签名验证。
❌ 不要在生产环境使用 Test Card 进行真实交易。
国际支付看似复杂,实则是由状态机、异步消息和安全令牌构成的精密仪器。通过图解原理拆解源码,你不再是被文档淹没的菜鸟,而是能驾驭数据流的工程师。
还有什么不懂的?评论区留言挨个回