使用 Google Play 结算服务实现订阅替换

1. 简介

此 Codelab 将教您如何使用 Google Play 结算库 (PBL) 来管理订阅方案变更。您将了解各种替换模式如何影响价格和用户使用权,同时学习如何处理后端实时开发者通知 (RTDN)。

观众群

此 Codelab 专为 Android 应用开发者而设计,提供了有关实现复杂的订阅管理功能的指南。该指南可帮助您为用户提供顺畅的体验,以便他们升级、降级或在不同的订阅方案之间转换。

学习内容…

  • 如何在 Play 管理中心内创建订阅
  • 如何选择正确的 ReplacementMode(例如,WITH_TIME_PRORATIONDEFERRED),以符合应用的升级和降级政策。
  • 如何配置 launchBillingFlow 中的 BillingFlowParams 以触发 Google Play 购买流程,从而替换方案。
  • 如何使用实时开发者通知 (RTDN) 和 purchases.subscriptionsv2 API 在后端安全地撤消旧访问权限并授予新访问权限

所需条件

2. 构建示例应用

本 Codelab 将使用一个示例 Android 应用来演示如何在 PBL 中实现订阅替换。此示例应用旨在成为一个功能齐全的 Android 应用,其中包含完整的源代码,可展示以下方面:

  • 将应用与 PBL 集成
  • 实现订阅替换

如果您已熟悉订阅替换和 PBL,可以下载示例应用并进行试用。

以下演示视频展示了示例应用在部署和运行后的外观和行为。

前提条件

在构建和部署示例应用之前,请执行以下操作:

构建

如需构建示例应用以按照 Codelab 中的说明操作,请执行以下操作:

  1. 从 GitHub 下载示例应用
  2. 更新示例应用 build.gradle 中的 applicationId,以反映 Play 管理中心内应用的 Application Id。
  3. 构建示例应用
    注意:此命令可成功构建应用以进行本地测试。不过,运行应用不会提取商品和价格,因为尚未在 Play 管理中心内创建所需的订阅。下一部分将介绍如何在开发者控制台中创建订阅。

3. 在 Play 管理中心内创建订阅

Google Play 订阅系统让您可以灵活地创建、管理和销售订阅。在 Play 管理中心内,您可以配置包含多个基础方案的订阅,每个基础方案可包含多项优惠。您可以为订阅优惠设置多种定价模式和资格条件。在此 Codelab 中,您将创建三个订阅:专业版方案基本方案Lite 方案,模拟各种价位的典型订阅产品。每个订阅都将有一个按月执行的周期性基础方案。

创建新订阅

创建新订阅

  1. 打开 Play 管理中心,然后前往“订阅”页面借助 Google Play 创收 > 商品 > 订阅
  2. 点击创建订阅
  3. 输入订阅详情:
    • ProductID:输入唯一商品 ID。输入 premium_plan
    • 名称:输入订阅的简称。示例:Premium Plan
  4. 点击创建

创建基础方案

  1. 打开 Play 管理中心,然后前往“订阅”页面借助 Google Play 创收 > 商品 > 订阅
  2. 在要创建基础方案的订阅项目旁边,点击向右箭头以查看订阅详情。
  3. 点击添加基础方案
  4. 输入基础方案 ID。示例 monthly-auto-renewing
  5. 选择类型为自动续订
  6. 对于自动续订型基础方案,请设置以下内容:
    • 结算周期:按月
    • 宽限期:7 天
    • 结算方案和优惠变更:在结算日期扣款
    • 重新订阅:点按允许
  7. 价格和适用范围部分,点击设置价格以设置基础方案的价格。
  8. 选择所有国家和地区,然后点击设置价格
  9. 将此基础方案的价格设置为 10 美元,然后点击更新
  10. 设置好基础方案的价格后,点击右下角的保存,然后点击启用

为示例应用创建订阅

在此 Codelab 中,请创建两个具有以下配置的其他订阅:

  • 基本方案
    • 商品 ID:basic_plan
    • 名称:基础方案
    • 基础方案 ID:monthly-auto-renewing
    • 价格:5 美元
  • Lite 方案
    • 商品 ID:lite_plan
    • 名称:Lite 方案
    • 基础方案 ID:monthly-auto-renewing
    • 价格:3 美元

示例应用已配置为使用这些商品 ID 和基础方案 ID。您可以创建具有不同配置的不同订阅,在这种情况下,您必须修改示例应用以使用您创建的产品 ID。

订阅创建视频

以下视频展示了之前介绍的在 Play 管理中心内创建订阅的步骤。

4. 订阅替换

与 PBL 集成的开发者可以为现有订阅者提供各种选项,方便他们更改订阅方案以更好地满足个人需求:

  • 如果您销售多个订阅层级(例如基础付费订阅),可以允许用户通过购买不同订阅的基础方案或优惠来切换层级。
  • 您可以允许用户更改其当前结算周期,例如从包月方案改为包年方案。
  • 您还可以允许用户在自动续订和预付费方案之间切换。

当用户决定升级、降级或更改其订阅时,您可以指定替换模式,确定如何采用当前结算周期的按比例计费值,以及用户使用权变更的生效时间。

Play 结算库提供了多种 ReplacementMode 选项来控制此行为。

可用的替换模式

  • WITH_TIME_PRORATION:订阅项会立即升级或降级。系统会根据差价调整任何剩余时长,并通过更新下一个结算日期将剩余时长计入新的订阅。这是默认行为
  • CHARGE_PRORATED_PRICE:订阅项会立即升级,结算周期保持不变。用户随后需要补足剩余订阅期的差价。
  • CHARGE_FULL_PRICE:订阅项会立即升级或降级,并且系统会即刻按全价向用户收取新使用权的费用。如果使用权不变,系统会将之前订阅的剩余价值结转;如果使用权有变化,系统会将剩余价值按比例折算成时间。
  • WITHOUT_PRORATION:订阅商品会立即升级或降级,在订阅续订时将按新价格收取费用。结算周期保持不变。
  • DEFERRED:只有在订阅续订时,订阅商品才会升级或降级。

5. WITH_TIME_PRORATION

在此替换模式下,订阅项会立即升级或降级。系统会根据差价调整任何剩余时长,并将下一个结算日期往后推延,将剩余时长计入新的订阅。这是默认行为。

示例场景

某用户在 4 月 15 日(即月度结算周期过半时)从基础方案(每月 4.99 美元)改用高级方案(每月 9.99 美元)。

在这种情况下:

  • 用户会立即获得 Premium 方案的访问权限。
  • Google Play 会自动计算按比例分摊的期限。假设 Play 计算出基础方案剩余的 15 天相当于高级方案的 7 天,则下一个结算日期会提前到 4 月 21 日。
  • 用户无需立即付款。

代码段

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

升级(采用 WITH_TIME_PRORATION)

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Premium 方案。

用户的订阅会立即升级为 Premium 方案。用户需立即支付的金额为 0.00 美元。基础方案的剩余价值会按比例折算为高级方案的时长,从而提前下一个续订日期。系统将在新调整的结算日期向用户收取续订费用 9.99 美元。

降级(采用 WITH_TIME_PRORATION)

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Lite 方案。

用户的授权会立即降级为 Lite 方案。您需要立即支付的金额为 0.00 美元。基础方案的剩余价值会按比例转换为精简版方案的时间,从而大幅延长下一个续订日期。系统将在新调整的结算日期向用户收取 2.99 美元的续订费用。

总结

在本部分中,您了解了 WITH_TIME_PRORATION 如何通过根据价格差调整下次续订的时间来修改用户授权,而不会立即收取费用。这是升级或降级用户的有效默认策略。

6. CHARGE_PRORATED_PRICE

在此替换模式下,订阅项会立即升级,结算周期保持不变。用户随后需要补足剩余订阅期的差价。

注意:此选项仅适用于每时间单位的价格会提高的订阅商品升级。

示例场景

某用户订阅的是基础方案(每月 4.99 美元),在 4 月 20 日决定升级到高级方案(每月 9.99 美元),此时距离其每月结算周期结束还有大约 10 天。

在这种情况下:

  • 用户会立即获得 Premium 方案的访问权限。
  • 系统会立即向用户收取当前结算周期剩余 10 天的按比例计算的差价。这相当于大约 2.99 美元,可购买 10 天的 Premium 方案。
  • 用户的结算日期不会更改。

代码段

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

以 CHARGE_PRORATED_PRICE 升级

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Premium 方案。

用户会立即升级到 Premium 方案,并保留原续订日期。您需要立即支付的金额是高级方案和基础方案价格之间按当前结算周期剩余天数比例计算的差额。在续订日期,系统将向用户收取 9.99 美元的 Premium 全额续订费用。

以 CHARGE_PRORATED_PRICE 降级

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Lite 方案。

此替换模式仅适用于每时间单位的价格会提高的订阅商品升级,因此在降级期间会导致错误。结算流程将失败,并向用户显示一条错误消息,指出降级不支持按比例计费模式。

总结

本部分介绍了 CHARGE_PRORATED_PRICE 如何通过向用户收取剩余结算周期的确切差价来实现立即升级,同时保持结算周期不变。如果用户想要升级到费用更高的层级,但不想更改结算日期,此功能非常有用。

7. CHARGE_FULL_PRICE

在此替换模式下,订阅商品会立即升级或降级,并且系统会即刻按全价向用户收取新使用权的费用。如果使用权不变,系统会将之前订阅的剩余价值结转;如果使用权有变化,系统会将剩余价值按比例折算成时间。

示例场景

用户订阅的是基本方案(自 4 月 1 日起,每月 4.99 美元)。4 月 20 日,用户希望改用 Premium 方案(每月 9.99 美元)。

在这种情况下:

  • 系统会立即向用户收取 Premium 方案的全价 (9.99 美元)。
  • 基础方案的剩余价值(例如 10 天的价值)会转换为高级方案的等效时间。在此示例中,10 天的基本版相当于 5 天的高级版
  • 用户的下一个续订日期会进行调整,以包含此按比例计算的时间。因此,续订日期将变为 5 月 25 日(4 月 20 日 + 1 个月 + 5 天)。
  • 后续续订将从 5 月 25 日开始每月进行一次。

代码段

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

以 CHARGE_FULL_PRICE 价格升级

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Premium 方案。

用户会立即升级到 Premium 方案。您需要立即支付的金额是 Premium 方案的全价,即 9.99 美元。基本方案中的所有剩余价值都会转换为新高级方案中的时间,从而略微延长首次续订日期。之后,续订金额将为每个周期 9.99 美元。

降级,但需支付全价

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Lite 方案。

用户会立即降级到 Lite 方案,并开始新的结算周期。需立即支付的金额为目标价格 2.99 美元的全额。系统会将基本方案的未使用部分按比例转换为新 精简版方案的时间,从而延长首次续订日期。之后,续订金额将为每个周期 2.99 美元。

总结

在本部分中,我们介绍了 CHARGE_FULL_PRICE 如何在转换当天向用户收取全额自付费用,并立即开始新的结算周期。原方案中的任何剩余余额都会按比例计入下一个续订日期。

8. WITHOUT_PRORATION

在此替换模式下,订阅商品会立即升级或降级,并且会在订阅续订时按新价格收费。

示例场景

用户订阅的是基本方案(自 4 月 1 日起,每月 4.99 美元)。4 月 20 日,用户希望改用 Premium 方案(每月 9.99 美元)。

在这种情况下:

  • 用户会立即获得 Premium 方案的访问权限。
  • 用户无需支付更高的 9.99 美元价格,直到下一个订阅续订日期(5 月 1 日)为止。

代码段

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

升级(不按比例退款)

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Premium 方案。

用户会立即升级到 Premium 方案,同时保留其现有的续订日期。您需要立即支付的金额为 0.00 美元。在下一个结算日期之前,用户可以在当前结算周期的剩余时间内使用 Premium 方案,之后将按新的续订金额 9.99 美元续订。

降级(采用 WITHOUT_PRORATION)

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Lite 方案。

用户立即降级为 Lite 方案,无法再使用他们付费购买的基本功能。您需要立即支付的金额为 0.00 美元。结算周期保持不变,用户将在下一个预定的续订日期按新的较低费率(2.99 美元)付费。

总结

本部分演示了 WITHOUT_PRORATION 如何在不收取结账费用的情况下立即替换用户的授权,同时保持结算周期不变。

9. DEFERRED

在此替换模式下,只有在订阅续订时,订阅项才会升级或降级,但新购买的内容会立即发放。现有商品设置为不可续订,并在当前结算周期结束时到期,而新请求的使用权会在到期后立即开始。

示例场景

用户订阅的是基本方案(自 4 月 1 日起,每月 4.99 美元)。4 月 20 日,用户希望改用 Premium 方案(每月 9.99 美元)。

在这种情况下:

  • 用户不会立即产生费用。
  • 在当前结算周期结束(4 月 30 日)之前,用户仍可继续使用基础版功能。
  • 订阅方案将在下一个续订日期(5 月 1 日)自动升级为 Premium

代码段

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.DEFERRED;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

使用 DEFERRED 进行升级

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.DEFERRED
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Premium 方案。

用户将继续使用基础方案,直到当前结算周期结束。您需要立即支付的金额为 0.00 美元。在续订日期,用户的授权将升级为 Premium 方案,系统会向其收取新的续订金额 9.99 美元。

使用 DEFERRED 进行降级

如需模拟此场景,请执行以下操作:

  • 在示例应用的 MainActivity 中,将代码段中的 replacementMode 更新为 SubscriptionProductReplacementParams.ReplacementMode.DEFERRED
  • 重新构建并启动应用。
  • 从 Google Play 商店中取消现有订阅(如有),并使其过期。
  • 购买基础方案。
  • 改用 Lite 方案。

用户将继续使用基础方案,直到当前结算周期结束。您需要立即支付的金额为 0.00 美元。在续订日期,其订阅将升级为 Lite 方案,并且系统会向其收取新的续订金额 2.99 美元。

总结

本部分介绍了DEFERRED替换模式如何将升级或降级推迟到有效付费期结束时。因此,此设置非常适合降级,以防止已购买的功能丢失。

10. 后端和客户端处理

在用户成功触发订阅替换后,请确保您的应用和后端正确处理此变更,以避免服务中断或重复扣款等问题。

示例场景

  • 用户拥有基础包月方案(product_id 为 basic_plan,purchase_token 为 basic_purchase_token_123)。
  • 用户使用立即替换模式(WITHOUT_PRORATIONWITH_TIME_PRORATIONCHARGE_PRORATED_PRICECHARGE_FULL_PRICE 之一)切换到高级方案
  • 成功切换订阅后,Google 会将其视为订阅,并为专业版方案(product_id premium_plan 和 purchase_token premium_purchase_token_123)创建新的购买令牌。

客户端处理

onPurchasesUpdated

当替换购买交易完成时,系统会触发 PurchasesUpdatedListener。虽然这是切换,但 Google Play 会将高级版方案视为全新购买

应用将收到一个 Purchase 对象,其中包含购买令牌 premium_purchase_token_123 和 product_id premium_plan。您必须像处理新订阅者一样处理此情况:验证令牌并准备授予访问权限

@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
        for (Purchase purchase : purchases) {
            // purchase.getPurchaseToken() = premium_purchase_token_123
            // purchase.getProducts() will contain premium_plan
            // Verify the purchase and grant entitlement
            handleNewPurchase(purchase);
        }
    }
}

queryPurchasesAsync

queryPurchasesAsync 仅返回通过您的应用购买的有效订阅。您应依靠此方法来确定要向用户显示哪些使用权。对于立即替换,queryPurchasesAsync() 将停止返回旧的 BASIC 购买令牌,现在只会返回新的 PREMIUM 购买令牌。

每当应用恢复或购买交易完成时,都应调用此方法。如果存在 Premium 令牌,则立即授予 Premium 功能并移除基本功能。

后端处理 (RTDN)

发生替换时,Google Play 会向您配置的 Pub/Sub 主题发送实时开发者通知 (RTDN)。

  • 如果选择立即换货,Google 会发送包含新购买令牌的 SUBSCRIPTION_PURCHASED RTDN。RTDN 载荷示例
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":4, // SUBSCRIPTION_PURCHASED
        "purchaseToken":"premium_purchase_token_123" //purchase token for the new subscription
      }
    }
    
  • 当您的服务器从 RTDN 收到新的购买令牌时,请使用新的购买令牌调用 purchases.subscriptionsV2 API 以提取购买详情。API 响应包含一个 linkedPurchaseToken 字段,用于确定购买令牌是指新的订阅购买交易还是订阅替换交易。
  • 如果发生订阅替换,linkedPurchaseToken 是指旧订阅的购买令牌。在这种情况下,该值为 basic_purchase_token_123。示例 GET purchases.subscriptionsV2 响应
    curl \
    'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/<application_id>/purchases/subscriptionsv2/tokens/premium_purchase_token_123' \
    --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
    --header 'Accept: application/json'
    
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "startTime": "...",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>",
      "linkedPurchaseToken": "basic_purchase_token_123", // The purchase token of the subscription that was replaced (Basic Plan in this case)
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // productID of the new subscription (Premium Plan in this case)
          "expiryTime": "....",
          "autoRenewingPlan": {...},
          "offerDetails": {
            "basePlanId": "monthly-auto-renewing" // base plan ID of the new subscription
          },
          "itemReplacement": { // Details about the subscription replacement
            "productId": "subscription_basic", // productID of the old subscription (Basic Plan in this case)
            "replacementMode": "CHARGE_PRORATED_PRICE", // Replacement strategy used for this subscription change
            "basePlanId": "monthly-auto-renewing" // base plan ID of the old subscription 
          },
          "offerPhase": {...}
        }
      ],
      "etag": "<etag_value>"
    }
    
  • 必须确认新的 Premium 购买交易。此操作可以在应用内或后端完成。如果未能在 3 天内确认购买交易,系统将退款并撤消相应授权。如需详细了解如何处理和确认购买交易,请参阅开发者文档

总结

本部分介绍了在客户端和后端处理即时订阅替换的步骤。您了解到,Google Play 会将新方案视为全新的购买交易,并签发新的购买令牌。在客户端上,您必须使用 PurchasesUpdatedListener 处理此新购买交易,并根据 queryPurchasesAsync 的响应更新授权。在后端,您应监听新令牌的 SUBSCRIPTION_PURCHASED RTDN,使用 purchases.subscriptionsv2 API 识别旧订阅的 linkedPurchaseToken,并及时撤消与旧令牌关联的访问权限,同时授予新使用权。请务必确认新的购买交易。

11. 处理 DEFERRED 替换

与立即替换模式不同,ReplacementMode.DEFERRED 会将订阅更改和使用权更新延迟到当前结算周期结束时。处理延迟替换需要特定的逻辑,以确保用户在适当的时间获得正确的授权。

示例场景

  • 用户拥有基础包月方案(商品 ID 为 basic_plan,购买令牌为 basic_purchase_token_123),该方案将于 4 月 15 日续订
  • 4 月 1 日,用户决定使用 ReplacementMode.DEFERRED 改为 Premium 方案。
  • Google 会立即为高级方案(商品 ID 为 premium_plan,购买令牌为 premium_purchase_123)创建购买令牌,但向用户收取的金额和相应权益会安排在 4 月 15 日生效。

处理延迟替换

1. 购买流程成功后立即启动(应用)

  • 购买流程完成后,系统会调用 PurchasesUpdatedListener。应用将收到一个包含新购买令牌 premium_purchase_token_123Purchase 对象,但 product_id 仍会引用旧 basic_plan,因为用户仅拥有 Basic 方案的使用权。您必须将此视为新购买交易并确认令牌。
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                // purchase.getPurchaseToken() = premium_purchase_token_123
                // purchase.getProducts() will contain basic_plan
                // Verify and acknowledge the purchase
                handleNewPurchase(purchase);
            }
        }
    }
    
  • queryPurchasesAsync 会立即返回带有新购买令牌 (premium_purchase_token_123) 的购买交易以及与其关联的原始使用权 (basic_plan)。您可以依靠此功能继续向用户授予基本方案的许可。

2. 购买流程成功后立即启动(后端)

  • 购买流程结束后,立即针对新购买令牌 (premium_purchase_token_123) 发送 SUBSCRIPTION_PURCHASED RTDN。RTDN 载荷示例
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":4, // SUBSCRIPTION_PURCHASED
        "purchaseToken":"premium_purchase_token_123" //purchase token for the new subscription
      }
    }
    
  • 使用新购买令牌调用 GET purchases.subscriptionsv2 以获取购买交易详情。响应包含 2 个订单项。
    • 一项代表旧订阅(基础方案),且具有未来的 expiryTime。旧订阅不会续订,并且具有包含新订阅(高级版方案)的 deferredItemReplacement。这表示旧授权在过期后将替换为新授权。
    • 一个表示新购买的订阅。未为“expiryTime”设置值
    API 响应示例
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "startTime": "2026-05-07T15:50:11.383Z",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>",
      "linkedPurchaseToken": "basic_purchase_token_123",
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // Premium Plan has no expiry time
          "autoRenewingPlan": {...},
          "offerDetails": {...},
          "itemReplacement": {. // Subscription replacement details
            "productId": "basic_plan",
            "replacementMode": "DEFERRED",
            "basePlanId": "monthly-auto-renewing"
          },
          "offerPhase": {}
        },
        {
          "productId": "basic_plan", // Subscription to be replaced
          "expiryTime": "2026-05-07T15:54:34.768Z", // Expiry time in the future
          "autoRenewingPlan": {},
          "offerDetails": {...},
          "deferredItemReplacement": { // identifier indicating this subscription will be replaced upon renewal
            "productId": "subscription_premium"
          },
          "latestSuccessfulOrderId": "GPA.<order_id>",
          "itemReplacement": {...},
        }
      ],
      "etag": "<etag>"
    }
    
  • 必须确认新的购买令牌。此操作可以在应用内或后端完成。如需详细了解如何处理和确认购买交易,请参阅开发者文档
  • 针对旧购买令牌 (basic_purchase_token_123) 发送 SUBSCRIPTION_EXPIRED RTDN。RTDN 载荷示例
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":13, // SUBSCRIPTION_EXPIRED
        "purchaseToken":"basic_purchase_token_123" //purchase token for the old subscription
      }
    }
    
  • 使用旧购买令牌调用 GET purchases.subscriptionsv2 API 时,显示为已过期 (SUBSCRIPTION_STATE_EXPIRED)。旧方案剩余时长的使用权会转移到新购买交易。

3. 在替换日期 - 购买流程后的首次续订(应用)

  • queryPurchasesAsync 会返回带有新购买令牌 (premium_purchase_token_123) 的购买交易以及与其关联的新订阅 (premium_plan)。
  • 新购买交易应在购买流程成功后已处理完毕,因此除了确保向用户授予对正确订阅的访问权限之外,您无需执行任何特殊操作。

4. 在替换日期 - 购买流程后的首次续订(后端)

  • 使用 ReplacementMode.DEFERRED 时,首次续订将遵循任何其他处理 SUBSCRIPTION_RENEWED RTDN 的续订的标准行为。当发生这种情况时,您无需针对替换处理特殊逻辑。
  • 使用新购买令牌调用 GET purchases.subscriptionsv2 以获取购买交易详情。响应包含 2 个订单项。
    • 一项代表旧订阅(基础方案),且 expiryTime 为过去的时间。旧订阅的 deferredItemReplacement 字段将不再设置值。
    • 一项代表新订阅,其 expiryTime 为未来时间,且 autoRenewEnabled 字段设置为 true
    API 响应示例
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>..0",
      "linkedPurchaseToken": "basic_purchase_token_123", // purchase token of the old subscription
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // New subscription
          "expiryTime": "2026-05-07T16:00:09.437Z", // Expiry time set in the future
          "autoRenewingPlan": {
            "autoRenewEnabled": true, // Auto Renewing Flag set to True
            "recurringPrice": {...}
          },
          "offerDetails": {...},
          "latestSuccessfulOrderId": "GPA.<order_id>..0",
          "itemReplacement": {. // Details of the subscription replacement
            "productId": "basic_plan",
            "replacementMode": "DEFERRED",
            "basePlanId": "monthly-auto-renewing"
          },
          "offerPhase": {...}
        },
        {
          "productId": "basic_plan", // Old subscription, Does not contains the deferredItemReplacement field
          "expiryTime": "2026-05-07T15:54:34.768Z", // Expiry time set in the past
          "autoRenewingPlan": {},
          "offerDetails": {...},
          "latestSuccessfulOrderId": "GPA.<order_id>..0",
          "itemReplacement": {...},
        }
      ],
      "etag": "<etag>"
    }
    

总结

本部分详细介绍了 ReplacementMode.DEFERRED 所需的特殊处理。您了解到,与立即模式不同,使用权变更仅在当前结算周期结束时发生。本部分介绍了您的应用和后端正确处理初始购买交易、确认新令牌以及在旧订阅过期且新订阅生效时管理使用权切换所需的步骤。

12. 订阅替换 Playground

示例应用中的替换 Playground 功能可让您测试在 Google Play 管理中心账号中配置的订阅产品的订阅升级和降级。本部分介绍了如何使用替换 Playground 功能。

设置

如需使用替换 Playground 功能,请确保满足以下条件:

  • build.gradle 文件中的 packageId 与 Google Play 管理中心内配置的应用一致。
  • 您的测试用户账号已在 Google Play 管理中心内注册为许可测试人员。如需详细了解许可测试,请参阅测试应用的结算实现

订阅替换 Playground

示例应用包含一个替换 Playground 标签页,可用于模拟订阅变更。您可以查询 Play 管理中心内定义的订阅,并使用各种替换模式测试订阅之间的切换。此 Playground 可帮助您了解不同模式如何影响订阅的结算周期和使用权,以便您确定哪些选项最符合您的业务需求。

如需使用 Playground 模拟替换,请按以下步骤操作:

  1. 前往园地标签页。
  2. 如果您有有效订阅:系统会突出显示该订阅。这是将被替换的订阅。

Playground 首页

  1. 如果您没有有效订阅:您需要先购买订阅。
    • 默认情况下,Playground 会列出为此 Codelab 创建的基本高级Lite 方案。
    • 如需测试在 Play 管理中心内配置的其他方案,请点击添加自定义方案,然后按 productIdbasePlanId 进行搜索。
    • 购买所选订阅。
    • 系统现在应会显示用户新购买的有效订阅。添加自定义订阅
  2. 选择用户想要改用目标订阅。
  3. 为过渡选择替换模式

选择替换模式

  1. 点击测试替换按钮以模拟订阅替换。
  2. 您应该会看到 Google Play 结算服务底部动作条,其中包含订阅替换的计算详情(例如立即收取的费用和结算周期调整)。

订阅替换结算购物车

  1. 完成交易。
  2. 前往 Play 商店应用中的管理订阅页面,查看有效订阅的变更,以及更新后的续订日期和价格的详细信息。

Play 商店订阅管理

13. 后续步骤

参考文档

14. 恭喜

恭喜!您已成功实现采用各种按比例分摊模式的订阅替换,并为方案转换配置了后端处理。

您学到的内容

  • 如何使用特定替换模式配置 SubscriptionProductReplacementParams
  • 立即升级和延迟降级之间的区别。
  • 如何使用 RTDN 通过 linkedPurchaseToken 使旧订阅令牌失效。

调查问卷

我们非常重视您对此 Codelab 的反馈。请考虑抽出几分钟时间填写我们的调查问卷。