PAYATHON 2026

如何在 Android application 中实现 credit card payment

支付老李

明确结论

Android 没有用于“验证信用卡并完成扣款”的通用 API。应用需要接入具备收单能力的支付服务商,例如 Stripe、Adyen、Braintree,或所在地区合规的支付机构,并使用其 Android SDK、托管支付页面或 Payment Sheet。

不要在应用内自行判断卡号是否有效,也不要把卡号和 CVV 直接发送到普通业务服务器。卡片能否用于支付,最终取决于支付服务商、收单行和发卡行的授权结果。

推荐的支付流程

完整的支付流程通常如下:

  1. Android 向业务后端请求创建支付订单。
  2. 后端根据商品和订单记录确定金额,再调用支付服务商 API 创建支付对象。
  3. 后端向 Android 返回一次性的客户端支付参数。
  4. Android 使用支付服务商 SDK 展示安全的卡片输入界面。
  5. SDK 将卡片信息直接提交给支付服务商,并完成 3D Secure 等身份验证。
  6. 支付服务商向客户端返回授权结果,同时通过 Webhook 通知后端。
  7. 后端验证 Webhook 签名,并根据最终支付状态更新订单。
  8. Android 从后端查询订单状态,展示成功、失败或处理中。
Android App
    │
    ├── 创建业务订单 ──────────────> 业务后端
    │                                  │
    │                                  ├── 创建支付 ──> 支付服务商
    │                                  │
    │<──────── 返回客户端支付参数 ──────┤
    │
    ├── SDK 收集卡片并确认支付 ───────> 支付服务商
    │                                  │
    │                                  ├── 银行授权 / 3D Secure
    │                                  │
    │                                  └── Webhook ──> 业务后端
    │
    └── 查询订单最终状态 ─────────────> 业务后端

Android 端示例结构

不同服务商和 SDK 版本使用的类名可能不同。下面用抽象接口说明客户端需要负责的工作。生产代码应改用所选支付服务商官方 SDK 提供的 Payment Sheet 或托管组件。

data class CreatePaymentRequest(
    val orderId: String
)

data class CreatePaymentResponse(
    val clientToken: String
)

interface PaymentApi {
    @POST("payments")
    suspend fun createPayment(
        @Body request: CreatePaymentRequest
    ): CreatePaymentResponse

    @GET("orders/{orderId}")
    suspend fun getOrder(
        @Path("orderId") orderId: String
    ): OrderResponse
}

用户点击支付按钮后,先请求后端创建支付:

lifecycleScope.launch {
    try {
        payButton.isEnabled = false

        val payment = paymentApi.createPayment(
            CreatePaymentRequest(orderId = orderId)
        )

        paymentSdk.presentPaymentSheet(
            activity = this@CheckoutActivity,
            clientToken = payment.clientToken
        )
    } catch (e: Exception) {
        showMessage("暂时无法发起支付,请稍后重试")
        payButton.isEnabled = true
    }
}

支付组件返回结果后,客户端可以更新界面,但不能仅根据客户端回调把订单标记为已支付:

fun handlePaymentResult(result: PaymentResult) {
    when (result) {
        PaymentResult.Completed -> {
            showMessage("支付已提交,正在确认结果")
            refreshOrderFromServer()
        }

        PaymentResult.Canceled -> {
            showMessage("支付已取消")
            payButton.isEnabled = true
        }

        is PaymentResult.Failed -> {
            showMessage(result.displayMessage ?: "支付失败,请检查卡片或更换支付方式")
            payButton.isEnabled = true
        }
    }
}

这里的 paymentSdk 和 PaymentResult 只是示意名称,并非 Android 标准 API。实际接入时,必须使用服务商当前官方文档提供的类和方法。

后端必须完成的工作

支付金额不能由 Android 客户端直接决定,因为客户端数据可能被修改。后端应根据 orderId 查询数据库并重新计算金额。

下面是与具体支付服务商无关的伪代码:

app.post("/payments", async (req, res) => {
  const order = await database.orders.findById(req.body.orderId);

  if (!order || order.userId !== req.user.id) {
    return res.status(404).json({ error: "ORDER_NOT_FOUND" });
  }

  if (order.status === "PAID") {
    return res.status(409).json({ error: "ORDER_ALREADY_PAID" });
  }

  const amount = calculateAmountFromServerData(order);

  const payment = await paymentProvider.createPayment({
    amount,
    currency: order.currency,
    reference: order.id,
    idempotencyKey: `order:${order.id}`
  });

  await database.orders.savePaymentReference(
    order.id,
    payment.id
  );

  res.json({
    clientToken: payment.clientToken
  });
});

后端还需要接收 Webhook:

app.post("/payment-webhook", rawBodyMiddleware, async (req, res) => {
  const event = paymentProvider.verifyWebhook(
    req.rawBody,
    req.headers,
    process.env.PAYMENT_WEBHOOK_SECRET
  );

  if (event.type === "payment.succeeded") {
    await markOrderAsPaidOnce(event.reference, event.paymentId);
  } else if (event.type === "payment.failed") {
    await markOrderAsFailed(event.reference, event.paymentId);
  }

  res.sendStatus(200);
});

Webhook 必须使用支付服务商提供的方法验证签名,不能只相信请求体中的状态。更新订单时也要实现幂等处理,避免服务商重复发送通知,进而造成重复发货或重复记账。

“验证信用卡”具体指什么

这里需要区分两类验证。

本地格式检查

客户端可以检查:

  • 卡号是否符合基本长度要求
  • Luhn 校验是否通过
  • 有效期是否已经过去
  • 必填字段是否完整

这些检查只能改善表单填写体验,无法证明卡片真实存在,也无法判断余额、信用额度、挂失状态,或银行是否会批准这笔交易。

银行授权

真正的接受或拒绝发生在支付授权阶段。支付服务商会把请求发送到银行卡网络和发卡行,可能得到以下结果:

  • 支付成功
  • 卡片被拒绝
  • 余额或额度不足
  • 需要 3D Secure 验证
  • 网络超时,状态暂时未知
  • 支付仍在处理中

因此,支付状态不适合简单设计为同步返回的 true 或 false。业务订单至少要考虑 PENDING、PAID、FAILED 和 CANCELED 等状态。

安全与合规注意事项

不要自行保存敏感卡片数据

通常不应保存:

  • 完整卡号
  • CVV/CVC
  • 磁道数据
  • PIN

这些信息尤其不能写入日志、崩溃报告、分析平台或截图。即使卡号通过 HTTPS 传输,只要数据经过自己的服务器,仍可能显著扩大 PCI DSS 的合规范围。

如果需要支持“保存银行卡”,应保存支付服务商返回的 token 或 payment method identifier,而不是原始卡号。

使用 HTTPS 和后端密钥

支付服务商的 secret key 只能保存在后端,不能写入 APK。APK 可以被反编译,因此任何打包进客户端的长期密钥都应视为可被公开获取。

Android 端通常只保存服务商允许公开使用的 publishable key,以及后端临时返回的一次性支付参数。

以后端最终状态为准

客户端可能被篡改,也可能在支付完成后断网或被系统终止。因此:

  • 不要根据客户端显示的“成功”页面直接发货。
  • 使用经过签名验证的 Webhook 更新支付状态。
  • 客户端恢复后应从后端重新查询订单。
  • 创建支付、处理回调和发货操作都要实现幂等。

支持强客户认证

部分国家和地区要求 SCA 或 3D Secure。选择支付 SDK 时,需要确认它可以处理页面跳转、验证界面,以及应用返回后的状态恢复。不要自行模拟这些流程。

如何选择 API 和教程

优先查看支付服务商当前官方文档中的以下内容:

  • Android SDK quickstart
  • Payment Sheet 或 Drop-in UI
  • Create payment / payment intent
  • 3D Secure
  • Webhook signature verification
  • Idempotency
  • Test cards 和 sandbox
  • PCI DSS integration guide

如果业务只需要银行卡付款,通常可以优先采用 Payment Sheet 或托管支付页面。与自行绘制卡片表单相比,这些方案更容易处理校验、令牌化、3D Secure 和合规要求。

Google Pay 可以改善支付体验,但它属于钱包和支付信息传递方式,并不等同于完整的收单后端。实际扣款通常仍要配合支付服务商完成。

上线前检查

在生产环境开放真实付款前,至少要完成以下检查:

  • 使用服务商 sandbox 和官方测试卡覆盖成功、拒绝、3D Secure、超时和取消场景。
  • 验证 Webhook 签名,并妥善处理重复、延迟和乱序通知。
  • 确认金额和币种完全由后端计算。
  • 确认 secret key 和 Webhook secret 没有进入 APK 或代码仓库。
  • 检查日志和监控系统,确保它们不会采集卡号、CVV 等敏感字段。
  • 根据业务所在地、用户所在地和支付服务商要求,评估 PCI DSS、隐私和税务合规。
  • 使用真实的小额交易完成上线前的端到端验证,并准备退款和争议处理流程。

备注:内容仅供参考。