需要关于 payment gateways 的入门建议
结论
第一次接触支付网关时,建议选择支持「托管收银台(Hosted Checkout)」的成熟支付服务商,并在测试环境中先跑通一条最小支付链路:
- 服务端创建支付订单或 Checkout Session。
- 前端跳转到服务商托管的支付页面。
- 支付完成后接收 Webhook 通知。
- 服务端验证通知签名,根据 Webhook 结果更新订单状态。
- 在测试环境中覆盖成功、失败、取消、退款和重复通知等场景。
刚开始接入时,不要自行采集银行卡号,也不要仅凭前端跳转结果判断支付是否成功。托管收银台可以减少安全合规和前端集成方面的工作,更适合初次接入支付系统的开发者。
支付网关解决什么问题
支付网关连接商户系统与银行、银行卡网络、电子钱包及其他支付渠道,通常承担以下工作:
- 安全传输支付信息;
- 创建和确认支付;
- 返回支付处理状态;
- 支持退款、撤销及争议处理;
- 通过 Webhook 异步通知支付结果;
- 提供风控、对账和交易查询能力。
「支付网关」「支付处理商」和「收单机构」在业务上并不是同一个概念,但有些服务商会将这些能力整合成统一产品。入门时,可以先确认产品能否覆盖收款、退款、通知和对账,不必马上弄清所有清算术语。
选择服务商时看哪些方面
业务和地区支持
先确认服务商是否支持:
- 商户注册地和目标市场;
- 所需币种;
- 银行卡、电子钱包或本地支付方式;
- 一次性付款、订阅或分期等业务模式;
- 所在行业和商品类型。
不同国家和地区常用的支付方式差别很大。面向中国内地用户与面向欧美用户时,适合的服务商和支付方式往往不同。
费用结构
除了交易费率,还要了解:
- 固定手续费;
- 跨境和货币转换费用;
- 退款后是否退还原交易手续费;
- 拒付或争议处理费;
- 提现费用及结算周期;
- 是否收取月费、最低消费或额外功能费。
实际费率会因地区、行业、交易量和合同而变化,应以服务商当前的公开文档和正式报价为准。
接入难度
对初学者来说,以下能力会直接影响接入体验:
- 完整且便于查阅的 API 文档;
- 可独立使用的测试环境;
- 官方 SDK;
- 托管收银台;
- 清晰的错误码;
- Webhook 重试和事件查询;
- 可以查看交易状态的管理后台。
安全与合规
如果系统直接处理银行卡号、有效期和安全码,通常需要承担更多 PCI DSS 合规责任。优先使用托管收银台或服务商提供的安全支付组件,让敏感信息直接提交给支付服务商。
无论采用哪种方式,都不应:
- 在日志中记录完整卡号或安全码;
- 将敏感支付数据保存在普通数据库中;
- 把 API Secret 放进浏览器代码或移动端安装包;
- 通过未加密的 HTTP 页面处理支付;
- 信任未经签名验证的 Webhook 请求。
推荐的学习顺序
1. 先理解订单和支付不是同一个对象
业务订单记录「用户购买了什么」,支付记录则表示「款项是否处理成功」。两者应分别保存,再通过内部订单号建立关联。
一张订单可能对应多次支付尝试。比如,用户第一次使用银行卡支付失败,之后改用电子钱包。因此,不能假设一张订单永远只有一条支付记录。
常见状态可以设计为:
Order:
PENDING -> PAID -> FULFILLED
-> CANCELLED
-> REFUNDED
Payment:
CREATED -> PROCESSING -> SUCCEEDED
-> FAILED
-> CANCELLED
状态名称没有统一标准,可以根据业务需要定义,但必须限制合法的状态转换。
2. 建立最小支付流程
基本流程如下:
浏览器 -> 商户服务端:提交购买请求
商户服务端 -> 支付网关:创建支付会话
支付网关 -> 商户服务端:返回支付页面 URL
商户服务端 -> 浏览器:返回 URL
浏览器 -> 支付网关:完成支付
支付网关 -> 商户 Webhook:异步通知支付结果
商户服务端 -> 数据库:更新订单和支付状态
前端成功页面可以用来展示结果,但不能作为发货依据。用户付款后可能直接关闭页面,也可能伪造跳转参数。最终支付状态应根据签名验证通过的 Webhook 确定,或者由服务端主动查询支付结果。
3. 在服务端创建支付会话
不同服务商使用的 SDK 和字段名称各不相同。下面是一段不依赖具体平台的示意代码:
app.post("/api/checkout", async (req, res) => {
const order = await createPendingOrder({
userId: req.user.id,
productId: req.body.productId
});
// 金额必须由服务端根据商品数据计算,不能信任前端传入的金额。
const amount = await calculateOrderAmount(order.id);
const session = await paymentGateway.createCheckoutSession({
amount,
currency: "USD",
reference: order.id,
successUrl: "https://example.com/payment/success",
cancelUrl: "https://example.com/payment/cancel",
idempotencyKey: `checkout:${order.id}`
});
await savePaymentAttempt({
orderId: order.id,
gatewayPaymentId: session.id,
status: "CREATED"
});
res.json({ checkoutUrl: session.url });
});
这段代码需要注意三点:
- 金额由服务端计算,防止用户篡改请求;
- 通过
reference等字段传递内部订单号; - 使用幂等键,避免重复点击或网络重试产生多笔支付。
createCheckoutSession、reference 和 idempotencyKey 只是示意名称,实际参数应以所选服务商最新的 API 文档为准。
4. 正确处理 Webhook
Webhook 端点应读取原始请求体,并按照服务商提供的方法验证签名:
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = paymentGateway.verifyWebhook({
rawBody: req.body,
signature: req.headers["payment-signature"],
secret: process.env.PAYMENT_WEBHOOK_SECRET
});
} catch {
return res.status(400).send("Invalid signature");
}
const alreadyProcessed = await hasProcessedEvent(event.id);
if (alreadyProcessed) {
return res.sendStatus(200);
}
if (event.type === "payment.succeeded") {
await markOrderAsPaid({
orderId: event.data.reference,
gatewayPaymentId: event.data.id,
eventId: event.id
});
}
if (event.type === "payment.failed") {
await markPaymentAsFailed({
gatewayPaymentId: event.data.id,
eventId: event.id
});
}
res.sendStatus(200);
}
);
生产代码还要处理数据库事务、并发更新和异常重试。Webhook 可能重复发送,事件也不一定按照发生顺序到达,所以处理逻辑必须具备幂等性。
如果事件处理耗时较长,可以先将事件可靠地保存下来并返回成功响应,再交给后台任务继续处理。事件尚未入库时不能直接返回成功,否则系统一旦崩溃,这条通知可能永久丢失。
必须测试的场景
上线前至少要覆盖以下情况:
- 支付成功;
- 支付被拒绝;
- 用户主动取消;
- 支付处于处理中,稍后才成功或失败;
- 用户重复点击付款按钮;
- 创建支付请求超时,但网关实际已经收到请求;
- Webhook 重复发送;
- Webhook 延迟或乱序;
- Webhook 签名无效;
- 全额退款和部分退款;
- 金额或币种不匹配;
- 支付成功后业务处理失败;
- 同一订单出现多个支付尝试。
测试环境只能验证基本流程是否正确。正式上线前,还应使用服务商允许的小额真实交易检查生产配置、回调地址、结算和退款流程,具体操作应遵守服务商规则。
常见误区
使用浮点数计算金额
不要用 10.1 + 20.2 这样的浮点运算处理货币。金额通常应以最小货币单位的整数表示,例如:
const amount = 1099; // USD 10.99,单位为 cent
不同币种使用的最小单位不一定相同,不能假设所有币种都有两位小数。
依赖前端返回的金额和商品信息
前端数据可能被修改。商品价格、优惠、税费、运费和最终应付金额都应由服务端重新计算。
把超时当成支付失败
请求超时只表示客户端没有及时收到结果,并不代表网关没有处理请求。发生超时时,应使用幂等键重试,或者根据订单号查询支付结果,不能直接创建一笔新支付。
收到成功事件就立即重复发货
Webhook 可能会重试。应根据事件 ID、支付 ID 或订单状态进行去重,确保同一笔支付只触发一次发货、充值或权益发放。
忽略对账
即使 Webhook 工作正常,也要定期核对本地支付记录、网关交易记录和实际结算记录。通过对账,可以发现通知丢失、金额异常、退款遗漏,以及人工操作引起的状态差异。
上线前检查清单
- API Secret 只保存在服务端,通过环境变量或密钥管理服务加载;
- 测试密钥与生产密钥严格分离;
- Webhook 使用 HTTPS,并完成签名验证;
- 金额、币种和订单归属均在服务端校验;
- 创建支付、退款和业务履约操作具备幂等性;
- 日志不包含银行卡数据、密钥或完整的个人敏感信息;
- 订单、支付尝试、退款和 Webhook 事件都可以追踪;
- 已配置监控、错误告警和人工补偿流程;
- 已测试退款、争议及支付失败场景;
- 已确认服务商费率、结算周期和合规要求;
- 已准备隐私政策、退款政策和客服处理流程。
第一次集成支付系统时,先把一笔测试订单的创建、付款、通知、入账和退款流程可靠地跑通。完成这条链路后,再逐步加入订阅、本地支付方式、风控和自动对账等能力。
备注:内容仅供参考。
