PAYATHON 2026

当前订阅到期前升级时,如何延后新订阅的生效日期?

支付小周

结论

不要在服务端手动修改订阅的 startTimeMillis 或 expiryTimeMillis。Google Play 没有提供可任意调整这些时间的 API。

发起订阅变更时,应当:

  • 传入当前订阅的 purchaseToken,让 Google Play 知道新订阅用于替换现有订阅,并非一份独立订阅。
  • 使用 ReplacementMode.DEFERRED。
  • 在当前计费周期结束前,继续保留旧订阅的权益。
  • 旧订阅到期后,由 Google Play 自动启用新订阅并扣费。

如果没有传入旧订阅的 purchaseToken,Google Play 会将新购买视为另一份独立订阅。两份订阅可能同时有效,造成问题中提到的权益重叠。

为什么会发生订阅重叠

Google Play 不会根据商品名称自动判断订阅升级关系。即使两个商品分别代表”月订阅”和”三个月订阅”,直接使用普通购买流程时,它仍可能把两次购买当作彼此独立的订阅。

一次完整的升级流程需要明确提供:

  1. 当前有效订阅的 purchaseToken。
  2. 用户准备购买的新订阅商品或 base plan。
  3. 订阅替换模式。

DEFERRED 表示延迟替换。旧订阅保持有效,到期时间不变;新订阅等旧订阅到期后再生效。

以问题中的时间为例:

  • 月订阅从 1 月 1 日开始,原计划在 2 月 1 日左右续订。
  • 用户在 1 月 15 日选择三个月订阅。
  • 使用 DEFERRED 后,月订阅仍然有效,直到原计费周期结束。
  • 三个月订阅在旧周期结束时开始计费并生效,不会从 1 月 15 日立即开始。

具体时间应以 Google Play 返回的时间戳为准,不要把每个”月”都按固定的 30 天计算。

Billing Library 实现示例

下面的代码使用较新的 Google Play Billing Library ProductDetails API。项目中实际可用的常量取决于当前使用的 Billing Library 版本。

fun launchDeferredSubscriptionChange(
    activity: Activity,
    billingClient: BillingClient,
    oldPurchaseToken: String,
    newProductDetails: ProductDetails,
    newOfferToken: String
): BillingResult {
    val productDetailsParams =
        BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(newProductDetails)
            .setOfferToken(newOfferToken)
            .build()

    val subscriptionUpdateParams =
        BillingFlowParams.SubscriptionUpdateParams.newBuilder()
            .setOldPurchaseToken(oldPurchaseToken)
            .setSubscriptionReplacementMode(
                BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED
            )
            .build()

    val billingFlowParams =
        BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(listOf(productDetailsParams))
            .setSubscriptionUpdateParams(subscriptionUpdateParams)
            .build()

    return billingClient.launchBillingFlow(activity, billingFlowParams)
}

其中必须正确设置旧订阅的 token:

.setOldPurchaseToken(oldPurchaseToken)

同时将替换模式设为 DEFERRED:

.setSubscriptionReplacementMode(
    BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED
)

oldPurchaseToken 必须来自用户当前有效的旧订阅。不能传入新订阅的 token,也不能用 orderId 代替。

较旧版本的 Billing Library 可能使用以下接口实现相同功能:

BillingFlowParams.ProrationMode.DEFERRED

新版本正逐步用 SubscriptionUpdateParams.ReplacementMode 取代旧接口。没有必要为了照搬示例而降级依赖,应按项目当前的 Billing Library 版本选择 API。

服务端应该如何处理

客户端完成购买流程后,服务端仍需通过 Google Play Developer API 验证购买状态,并以 Google Play 返回的数据为准。

服务端至少需要保存:

  • 用户账户标识。
  • 订阅商品及 base plan。
  • 当前 purchaseToken。
  • 到期时间。
  • 自动续订状态。
  • 新旧订阅之间的关联。
  • 实时开发者通知事件的处理结果。

采用延迟替换时,不能在用户点击升级后立即移除旧订阅权益。服务端应按照旧订阅的到期时间继续授权,收到新订阅生效或续订通知后,再切换到新方案。

查询订阅时,如果项目使用较新的接口,可优先考虑 purchases.subscriptionsv2.get。旧版响应可能包含:

{
  "startTimeMillis": "1535366609722",
  "expiryTimeMillis": "1535368525233",
  "autoRenewing": true
}

这些字段表示 Google Play 管理的开始时间、到期时间和自动续订状态。它们是查询结果,不能通过更新请求自由修改。

时间戳可以按毫秒转换,例如:

val expiryInstant = Instant.ofEpochMilli(expiryTimeMillis.toLong())

判断用户是否仍有权益时,不能只检查 autoRenewing。用户取消自动续订后,autoRenewing 可能变为 false,但在 expiryTimeMillis 之前,通常仍享有已经支付的订阅权益。服务端还要结合购买状态、到期时间、暂停、宽限期、账号保留期和撤销等状态进行判断。

是否需要取消第一个订阅

通常不需要,也不应提前单独取消旧订阅。

购买新方案时,只要通过订阅替换流程传入旧订阅的 purchaseToken,Google Play 就会处理旧订阅的续订关系。选择 DEFERRED 后,旧订阅将持续到当前周期结束,再由新订阅接替。

以下几种操作含义不同:

  • cancel:通常只关闭后续自动续订,不会立即终止当前已付款周期的权益。
  • revoke:可能立即撤销订阅权益,不适合正常的升级或降级。
  • 退款:这是财务处理,不等同于订阅替换。

如果先取消旧订阅,再把新订阅作为独立商品购买,两笔交易之间不会自动建立可靠的替换关系,还可能造成重复订阅、权益中断或状态同步困难。

注意事项

不要自己计算三个月后的日期

订阅续订日期应由 Google Play 决定。月份长度、时区、付款重试、宽限期、暂停和账号保留等因素都可能影响实际状态。服务端应使用 API 返回的到期时间,并根据实时开发者通知更新权益。

确认两个方案支持订阅替换

新旧方案必须符合 Google Play 对订阅变更的适用条件。在现代订阅模型下,月方案和三个月方案通常应配置为可以相互替换的 subscription/base plan。实际支持范围还取决于商品类型、base plan 类型和项目使用的 Billing Library 版本。

防止用户再次走普通购买入口

升级按钮应先读取当前有效购买,再把对应的 purchaseToken 传入 SubscriptionUpdateParams。如果无法查到旧 token,不应直接改走普通购买流程,否则可能再次创建并行订阅。

做好重复通知和状态延迟处理

购买回调和实时开发者通知可能重复、延迟或乱序到达。服务端必须以幂等方式处理这些事件,并在重要状态发生变化后重新调用 Google Play Developer API 验证,不能只依赖客户端上报。

这个需求不需要修改新订阅的日期。正确的处理方式是使用 Google Play 的延迟订阅替换机制,传入旧订阅的 purchaseToken,并选择 ReplacementMode.DEFERRED。这样,新方案会在旧订阅周期结束后接续生效。

备注:内容仅供参考。