PAYATHON 2026

如何为在线软件接入信用卡定期付款与多币种支付?

支付阿杰

结论

这类需求可以选择支持订阅、Webhook 和多币种结算的支付网关。大多数在线软件或 SaaS 可以优先考虑 Stripe Billing。业务覆盖国家较多、支付方式复杂或交易规模较大时,可以评估 Adyen。如果已经在使用 PayPal 生态,也可以考虑 Braintree。

如果希望平台代为处理销售税、VAT、发票及部分合规事务,可以评估 Paddle 等 Merchant of Record 服务。这类服务与普通 payment gateway 的法律关系和结算模式不同,接入前需要确认费率、退款规则以及商户主体关系。

无论选择哪家服务商,都不建议让信用卡号经过自己的服务器。更合适的做法是在软件界面中嵌入服务商提供的安全组件,由服务商直接收集卡片信息,再返回 token、PaymentMethod ID 等非敏感标识。这样既能保留站内付款体验,也能大幅缩小 PCI DSS 合规范围。

推荐的实现方式

典型的支付流程如下:

  1. 客户端加载支付服务商提供的 SDK 或托管支付组件。
  2. 用户在组件中填写信用卡信息。
  3. 卡片数据直接发送给支付服务商,不经过业务服务器。
  4. 业务服务器创建客户、价格和订阅。
  5. 支付服务商按账期自动扣款。
  6. 扣款成功、失败、退款或订阅状态发生变化时,支付服务商调用 Webhook。
  7. 软件验证 Webhook 签名,并更新本地订阅权限。

不要仅凭用户支付后跳转到了”成功页面”来判断付款结果。用户可能关闭页面,网络也可能中断。经过签名验证的 Webhook 才是可信的结果来源。

使用 Stripe Billing 的示例流程

下面使用 Node.js 和 Express 展示核心逻辑。代码仅用于说明结构,实际字段应以接入时使用的 Stripe SDK 和官方 API 文档为准。

1. 为不同币种配置价格

订阅价格通常绑定一种币种。可以为同一个套餐分别建立 USD、EUR、GBP 等价格,并在数据库中保存允许使用的 price ID:

const PRICE_MAP = {
  basic: {
    usd: "price_basic_usd",
    eur: "price_basic_eur",
    gbp: "price_basic_gbp"
  },
  pro: {
    usd: "price_pro_usd",
    eur: "price_pro_eur",
    gbp: "price_pro_gbp"
  }
};

不要接受客户端直接提交的任意 price ID 或金额,否则用户可能篡改请求。客户端只需提交套餐和币种,服务器再从白名单中选择对应价格。

2. 在服务器创建订阅

import express from "express";
import Stripe from "stripe";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const PRICE_MAP = {
  basic: {
    usd: "price_basic_usd",
    eur: "price_basic_eur"
  }
};

app.use("/api", express.json());

app.post("/api/subscriptions", async (req, res) => {
  try {
    const { customerId, paymentMethodId, plan, currency } = req.body;

    const normalizedCurrency = String(currency).toLowerCase();
    const priceId = PRICE_MAP[plan]?.[normalizedCurrency];

    if (!priceId) {
      return res.status(400).json({
        error: "Unsupported plan or currency"
      });
    }

    await stripe.paymentMethods.attach(paymentMethodId, {
      customer: customerId
    });

    await stripe.customers.update(customerId, {
      invoice_settings: {
        default_payment_method: paymentMethodId
      }
    });

    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: "default_incomplete",
      expand: ["latest_invoice.payment_intent"],
      metadata: {
        internalPlan: plan
      }
    });

    const paymentIntent =
      subscription.latest_invoice?.payment_intent;

    res.json({
      subscriptionId: subscription.id,
      status: subscription.status,
      clientSecret: paymentIntent?.client_secret ?? null
    });
  } catch (error) {
    res.status(400).json({
      error: error instanceof Error ? error.message : "Payment error"
    });
  }
});

首次付款可能需要进行 3-D Secure 或其他 Strong Customer Authentication 验证。客户端应使用返回的 clientSecret 完成后续确认,不能假定订阅创建成功就代表付款成功。

卡片输入应使用 Stripe Elements、Payment Element 或 Checkout 等官方组件。不要自行制作普通 <input> 来收集卡号,再把数据提交给业务服务器。

3. 接收定期扣款通知

Webhook 路由通常需要读取原始请求体,否则可能无法正确验证签名:

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["stripe-signature"];

    let event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        signature,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch {
      return res.status(400).send("Invalid webhook signature");
    }

    switch (event.type) {
      case "invoice.paid": {
        const invoice = event.data.object;

        // 根据 customer 或 subscription 查找本地账户
        // 将对应账期标记为已付款
        await markInvoicePaid({
          providerEventId: event.id,
          invoiceId: invoice.id,
          customerId: invoice.customer,
          subscriptionId: invoice.subscription
        });
        break;
      }

      case "invoice.payment_failed": {
        const invoice = event.data.object;

        await markPaymentFailed({
          providerEventId: event.id,
          invoiceId: invoice.id,
          customerId: invoice.customer,
          subscriptionId: invoice.subscription
        });
        break;
      }

      case "customer.subscription.updated":
      case "customer.subscription.deleted": {
        const subscription = event.data.object;

        await syncSubscriptionStatus({
          providerEventId: event.id,
          subscriptionId: subscription.id,
          status: subscription.status
        });
        break;
      }
    }

    res.sendStatus(200);
  }
);

markInvoicePaid 等函数应使用数据库事务,并为 event.id 建立唯一约束。支付平台可能重复投递同一个事件,也不保证不同类型的事件会严格按照业务顺序到达,因此 Webhook 处理必须具备幂等性。

不要在 Webhook 中执行耗时任务。可以先验证签名、保存事件并返回 2xx,再通过任务队列异步开通权限、发送邮件或生成发票。

多币种支付需要区分三个概念

“支持多币种”不只是让界面显示不同货币,还要分别确认:

  • 呈现币种:用户看到并实际支付的币种。
  • 定价币种:套餐采用的计价币种,例如 USD 19 或 EUR 17。
  • 结算币种:支付服务商最终向商户银行账户打款时使用的币种。

即使支付服务商允许用户用 EUR 付款,也不代表服务商一定能将 EUR 原币打入商户账户。服务商可能先按照自己的汇率转换为商户的结算币种,并收取换汇费用。

通常应该为每个套餐设置明确的币种价格,而不是在扣款时自行按照实时汇率换算。例如:

Basic / USD / 19.00 per month
Basic / EUR / 17.00 per month
Basic / GBP / 15.00 per month

固定价格更稳定,也方便处理退款、财务核对和续费通知。还需要提前规定用户更改币种时如何处理,例如立即切换订阅、下个账期生效,或者取消旧订阅后重新创建。不要直接修改正在结算的账单金额。

如何选择支付服务商

可以从以下方面进行比较:

需求重点核对内容
定期付款是否提供原生订阅、试用期、升级降级、按比例计费和自动重试
结果通知是否提供签名 Webhook、事件重试和事件查询
多币种支付币种、结算币种、换汇规则及不同币种的最低金额
卡片安全是否有 Hosted Fields、Elements 或托管结账页
身份验证是否支持 3-D Secure、SCA 等当地要求
支付方式是否还需要 Apple Pay、Google Pay、SEPA 等方式
财务能力发票、退款、争议处理、对账和收入确认
商户条件是否支持公司的注册国家、行业和银行账户
成本交易费、订阅服务费、跨境费、换汇费和拒付费

对于希望快速接入 SaaS 订阅,并自行控制产品和税务流程的团队,Stripe 通常比较合适。Adyen 更适合支付方式多、覆盖地区广、交易规模较大的业务。Paddle 则适合希望由第三方作为记录商户,负责付款及部分税务工作的软件厂商,但它的业务控制方式和成本结构也有所不同。

服务商支持的国家、币种、费率和产品能力可能调整。最终选择前,应结合公司的注册地、用户所在地、预计交易量和结算账户,核对各服务商当前的官方支持范围。

容易忽略的事项

  • 绝不能在日志、数据库或错误追踪系统中记录完整卡号、CVC 或客户端密钥。
  • Secret Key 和 Webhook Secret 只能保存在服务器端的密钥管理系统或环境变量中。
  • Webhook 必须验证签名,并防止重复处理。
  • 软件权限应由订阅和账单状态决定,不能只看客户端返回的结果。
  • 扣款失败后通常需要设置宽限期和重试策略,不宜在收到一次失败通知后立即删除客户数据。
  • 需要处理退款、拒付、订阅取消、试用结束和付款方式过期等事件。
  • 测试环境与生产环境的 API 密钥、价格 ID、客户 ID 和 Webhook 地址必须彻底隔离。
  • 上线前需要完成退款条款、隐私政策、服务条款,以及适用地区的税务和消费者保护合规检查。

对于标准 SaaS 场景,使用 Stripe Billing、支付组件、服务端订阅接口和签名 Webhook,通常可以满足信用卡定期付款、付款结果通知和多币种支付的需求。最终如何选择,主要取决于商户注册地、目标市场、税务承担方式,以及团队希望保留多少支付流程的控制权。

备注:内容仅供参考。