PAYATHON 2026

需要关于 payment gateways 的入门建议

支付小周

结论

第一次接触支付网关时,建议选择支持「托管收银台(Hosted Checkout)」的成熟支付服务商,并在测试环境中先跑通一条最小支付链路:

  1. 服务端创建支付订单或 Checkout Session。
  2. 前端跳转到服务商托管的支付页面。
  3. 支付完成后接收 Webhook 通知。
  4. 服务端验证通知签名,根据 Webhook 结果更新订单状态。
  5. 在测试环境中覆盖成功、失败、取消、退款和重复通知等场景。

刚开始接入时,不要自行采集银行卡号,也不要仅凭前端跳转结果判断支付是否成功。托管收银台可以减少安全合规和前端集成方面的工作,更适合初次接入支付系统的开发者。

支付网关解决什么问题

支付网关连接商户系统与银行、银行卡网络、电子钱包及其他支付渠道,通常承担以下工作:

  • 安全传输支付信息;
  • 创建和确认支付;
  • 返回支付处理状态;
  • 支持退款、撤销及争议处理;
  • 通过 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 事件都可以追踪;
  • 已配置监控、错误告警和人工补偿流程;
  • 已测试退款、争议及支付失败场景;
  • 已确认服务商费率、结算周期和合规要求;
  • 已准备隐私政策、退款政策和客服处理流程。

第一次集成支付系统时,先把一笔测试订单的创建、付款、通知、入账和退款流程可靠地跑通。完成这条链路后,再逐步加入订阅、本地支付方式、风控和自动对账等能力。

备注:内容仅供参考。