PAYATHON 2026

如何使用 Google Play Billing API 取消订阅?

支付小周

结论

不能在 Android 客户端通过 BillingClient 直接取消订阅。

一般有两种处理方式:

  1. 由用户取消:应用跳转到 Google Play 的订阅管理页面,由用户确认取消。
  2. 由开发者取消:在可信任的服务端调用 Google Play Developer API,不能直接从 Android 客户端调用。

BillingClient 用于发起购买、查询商品和确认交易等客户端操作,没有取消订阅的方法。取消订阅涉及账户和订阅生命周期管理,需要用户在 Google Play 中确认,或者由获得授权的开发者服务端执行。

方案一:跳转到 Google Play 订阅管理页面

对于普通用户,通常会在应用中提供“管理订阅”或“取消订阅”按钮。用户点击后,打开对应订阅的 Google Play 管理页面:

fun openSubscriptionManagement(
    context: Context,
    productId: String
) {
    val uri = Uri.parse(
        "https://play.google.com/store/account/subscriptions" +
            "?sku=${Uri.encode(productId)}" +
            "&package=${Uri.encode(context.packageName)}"
    )

    val intent = Intent(Intent.ACTION_VIEW, uri).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    context.startActivity(intent)
}

参数说明:

  • productId 是订阅商品 ID。
  • package 是应用的包名。
  • 用户需要在 Google Play 页面中确认取消。

如果无法确定具体的商品 ID,也可以打开通用订阅管理页面:

fun openAllSubscriptions(context: Context) {
    val intent = Intent(
        Intent.ACTION_VIEW,
        Uri.parse("https://play.google.com/store/account/subscriptions")
    ).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    context.startActivity(intent)
}

启动前最好检查是否有应用能够处理该链接。如果无法打开,可以向用户显示提示:

fun openSubscriptionManagementSafely(
    context: Context,
    productId: String
) {
    val uri = Uri.parse(
        "https://play.google.com/store/account/subscriptions" +
            "?sku=${Uri.encode(productId)}" +
            "&package=${Uri.encode(context.packageName)}"
    )

    val intent = Intent(Intent.ACTION_VIEW, uri)

    if (intent.resolveActivity(context.packageManager) != null) {
        context.startActivity(intent)
    } else {
        Toast.makeText(
            context,
            "无法打开 Google Play 订阅管理页面",
            Toast.LENGTH_LONG
        ).show()
    }
}

方案二:通过服务端取消订阅

如果开发者需要主动取消订阅,例如处理客服请求、风控处置或账户关闭,可以从服务端调用 Google Play Developer API。

当前应优先参考 purchases.subscriptionsv2.cancel,请求形式如下:

POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}:cancel
Authorization: Bearer {accessToken}
Content-Type: application/json

{}

示例:

curl -X POST \
  "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.example.app/purchases/subscriptionsv2/tokens/PURCHASE_TOKEN:cancel" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

这里的 token 是购买成功后取得的 purchaseToken,不是商品 ID。服务端应保存订阅与以下信息的关联:

用户账号
packageName
productId
purchaseToken
当前订阅状态
到期时间

调用 Developer API 前,需要完成以下配置和检查:

  • 在 Google Cloud 项目中启用 Google Play Android Developer API。
  • 配置有权访问对应 Play Console 应用的服务账号,或其他受支持的服务端身份。
  • 在服务端获取 OAuth 2.0 访问令牌。
  • 验证 purchaseToken 确实属于当前用户和当前应用。
  • 调用取消接口后重新查询订阅状态,或者通过 Real-time Developer Notifications 同步后续变化。

Developer API 的请求体和取消类型可能会随接口演进而变化,应以项目当前使用版本的官方接口定义为准。部分现有集成中可能仍会使用旧的 purchases.subscriptions.cancel 接口,但新实现不应在没有确认当前支持状态时依赖该接口。

为什么不能在 BillingClient 中直接取消

订阅购买和订阅取消有不同的安全边界。

购买时,BillingClient 只是启动由 Google Play 控制的结算界面,应用本身不能静默完成扣款。取消订阅同样不能由客户端代码静默执行,否则遭到篡改的应用、恶意脚本或错误逻辑都可能擅自取消用户的订阅。

因此,Google Play 提供了两种取消方式:

  • 用户操作:进入 Google Play 的订阅管理页面并确认取消。
  • 开发者操作:使用受保护的服务端凭据调用 Developer API。

不要把服务账号密钥、OAuth 客户端密钥或具备 Play Console 权限的访问凭据打包进 APK。即使做了混淆,这些凭据仍可能被提取。

取消后的订阅状态

取消订阅通常只是关闭自动续订,并不代表当前权益会立即失效。多数情况下,用户仍能使用已付费周期内的订阅内容,直至当前计费周期结束。

用户点击取消后,应用不应立即删除其权益,而应根据服务端查询结果判断:

  • 是否仍在有效期内;
  • 自动续订是否已经关闭;
  • 实际到期时间;
  • 是否出现退款、撤销、宽限期或账号保留等状态变化。

如果业务要求立即终止权益或办理退款,普通的“取消续订”并不能解决全部问题。此时需要根据具体场景选择相应的 Developer API,或通过 Play Console 处理。

推荐实现

客户端只提供“管理订阅”入口,并将用户带到 Google Play。服务端负责保存和验证 purchaseToken、同步订阅状态,并在确有业务需要时调用 Google Play Developer API。

因此,BillingClient 中没有取消方法是正常的。用户取消订阅时进入 Google Play 管理页面,开发者代为取消时则调用受保护的服务端 API。

备注:内容仅供参考。