PAYATHON 2026

UWP 如何判断 Microsoft Store 订阅是否开启 Recurring Billing

支付小周

结论

不能。Windows.Services.Store API 只能告诉 UWP 应用:用户当前是否拥有有效许可,以及许可何时到期。它不会公开 Microsoft Store 账户是否开启了”定期计费(Recurring Billing)”。

因此,只要 ExpirationDate 尚未到期,下面两种情况在应用中可能表现得完全一样:

  • 定期计费已开启,订阅预计会自动续订;
  • 定期计费已关闭,但当前已付款周期仍然有效。

仅凭 StoreLicense.IsActive、StoreLicense.ExpirationDate、StoreSku.SubscriptionInfo 等客户端属性,无法可靠区分这两种状态。

为什么 IsActive 不能用于判断自动续订

StoreLicense.IsActive 表示用户当前是否拥有该 SKU 的有效使用权,不表示下一个计费周期是否会扣款。

用户关闭定期计费后,Microsoft Store 通常不会立即撤销已经支付的订阅权益。当前周期结束前,下面两个条件仍然成立:

license.IsActive == true
license.ExpirationDate > DateTimeOffset.Now

只有当前权益到期,且续订没有带来新的有效周期,许可状态才可能变为非活动状态。也就是说:

IsActive = 当前能否使用订阅功能

而不是:

IsActive = 是否启用了自动续订

StoreSku.SubscriptionInfo.BillingPeriod 和 BillingPeriodUnit 也不能用来判断自动续订。它们描述的是订阅商品的计费周期,例如一个月或一年,并不是用户当前的续订设置。

应用中应如何展示

应用无法确定自动续订状态,因此不应根据 ExpirationDate 显示”将在该日期自动续订”。界面只需陈述许可数据能够确认的事实,例如:

您的订阅当前有效,有效期至 2026 年 9 月 30 日。是否自动续订取决于 Microsoft Store 账户中的定期计费设置。

同时提供”管理订阅”入口,让用户前往 Microsoft 账户的服务与订阅页面,查看或修改定期计费状态。

客户端可以这样保存订阅状态:

public sealed class SubscriptionStatus
{
    public bool IsEntitled { get; set; }
    public DateTimeOffset? EntitlementEndDate { get; set; }

    // Windows.Services.Store 无法提供该值,因此不要把它定义为 bool。
    public bool? IsRecurringBillingEnabled { get; set; }
}

查询许可时,将续订状态明确保留为未知:

using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.Services.Store;

public async Task<SubscriptionStatus> GetSubscriptionStatusAsync()
{
    StoreContext context = StoreContext.GetDefault();
    StoreAppLicense appLicense = await context.GetAppLicenseAsync();

    StoreLicense subscriptionLicense = appLicense.AddOnLicenses
        .Select(item => item.Value)
        .Where(license => license.IsActive)
        .OrderByDescending(license => license.ExpirationDate)
        .FirstOrDefault();

    if (subscriptionLicense == null)
    {
        return new SubscriptionStatus
        {
            IsEntitled = false,
            EntitlementEndDate = null,
            IsRecurringBillingEnabled = null
        };
    }

    return new SubscriptionStatus
    {
        IsEntitled = true,
        EntitlementEndDate = subscriptionLicense.ExpirationDate,
        IsRecurringBillingEnabled = null
    };
}

展示文本可以写成:

public static string FormatSubscriptionMessage(
    SubscriptionStatus status)
{
    if (!status.IsEntitled)
    {
        return "当前没有有效订阅。";
    }

    string endDate = status.EntitlementEndDate?
        .ToLocalTime()
        .ToString("yyyy-MM-dd") ?? "未知日期";

    return $"订阅当前有效,有效期至 {endDate}。" +
           "请在 Microsoft Store 账户中确认是否已开启定期计费。";
}

用户点击按钮后,可以打开 Microsoft 账户的订阅管理页面:

using System;
using System.Threading.Tasks;
using Windows.System;

public static async Task OpenSubscriptionManagementAsync()
{
    await Launcher.LaunchUriAsync(
        new Uri("https://account.microsoft.com/services"));
}

这是账户级页面。用户可能需要登录购买订阅时使用的 Microsoft 账户。

代码中的其他注意事项

不要把 ExpirationDate.DateTime 当成本地时间

ExpirationDate 的类型是 DateTimeOffset,包含时区偏移信息。过早调用 .DateTime 会丢失偏移量。建议在业务模型中继续使用 DateTimeOffset:

pobj_SubscriptionData.SubscriptionEndDate = license.ExpirationDate;

等到需要显示时再转换:

string displayDate = license.ExpirationDate
    .ToLocalTime()
    .ToString("yyyy-MM-dd");

如果现有属性只能保存 DateTime,应明确它表示 UTC 还是本地时间,以免在订阅到期边界出现一天的显示误差。

不要把所有活动附加项都当作目标订阅

当前循环会让最后一个活动的 add-on 覆盖之前保存的数据:

foreach (var addOnLicense in appLicense.AddOnLicenses)
{
    StoreLicense license = addOnLicense.Value;

    if (license.IsActive)
    {
        // 每次都会覆盖
    }
}

如果应用包含多个 durable add-on 或多个订阅 SKU,应使用已知的 SkuStoreId 或其他稳定标识筛选目标订阅,例如:

StoreLicense license = appLicense.AddOnLicenses
    .Select(item => item.Value)
    .FirstOrDefault(item =>
        item.IsActive &&
        item.SkuStoreId == expectedSubscriptionSkuStoreId);

不要自行推断续订状态

下面这些条件都不能可靠判断续订状态:

  • ExpirationDate 接近当前日期;
  • 用户以前成功购买过订阅;
  • SKU 是订阅商品;
  • 许可仍为 IsActive;
  • 到期日期比一个计费周期更远;
  • 本地保存的上一次到期日期与当前日期不同。

到期日期延后,只能证明用户获得了新的权益周期,不能证明定期计费目前仍处于开启状态。用户可能在续订后立即关闭定期计费。

是否存在其他可靠方法

如果问题限定在 Windows.Services.Store 客户端 API,答案仍然是没有。应用只能读取当前权益状态,不能读取用户的自动续订开关。

除非 Microsoft 另行提供服务端接口,且明确记录该接口及相应字段适用于这类订阅产品,否则也不应根据订单历史、日期变化或本地缓存推断定期计费状态。无法取得权威状态时,应用应当:

  1. 使用 IsActive 决定当前是否开放订阅功能;
  2. 使用 ExpirationDate 显示当前权益的有效期;
  3. 将自动续订状态显示为”请在 Microsoft Store 中确认”;
  4. 提供 Microsoft 账户订阅管理入口;
  5. 不承诺”将在某日自动续订”,也不要把 ExpirationDate 直接描述为确定的取消日期。

备注:内容仅供参考。