PAYATHON 2026

Android 中如何处理实体商品购买与支付?

支付小周

明确结论

Android 应用可以销售实体商品,但此类交易通常不使用 Google Play Billing。开发者需要接入自己的订单系统,并使用第三方支付服务,例如收单银行、支付网关或支持 Google Pay 的支付服务商。

“自行处理支付”不代表必须从零开发一套完整的银行卡平台。更常见也更安全的分工是:

  • 应用负责展示商品、管理购物车和处理订单交互。
  • 服务端负责校验价格、管理库存和订单状态,以及编排支付流程。
  • 持牌支付服务商负责银行卡信息采集、令牌化、收单和 3-D Secure 等环节。
  • 应用和业务服务器应尽量避免接触原始卡号及 CVV。

Google Play Billing 主要用于应用内数字内容、功能或订阅。实体商品、配送服务和线下消费通常应使用其他支付方式。相关政策可能调整,上线前应核对目标市场适用的最新版 Google Play 支付政策和支付服务商要求。

推荐的支付架构

常见的支付流程如下:

  1. 用户在应用中选择实体商品并提交购物车。
  2. 应用把商品 ID 和数量发送给业务服务器。
  3. 服务器根据数据库中的数据重新计算价格、税费、优惠和运费,不能直接采用客户端传来的金额。
  4. 服务器创建待支付订单,再通过支付服务商创建支付会话。
  5. 应用使用支付服务商的 Android SDK、托管支付页面或 Google Pay 完成支付授权。
  6. 支付服务商返回一次性令牌或支付结果。
  7. 服务器调用支付服务商的服务端 API 确认支付。
  8. 支付服务商通过 Webhook 通知最终状态。
  9. 服务器验证 Webhook 签名,以幂等方式更新订单并安排发货。

判断订单是否支付成功,应以服务端查询结果或通过签名验证的 Webhook 为准,不能只看 Android 客户端显示的”成功”页面。

Android 端示例

下面是一个不依赖具体支付服务商的简化示例。应用只提交商品信息,金额由服务器计算:

data class CartItem(
    val productId: String,
    val quantity: Int
)

data class CheckoutRequest(
    val items: List<CartItem>,
    val shippingAddressId: String
)

data class CheckoutSession(
    val orderId: String,
    val paymentSessionToken: String
)

interface CheckoutApi {
    @POST("checkout/sessions")
    suspend fun createCheckoutSession(
        @Body request: CheckoutRequest
    ): CheckoutSession
}

创建支付会话后,把 paymentSessionToken 交给支付服务商提供的官方 Android SDK:

lifecycleScope.launch {
    try {
        val session = checkoutApi.createCheckoutSession(
            CheckoutRequest(
                items = cartItems,
                shippingAddressId = selectedAddressId
            )
        )

        paymentProvider.presentCheckout(
            activity = this@CheckoutActivity,
            sessionToken = session.paymentSessionToken
        )
    } catch (e: Exception) {
        showPaymentError("暂时无法发起支付,请稍后重试")
    }
}

这里的 paymentProvider 只是示意接口,并非 Android 标准 API。实际代码需要按照所选支付服务商的 SDK 编写。不要自行使用普通 EditText 收集银行卡号、有效期和 CVV,除非团队确实具备相应的支付安全和合规能力。

服务端示例

服务端必须重新读取商品价格并生成订单,不能接受客户端指定的最终金额:

app.post("/checkout/sessions", async (req, res) => {
  const userId = req.auth.userId;
  const { items, shippingAddressId } = req.body;

  const pricedItems = await priceProductsFromDatabase(items);
  const totalAmount = calculateTotal(pricedItems);

  const order = await orders.create({
    userId,
    items: pricedItems,
    shippingAddressId,
    totalAmount,
    currency: "CNY",
    status: "PAYMENT_PENDING"
  });

  const paymentSession = await paymentGateway.createSession({
    amount: totalAmount,
    currency: order.currency,
    reference: order.id,
    returnUrl: "example://payment-result"
  });

  res.json({
    orderId: order.id,
    paymentSessionToken: paymentSession.token
  });
});

处理 Webhook 时需要验证签名,并防止重复执行同一个通知:

app.post("/payments/webhook", rawBodyMiddleware, async (req, res) => {
  const event = paymentGateway.verifyWebhook(
    req.rawBody,
    req.headers
  );

  if (await webhookEvents.hasProcessed(event.id)) {
    return res.sendStatus(200);
  }

  await database.transaction(async (tx) => {
    await webhookEvents.markProcessed(tx, event.id);

    if (event.type === "payment.succeeded") {
      await orders.markPaidIfPending(tx, event.reference);
    }

    if (event.type === "payment.failed") {
      await orders.markPaymentFailedIfPending(tx, event.reference);
    }
  });

  res.sendStatus(200);
});

rawBodyMiddleware、签名算法和事件字段都取决于支付服务商,必须按照其官方文档规定的方式进行验证。

银行卡信息如何处理

应优先使用令牌化方案。银行卡信息直接提交给支付服务商,业务系统只接收类似 paymentMethodToken 的不可逆令牌。需要保存支付方式时,也应保存服务商生成的客户 ID 或支付方式令牌,不要保存完整卡号。

通常可以保存以下展示信息:

  • 卡组织,例如 Visa 或 Mastercard。
  • 卡号后四位。
  • 有效期,但需要结合服务商要求和业务必要性判断。
  • 支付服务商生成的支付方式令牌。

以下信息不应保存:

  • CVV、CVC 等银行卡安全码。
  • 磁道数据。
  • PIN 或 PIN block。
  • 未经必要保护的完整银行卡号。
  • 可以还原银行卡敏感认证数据的日志、截图或分析事件。

即使采用令牌化方案,也要控制访问权限、加密业务数据,并设置合理的数据保留期限。

安全与合规注意事项

PCI DSS

只要系统存储、处理或传输银行卡数据,就可能进入 PCI DSS 的适用范围。支付服务商提供的托管页面、官方移动 SDK 或令牌化组件,可以大幅减少业务系统接触银行卡数据的范围,但通常不会自动免除所有合规义务。

具体适用哪类自评问卷或审计要求,需要由支付服务商、收单机构或合规顾问根据实际数据流确定。

传输和存储安全

所有支付接口都应使用 HTTPS/TLS,并采取以下措施:

  • 由服务端验证证书,不提供绕过证书错误的逻辑。
  • 密钥和支付网关私钥只保存在服务端。
  • 不把密钥硬编码到 APK 中。
  • 不在日志、崩溃报告或分析平台中记录卡号、令牌和个人地址。
  • 使用 Android Keystore 支持的密钥保护敏感本地数据。
  • 不通过明文 SharedPreferences 保存支付凭证。
  • 对支付、退款和地址修改等操作进行重新认证或风险校验。

Android 客户端中的常量最终都可能被提取,因此不能依靠 APK 隐藏服务器密钥。

支付状态与重复扣款

网络超时不代表支付失败。如果客户端每次重试都创建新交易,可能导致重复扣款。创建订单和确认支付时,应使用唯一订单号和幂等键。

支付结果至少需要区分以下状态:

  • 等待授权。
  • 需要额外验证。
  • 已授权或已支付。
  • 明确失败。
  • 状态未知,需要服务端查询。
  • 已退款或部分退款。

身份验证和欺诈风险

实体商品涉及配送和拒付风险,需要考虑:

  • 3-D Secure 或目标地区要求的强客户认证。
  • 对账单地址和收货地址进行风险检查。
  • 检测设备、账号、订单频率和异常金额。
  • 人工审核高风险订单。
  • 防止退款、取消、缺货或重复发货造成问题。

是否必须启用 3-D Secure,取决于支付地区、卡组织、收单机构和服务商的规则。

隐私与消费者保护

收货地址、电话号码和订单历史也属于敏感数据。应用需要提供清晰的隐私说明,并根据经营地区处理以下事项:

  • 数据收集目的和保存期限。
  • 用户访问、删除或更正数据的方式。
  • 退款、退货、配送和取消规则。
  • 税费、运费和最终价格的展示方式。
  • 未成年人交易及受限制商品。
  • 当地的数据保护、电子商务和消费者权益要求。

Google Pay 与 Google Play Billing 的区别

两者用途不同:

  • Google Play Billing 用于其适用范围内的应用内数字商品和数字服务。
  • Google Pay 是一种支付方式,可用于实体商品或其他符合其规则的交易。

Google Pay 通常向支付服务商提供经过保护的支付凭据,但不能代替商品订单、库存、税费、退款和履约系统。使用 Google Pay 时,仍需配置受支持的支付网关或收单处理方式。

实际落地建议

对于大多数销售实体商品的 Android 应用,可以采用以下实现方式:

  1. 选择能够在经营地区合法提供服务的支付服务商。
  2. 使用服务商的官方 Android SDK、托管结账页或 Google Pay 集成。
  3. 让银行卡数据直接进入支付服务商的系统。
  4. 在服务端计算金额并创建订单。
  5. 使用签名 Webhook 和服务端查询确认支付状态。
  6. 对支付创建、支付确认和 Webhook 处理实施幂等控制。
  7. 上线前完成 PCI DSS、隐私、税务、退款和应用商店政策检查。

因此,Office Depot 一类应用需要自己的商品、订单和履约后台,但不一定自行处理银行卡底层业务。敏感的支付信息采集、令牌化和收单处理,通常由专业支付服务商完成。

备注:内容仅供参考。