PAYATHON 2026

如何在 Android / Blackberry 应用中完成信用卡支付?

支付小周

结论

Drupal Web Services 可以接入支付流程,但不应直接接收、保存或处理信用卡号、CVV 等敏感数据。建议采用以下流程:

  1. 移动端向 Drupal 发起注册和订单创建请求。
  2. Drupal 按服务器端保存的价格创建待支付订单,并向支付服务商创建支付会话。
  3. 移动端通过支付服务商的官方 SDK 或托管收银台付款。
  4. 支付服务商通过带签名的 Webhook 通知 Drupal。
  5. Drupal 验证通知并查询支付结果,确认成功后再激活账户。

这种设计既保留了 Ubercart 的订单管理能力,也缩小了 PCI DSS 的合规范围。移动应用不能自行声明”付款成功”,Drupal 也不能仅凭客户端回调开通账户。

如果注册费对应应用内数字内容、会员功能或服务,Google Play 等应用商店可能要求使用其应用内结算系统。实体商品或在应用外消费的现实服务,通常可以使用第三方信用卡支付。商店政策可能调整,上线前需要核对目标商店、发行地区和业务类型对应的最新规则。

推荐架构

典型流程如下:

Mobile App
    |
    | 1. 请求注册并创建订单
    v
Drupal / Ubercart
    |
    | 2. 使用服务器端密钥创建支付会话
    v
Payment Provider
    ^
    | 3. 官方 SDK / Hosted Checkout
    |
Mobile App

Payment Provider
    |
    | 4. 签名 Webhook
    v
Drupal
    |
    | 5. 验证金额、币种、订单号和支付状态
    v
激活账户 / 完成 Ubercart 订单

在这套架构中,Drupal Web Services 负责订单编排和状态查询,不负责银行卡收单。

实现步骤

1. 选择支付渠道

主要有三种方案:

  • 应用商店内购:适用于数字会员、数字内容和应用功能解锁。
  • 支付服务商原生 SDK:银行卡信息由 SDK 收集,应用和 Drupal 只会接触 token、payment method ID 或支付会话 ID。
  • 托管收银台:用户在浏览器、自定义标签页或系统 WebView 之外的页面完成付款,开发和合规成本通常较低。

不要自行开发信用卡输入表单,再通过 Drupal 接口上传完整卡号。即使整个过程都使用 HTTPS,这种做法仍会扩大攻击面,并增加 PCI DSS 合规责任。

2. 由 Drupal 创建注册订单

移动端只提交套餐或商品标识,不提交最终价格:

POST /api/mobile/registration-orders
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "plan_id": "standard_registration",
  "currency": "USD",
  "idempotency_key": "8bf79c80-..."
}

服务器根据 plan_id 从可信配置或商品数据中读取金额,创建 Ubercart 订单,然后返回支付会话:

{
  "order_id": "12345",
  "payment_session_id": "ps_xxx",
  "client_secret": "client_value_if_required",
  "amount": 2500,
  "currency": "USD",
  "status": "pending"
}

这里的 2500 通常是最小货币单位,例如 25.00 USD。实际格式取决于支付服务商。对于采用零位或三位小数的币种,不能按默认规则猜测金额格式。

不同版本的 Ubercart、Drupal 和支付模块,创建订单及更新订单状态的 API 可能不同。所以下面的代码仅用于说明架构,不能直接复制到所有版本中使用:

<?php

function createRegistrationPayment(array $request): array
{
    $planId = $request['plan_id'];
    $idempotencyKey = $request['idempotency_key'];

    // 必须从服务器端读取,不能相信客户端传入的金额。
    $plan = loadRegistrationPlan($planId);
    if (!$plan) {
        throw new InvalidArgumentException('Unknown registration plan');
    }

    // 同一个幂等键只能生成一个业务订单。
    $existingOrder = findOrderByIdempotencyKey($idempotencyKey);
    if ($existingOrder) {
        return buildPaymentResponse($existingOrder);
    }

    $order = createUbercartOrder([
        'amount' => $plan->amount,
        'currency' => $plan->currency,
        'status' => 'pending',
        'idempotency_key' => $idempotencyKey,
    ]);

    $session = paymentProviderCreateSession([
        'amount' => $plan->amount,
        'currency' => $plan->currency,
        'merchant_order_id' => (string) $order->id,
    ]);

    saveProviderPaymentId($order->id, $session->id);

    return [
        'order_id' => (string) $order->id,
        'payment_session_id' => $session->id,
        'client_secret' => $session->clientSecret ?? null,
        'amount' => $plan->amount,
        'currency' => $plan->currency,
        'status' => 'pending',
    ];
}

支付服务商的私钥只能存放在 Drupal 服务端,不能打包到 Android 或 BlackBerry 应用中。

3. 在移动端调用官方支付组件

Android 端应使用支付服务商支持的 SDK,或打开服务商提供的托管页面。下面的 Kotlin 代码只表示通用调用关系,实际类名和参数应以所选支付服务商当前版本的 SDK 为准:

data class RegistrationOrder(
    val orderId: String,
    val paymentSessionId: String,
    val clientSecret: String?,
    val amount: Long,
    val currency: String,
    val status: String
)

suspend fun startRegistrationPayment(planId: String) {
    val order = api.createRegistrationOrder(
        planId = planId,
        idempotencyKey = UUID.randomUUID().toString()
    )

    paymentSdk.presentCheckout(
        clientSecret = requireNotNull(order.clientSecret)
    )
}

即使 SDK 返回成功,客户端也不能直接激活账户,而应向 Drupal 查询订单状态:

suspend fun waitForConfirmation(orderId: String) {
    val order = api.getRegistrationOrder(orderId)

    when (order.status) {
        "paid" -> showRegistrationCompleted()
        "pending" -> showPaymentProcessing()
        "failed" -> showPaymentFailed()
    }
}

客户端可能被篡改、调试或重放,因此移动端回调只能用于更新界面提示,不能作为支付成功的依据。

4. 使用 Webhook 确认付款

付款完成后,支付服务商应向 Drupal 的专用 HTTPS 地址发送通知。Drupal 必须先验证签名,再处理事件:

<?php

function handlePaymentWebhook(
    string $rawBody,
    string $signatureHeader
): void {
    $event = verifyProviderWebhook(
        $rawBody,
        $signatureHeader,
        getWebhookSecret()
    );

    if ($event->type !== 'payment.succeeded') {
        return;
    }

    $payment = $event->payment;
    $order = loadOrderByProviderPaymentId($payment->id);

    if (!$order) {
        throw new RuntimeException('Order not found');
    }

    if ($payment->amount !== $order->amount) {
        throw new RuntimeException('Amount mismatch');
    }

    if ($payment->currency !== $order->currency) {
        throw new RuntimeException('Currency mismatch');
    }

    // Webhook 可能重复发送,处理逻辑必须幂等。
    if ($order->status === 'paid') {
        return;
    }

    markOrderAsPaidAtomically($order->id, $payment->id);
    activateRegisteredAccount($order->userId);
}

“订单标记成功”和”账户激活”最好放在数据库事务中完成,或使用具备并发保护的状态转换,防止重复通知造成重复记账或重复发放权益。

关键安全要求

不信任客户端

客户端提交的以下数据都不能直接作为付款依据:

  • 金额和币种
  • 商品名称或注册套餐价格
  • “支付成功”标志
  • 用户 ID 或订单所有者
  • 支付服务商返回的数据副本

服务器必须重新计算价格,并确认当前登录用户有权访问对应订单。

不保存银行卡敏感信息

以下信息不应出现在 Drupal、Ubercart 日志、移动端日志或分析平台中:

  • 完整银行卡号,即 PAN
  • CVV/CVC
  • 磁道数据
  • 支付服务商私钥
  • 可重复使用的身份验证数据

如果业务要求系统直接处理卡号,就需要完成相应的 PCI DSS 评估并部署安全控制。普通移动应用通常不适合自行承担这项工作。

正确处理认证和传输

所有接口都必须使用 HTTPS,并正确验证服务器证书。此外还需要:

  • 使用短期访问令牌或可靠的会话认证。
  • 分别校验创建订单、查询订单和退款操作的权限。
  • 为敏感接口设置速率限制。
  • 防止请求重放,并在创建订单时使用幂等键。
  • 不在 URL 查询参数中传递 token 或支付信息。
  • 不将支付密钥硬编码到 APK、BAR 或其他客户端安装包中。
  • 验证 Webhook 的签名、时间戳和事件唯一标识。
  • 对异常订单、重复支付和金额不匹配发出告警。

证书固定可以增加一层防护,但也会带来证书轮换和旧客户端失效等问题,而且不能替代正常的 TLS 验证。

正确设计订单状态

建议至少设置以下状态:

pending
requires_action
processing
paid
failed
cancelled
refunded

有些支付方式不会立即完成,或者需要 3-D Secure 等额外验证。因此,用户关闭支付页面不一定表示支付失败,SDK 已提交也不等于付款成功。最终状态应以经过验证的 Webhook 为准。必要时,Drupal 还应通过服务器端 API 主动向支付服务商查询。

Android 与 BlackBerry 的差异

Android 通常可以使用支付服务商的 Android SDK、系统浏览器或 Custom Tabs。如果应用通过 Google Play 分发,还要先确认注册费是否属于必须使用 Google Play Billing 的数字交易。

对于”BlackBerry”,需要先确定具体平台:

  • 如果是可以运行 Android 应用的 BlackBerry 设备,可以评估支付 SDK 的 Android 兼容性,但必须实际测试其依赖组件。
  • 如果是旧版 BlackBerry OS 或 BB10,许多现代支付服务商已经不再提供原生 SDK。这种情况下,通常只能使用托管收银台,或保留现有的网站支付流程。
  • 如果应用通过特定商店发布,还需要遵守该商店关于数字商品和外部支付的规则。

在设备型号、系统版本和应用商店都不明确的情况下,无法保证现代支付 SDK 能直接支持旧 BlackBerry 平台。托管支付页面通常能覆盖更多平台,但仍需确认支付服务商支持的浏览器、TLS 版本和回跳机制。

与 Ubercart 集成时的注意事项

Ubercart 可以继续管理商品、订单和注册权益,但不能默认把网站现有的支付页面接口直接开放给移动端。应为移动端建立边界明确的 API,同时复用服务器端已有的订单规则。

集成时需要检查:

  • 移动订单是否进入与网站订单相同的状态机。
  • 支付成功后由哪个流程激活 Drupal 用户。
  • 支付失败、取消或超时后,系统是否会清理未完成的注册。
  • 退款后是否需要撤销会员权限。
  • 同一笔支付交易能否绑定到多个账户。
  • Webhook 重复或乱序到达时,系统能否得出正确结果。
  • 订单和支付记录是否保存了可审计的服务商交易 ID。

如果现有 Ubercart 支付模块只支持网页表单,不具备可靠的服务端 token 化或 Webhook 能力,可以由 Drupal 直接调用支付服务商的服务器端 API,再将结果同步回 Ubercart。也可以使用托管收银台,通过订单号关联 Drupal 订单。

移动端负责展示并调用安全的支付组件,Drupal 负责定价、订单、权限和结果验证,支付服务商负责处理银行卡数据。这套分工比通过通用 Web Service 直接传输信用卡信息更安全,也更便于维护。

备注:内容仅供参考。