PAYATHON 2026

移动应用如何让用户直接向第三方付款?

支付阿杰

明确结论

可以实现,但不能使用普通的个人收款接口,也不应由应用保存第三方银行卡信息并自行发起转账。合适的做法是接入支付机构提供的”平台支付”或”市场支付”产品,例如 marketplace payment、connected accounts、sub-merchant 或分账服务。

通常按以下流程处理:

  1. 第三方收款人入驻支付平台,完成个人实名认证或企业认证。
  2. 用户在应用内付款。
  3. 支付机构处理资金,并将款项结算至第三方账户。
  4. 你的服务器负责创建订单、指定收款方、接收异步通知和记录交易状态。
  5. 如果业务需要,支付机构可以从交易款中扣除平台服务费。

可评估的产品包括 Stripe Connect、Adyen for Platforms、PayPal Commerce Platform,以及支付宝、微信支付等面向平台或服务商的商户入驻和分账产品。产品是否开放、支持哪些国家和业务类型,需要以当地产品文档和支付机构的审核结果为准。

为什么普通支付接口不支持

普通收款接口通常只对应一个签约商户。用户付款后,资金会进入该商户的支付账户,无法在每笔交易中随意指定另一个个人或企业作为最终收款人。

支付机构限制这类能力,主要是因为需要处理以下问题:

  • 收款人的身份认证,即 KYC 或 KYB。
  • 反洗钱、制裁名单和异常交易审查。
  • 退款、拒付和欺诈损失的责任归属。
  • 税务申报、发票和交易记录保存。
  • 平台是否构成资金托管、支付服务或汇款业务。
  • 用户付款时看到的商户名称是否与实际收款方一致。

因此,“不经过自己的银行账户”不等于可以避开平台责任。即使支付机构把资金直接结算给第三方,应用运营方仍可能需要负责收款人入驻、争议处理和风险控制。

推荐的系统结构

移动应用
   │
   │ 创建订单
   ▼
业务服务器
   │
   ├── 校验商品、金额和收款人
   ├── 请求支付机构创建支付
   └── 保存支付机构返回的 payment_id
   │
   ▼
支付机构
   ├── 向用户收款
   ├── 扣除支付手续费或平台服务费
   └── 将资金结算给已认证的第三方账户
   │
   ▼
第三方收款人

移动应用不应自行决定最终金额和收款账户。客户端传来的 amount、currency、recipient_id 等字段都可能被篡改,服务器必须根据订单数据重新计算并校验。

实现步骤

1. 确定业务中的法律角色

接入前需要先回答以下问题:

  • 商品或服务由谁提供?
  • 用户账单上应显示谁的名称?
  • 谁负责退款和售后?
  • 发生拒付后,损失由平台还是第三方承担?
  • 平台是否收取佣金?
  • 收款人是个人、企业,还是两者都有?
  • 是否涉及跨境付款或多币种结算?

这些答案会影响交易模式的选择,例如”第三方直接收款”或”平台代收并分账”。不能只根据接口名称判断资金是否经过平台,还要查看合同中的 merchant of record、settlement、chargeback 和 funds flow 条款。

2. 为每个第三方建立受管账户

平台支付产品通常要求每个收款人拥有一个支付机构账户。业务系统需要保存支付机构返回的账户标识,例如:

recipient.id              = 4821
recipient.providerAccount = acct_xxx
recipient.status          = verified

收款人的身份证件和银行卡信息最好通过支付机构提供的托管页面或官方 SDK 收集。应用服务器只保存账户 ID 和认证状态,避免直接接触敏感资料。

未完成认证的账户不应开放收款或提现功能。

3. 在服务器创建支付

以下示例使用 Stripe Connect 的 PaymentIntent,演示平台如何创建付款并指定已连接账户。实际可用字段、账户类型和责任划分,取决于账户所在地区、业务模式和当前 API 文档。

import Stripe from "stripe";

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

async function createPayment(orderId: string, currentUserId: string) {
  const order = await db.orders.findById(orderId);

  if (!order || order.userId !== currentUserId) {
    throw new Error("Order not found");
  }

  if (order.status !== "pending") {
    throw new Error("Order cannot be paid");
  }

  const recipient = await db.recipients.findById(order.recipientId);

  if (!recipient || recipient.status !== "verified") {
    throw new Error("Recipient is not eligible to receive payments");
  }

  const paymentIntent = await stripe.paymentIntents.create({
    amount: order.amount,
    currency: order.currency,
    automatic_payment_methods: {
      enabled: true,
    },
    transfer_data: {
      destination: recipient.stripeAccountId,
    },
    application_fee_amount: order.platformFee,
    metadata: {
      order_id: order.id,
      recipient_id: recipient.id,
    },
  });

  await db.orders.update(order.id, {
    providerPaymentId: paymentIntent.id,
  });

  return {
    clientSecret: paymentIntent.client_secret,
  };
}

amount 应使用最小货币单位。例如,对于支持两位小数的货币,1000 通常表示 10.00。不同货币采用的最小单位不完全相同,需要按照支付机构的货币说明处理。

如果平台不收取服务费,可以按所选产品的规则省略 application_fee_amount。不要在未确认接口要求的情况下直接将它设为零。

4. 使用官方移动端 SDK 完成支付

服务器返回 clientSecret 后,移动端通过支付机构的官方 SDK 展示支付界面。密钥、账户认证和创建支付的逻辑必须留在服务器端。

客户端流程可以概括为:

const response = await api.post("/payments", {
  orderId,
});

const result = await paymentSdk.confirmPayment(
  response.clientSecret
);

if (result.error) {
  showPaymentError(result.error.message);
} else {
  showProcessingStatus();
}

客户端显示”支付成功”,不能作为最终入账依据。网络中断、通知延迟、额外身份验证和异步支付方式都可能使客户端状态不完整。

5. 用 Webhook 确认最终状态

订单状态应由支付机构发送的服务端通知驱动:

async function handleWebhook(rawBody: Buffer, signature: string) {
  const event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  switch (event.type) {
    case "payment_intent.succeeded": {
      const payment = event.data.object;
      const orderId = payment.metadata.order_id;

      await db.orders.markPaidOnce(orderId, payment.id);
      break;
    }

    case "payment_intent.payment_failed": {
      const payment = event.data.object;
      await db.orders.markPaymentFailed(payment.id);
      break;
    }
  }
}

处理 Webhook 时必须验证签名,并保证幂等性。同一个事件可能重复发送,markPaidOnce 应确保系统不会重复发货、重复记账或重复通知。

不同支付模式的区别

平台支付产品通常提供多种资金模式:

  • 第三方直接收款:交易建立在第三方账户下,第三方通常承担更多商户责任。
  • 目标账户收款:平台创建交易,支付机构再将资金分配给指定账户。
  • 先收款后分账:资金按支付机构的规则进入平台体系,随后分给一个或多个收款方。
  • 批量付款:平台先持有资金,再向第三方付款。这种方式更接近 payout,不等同于用户直接向第三方付款。

如果一名用户的一笔付款只对应一个第三方,直接收款或目标账户模式通常更合适。如果一笔订单需要分给多个商家、配送方或服务人员,则可能需要使用分账能力。

不同模式的资金路径、退款权限和拒付责任差异很大,不能只根据接口调用次数或到账速度来选择。

必须注意的问题

不要在客户端保存密钥

secret key、商户私钥和 Webhook 密钥只能保存在服务器或受控的密钥管理系统中。即使移动应用经过混淆处理,也无法安全保存服务器密钥。

不要自行处理银行卡数据

优先使用支付机构的官方 SDK、托管收银台或令牌化组件。自行接收完整卡号、有效期和安全码,会大幅扩大 PCI DSS 的合规范围。

退款应沿原交易路径返回

退款通常应通过原始支付记录发起,不能简单地从平台账户另行转账给用户。平台服务费、支付手续费和已经结算的资金在退款时如何处理,也需要事先明确。

处理拒付和负余额

即使第三方已经收到结算款,用户仍可能发起拒付。系统需要明确:

  • 拒付金额从哪个账户扣除;
  • 第三方余额不足时由谁垫付;
  • 是否需要延迟结算或设置准备金;
  • 平台是否可以暂停高风险收款人的收款功能。

应用商店规则可能另有限制

如果移动应用销售数字内容、会员权益或应用内功能,Apple App Store 和 Google Play 可能要求使用其应用内购买系统。第三方支付平台能够完成交易,不代表这种支付方式一定符合应用商店的审核规则。实体商品、线下服务和数字商品通常适用不同政策,发布前需要核对目标商店当时有效的规则。

不要用个人转账接口模拟平台支付

让用户填写第三方银行卡、钱包账号或二维码,再由应用自动发起个人转账,通常无法提供可靠的订单关联、退款、拒付和合规机制,也可能违反支付机构的服务协议。

选择支付方案时应询问的内容

与支付机构沟通时,可以直接提供业务资金流图,并确认以下事项:

  • 是否支持平台或 marketplace 模式;
  • 是否允许个人和企业成为收款人;
  • 收款人需要提交哪些认证材料;
  • 用户付款后,资金是否能直接结算给第三方;
  • 是否支持平台佣金和多方分账;
  • 退款、拒付和负余额由谁承担;
  • 支持哪些国家、币种和付款方式;
  • 结算周期和提现费用是多少;
  • 是否提供移动端 SDK、Webhook 和测试环境;
  • 平台是否需要额外的支付业务许可。

最终方案需要由支付机构根据实际业务审核确认。技术上较稳妥的做法是使用支付机构提供的受管账户和平台支付能力,让资金留在其合规体系内,服务器则负责控制订单、收款人映射和交易状态。

备注:内容仅供参考。