PAYATHON 2026

如何通过 GetSubscriptionsForUser 判断自动续订偏好变更?

支付阿杰

结论

第五种场景不能只看 recurrenceState。

用户关闭下一周期的自动续订后,当前已支付的订阅周期仍然有效,所以 recurrenceState 仍可能是 Active。判断时需要同时检查:

  • recurrenceState == "Active":当前订阅有效。
  • autoRenew == false:当前周期结束后不会自动续订。
  • expirationTime > 当前时间:当前许可证尚未到期。

因此,第五种场景通常对应以下状态:

recurrenceState = Active
autoRenew = false
expirationTime > now

recurrenceState 表示订阅当前的生命周期状态,autoRenew 表示下一周期是否计划自动续订。

建议的状态判断

可以组合以下字段判断订阅状态:

场景recurrenceStateautoRenewexpirationTime
当前订阅有效,并已成功续订Active通常为 true晚于当前时间
因计费失败进入催款期,当前权益仍有效InDunning通常为 true结合返回值及业务宽限期判断
续订最终失败,订阅已失效Failed不应单独依赖通常不晚于当前时间
用户不再续订且订阅已经到期Inactive通常为 false不晚于当前时间
用户关闭下一周期自动续订,但当前仍有效Activefalse晚于当前时间

状态值的拼写必须与 API 的实际返回值一致。例如,文档中的枚举值是 Inactive,代码中不要自行写成 InActive。

判断步骤

续订任务不能在看到 Active 后直接延长一个完整周期。可以按以下流程处理:

  1. 通过 productId、skuId 等字段找到需要处理的订阅。
  2. 读取 recurrenceState,确定订阅当前的生命周期状态。
  3. 读取 autoRenew,确认下一周期是否仍计划自动扣款。
  4. 读取 expirationTime,确定当前权益实际有效到什么时间。
  5. 将本地许可证有效期同步到 Store 返回的 expirationTime,不要在每次查询到 Active 时固定增加一个周期。
  6. 保存订阅的 id、expirationTime 或 lastModified,保证续订任务重复执行时仍然幂等。

第五种场景可以这样判断:

subscription.RecurrenceState == "Active"
    && subscription.AutoRenew == false
    && subscription.ExpirationTime > DateTimeOffset.UtcNow

C# 示例

以下示例假设 API 响应已经反序列化为本地模型:

public enum SubscriptionDecision
{
    ActiveAndAutoRenewing,
    ActiveButAutoRenewDisabled,
    InDunning,
    RenewalFailed,
    ExpiredWithoutRenewal,
    Unknown
}

public static SubscriptionDecision Classify(
    StoreSubscription subscription,
    DateTimeOffset now)
{
    bool currentlyValid = subscription.ExpirationTime > now;

    if (subscription.RecurrenceState == "Active")
    {
        if (currentlyValid && !subscription.AutoRenew)
        {
            return SubscriptionDecision.ActiveButAutoRenewDisabled;
        }

        if (currentlyValid && subscription.AutoRenew)
        {
            return SubscriptionDecision.ActiveAndAutoRenewing;
        }
    }

    if (subscription.RecurrenceState == "InDunning")
    {
        return SubscriptionDecision.InDunning;
    }

    if (subscription.RecurrenceState == "Failed")
    {
        return SubscriptionDecision.RenewalFailed;
    }

    if (subscription.RecurrenceState == "Inactive" && !currentlyValid)
    {
        return SubscriptionDecision.ExpiredWithoutRenewal;
    }

    return SubscriptionDecision.Unknown;
}

public sealed class StoreSubscription
{
    public string Id { get; init; } = "";
    public string ProductId { get; init; } = "";
    public string SkuId { get; init; } = "";
    public string RecurrenceState { get; init; } = "";
    public bool AutoRenew { get; init; }
    public DateTimeOffset ExpirationTime { get; init; }
    public DateTimeOffset LastModified { get; init; }
}

处理许可证时,可以直接将 Store 返回的到期时间作为上限:

if (subscription.ExpirationTime > localLicense.ExpirationTime)
{
    localLicense.ExpirationTime = subscription.ExpirationTime;
    localLicense.StoreSubscriptionId = subscription.Id;
    localLicense.LastStoreModifiedTime = subscription.LastModified;

    await licenseRepository.SaveAsync(localLicense);
}

这样即使续订任务重复执行,也不会因为多次看到 Active 而反复增加许可证期限。

注意事项

Active 不等于”一定会再次续订”

Active 只能说明当前订阅有效。即使用户已经关闭自动续订,在当前付费周期结束前,订阅仍可能保持 Active。因此,不能仅凭 Active 推断下一周期一定会扣款。

autoRenew 表示计划,不表示续订已经成功

autoRenew == true 表示订阅当前设置为自动续订,并不代表下一次付款已经完成。判断续订是否成功,需要检查 expirationTime 是否向后推进,并结合 recurrenceState 判断。

许可证应同步到 expirationTime

如果任务是延长本地许可证,应将本地有效期更新到 API 返回的 expirationTime,不要根据订阅周期自行计算。这样可以减少重复任务、周期长度差异、催款状态及时间边界引起的错误。

时间比较统一使用 UTC

解析和比较 expirationTime 时,应使用 DateTimeOffset 或其他能够保留时区信息的类型,并统一按 UTC 比较,以免服务器本地时区导致订阅提前或延后失效。

保留未知状态分支

不要假设所有响应都属于预设的五种场景。如果 recurrenceState、autoRenew 或 expirationTime 未知或缺失,应记录原始响应并暂缓延长许可证,避免错误授予权益。

备注:内容仅供参考。