You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity In-App Purchase:如何检测订阅是否已过期?

嘿,这个问题我之前帮好几个开发者踩过坑,Unity里处理订阅过期确实得兼顾本地体验服务器安全,给你梳理一套靠谱的落地方案:

核心思路:双重验证确保订阅状态准确

只靠本地存储肯定不安全(很容易被篡改),所以必须结合「本地缓存+服务器端官方API验证」来实现,下面是具体步骤:

1. 用Unity IAP获取订阅收据并解析到期时间

首先确保你已经在Package Manager里导入了Unity Purchasing包,集成好基础的内购流程。当用户成功购买订阅后,在ProcessPurchase回调里可以拿到订阅的收据,从中解析出到期时间:

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
    Product purchasedProduct = args.purchasedProduct;
    
    // 只处理订阅类型的产品
    if (purchasedProduct.definition.type == ProductType.Subscription)
    {
        DateTime expirationDate = DateTime.MinValue;
        // 区分Android和iOS的收据解析逻辑
        if (Application.platform == RuntimePlatform.Android)
        {
            // 解析Google Play收据
            GooglePlayReceipt playReceipt = JsonUtility.FromJson<GooglePlayReceipt>(purchasedProduct.receipt);
            expirationDate = DateTimeOffset.FromUnixTimeSeconds(playReceipt.expirationTimestampSeconds).DateTime;
        }
        else if (Application.platform == RuntimePlatform.IPhonePlayer)
        {
            // 解析Apple App Store收据(Unity提供了AppleReceipt工具类)
            AppleReceipt appleReceipt = AppleReceipt.GetAppleReceiptFromBase64(purchasedProduct.receipt);
            foreach (var receiptItem in appleReceipt.ReceiptItems)
            {
                if (receiptItem.ProductID == purchasedProduct.definition.id)
                {
                    expirationDate = receiptItem.ExpirationDate;
                    break;
                }
            }
        }

        // 把到期时间临时存在PlayerPrefs(仅做缓存,不能作为唯一判断依据)
        if (expirationDate != DateTime.MinValue)
        {
            PlayerPrefs.SetString("SubExpiry", expirationDate.ToString("o")); // 用ISO格式避免时区问题
            PlayerPrefs.Save();
        }

        // 关键:把收据发送到你的服务器做官方验证,这一步不能省!
        StartCoroutine(SendReceiptToServer(purchasedProduct.receipt, purchasedProduct.definition.id));
    }

    return PurchaseProcessingResult.Complete;
}

// 发送收据到服务器的协程示例
IEnumerator SendReceiptToServer(string receipt, string productId)
{
    WWWForm form = new WWWForm();
    form.AddField("receipt", receipt);
    form.AddField("product_id", productId);
    form.AddField("user_id", PlayerPrefs.GetString("UserID")); // 假设你有用户唯一标识

    using (UnityWebRequest request = UnityWebRequest.Post("https://你的服务器地址/api/verify-sub", form))
    {
        yield return request.SendWebRequest();
        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("收据验证请求失败:" + request.error);
            // 这里可以加重试逻辑,比如隔5分钟再发送一次
        }
    }
}

2. 服务器端调用官方API验证订阅状态

这是确保订阅状态真实有效的核心步骤,本地数据随时可能被篡改,只有官方API返回的结果才可信。

针对Google Play的验证逻辑(伪代码示例)

你的服务器需要调用Google Play Developer API,传入订阅的token和产品ID,获取真实的到期时间、自动续费状态:

import requests
import json

def verify_google_subscription(receipt, product_id, package_name, service_account_key):
    # 从receipt里解析出Google Play的购买token
    receipt_json = json.loads(receipt)
    purchase_token = receipt_json["PurchaseToken"]
    
    # 获取OAuth2令牌(用服务账号密钥生成)
    auth_token = get_google_auth_token(service_account_key)
    
    # 调用Play API验证订阅
    api_url = f"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{package_name}/purchases/subscriptions/{product_id}/tokens/{purchase_token}"
    headers = {"Authorization": f"Bearer {auth_token}"}
    response = requests.get(api_url, headers=headers)
    
    if response.status_code == 200:
        sub_data = response.json()
        return {
            "is_valid": True,
            "expiry_time": sub_data.get("expiryTimeMillis"),
            "auto_renew": sub_data.get("autoRenewing"),
            "cancel_reason": sub_data.get("cancelReason")
        }
    return {"is_valid": False}

针对Apple App Store的验证逻辑(伪代码示例)

调用苹果的App Store验证API,解析PKCS7格式的收据:

import requests

def verify_apple_subscription(receipt, shared_secret):
    payload = {
        "receipt-data": receipt,
        "password": shared_secret, # 你的App Store共享密钥
        "exclude-old-transactions": True
    }
    # 测试用沙盒地址:https://sandbox.itunes.apple.com/verifyReceipt,正式环境用生产地址
    response = requests.post("https://buy.itunes.apple.com/verifyReceipt", json=payload)
    if response.status_code == 200:
        receipt_data = response.json()
        if receipt_data["status"] == 0:
            # 取最新的订阅交易记录
            latest_transaction = receipt_data["latest_receipt_info"][-1]
            return {
                "is_valid": True,
                "expiry_time": latest_transaction["expires_date_ms"],
                "auto_renew": latest_transaction["is_auto_renewing"] == "true"
            }
    return {"is_valid": False}

服务器验证后,把用户的订阅状态(是否有效、到期时间)存在数据库里,供客户端查询。

3. 客户端定期校验订阅状态

客户端不能只依赖本地缓存,需要定期向服务器请求最新状态:

  • 每次App启动时发起校验
  • 可以设置定时器(比如每6小时一次)主动拉取状态
  • 当用户切换账号、重新登录时也需要校验
// 从服务器校验订阅状态的协程
IEnumerator CheckSubscriptionStatusFromServer()
{
    string userId = PlayerPrefs.GetString("UserID");
    UnityWebRequest request = UnityWebRequest.Get($"https://你的服务器地址/api/check-sub?user_id={userId}");
    yield return request.SendWebRequest();
    
    if (request.result == UnityWebRequest.Result.Success)
    {
        SubscriptionStatus status = JsonUtility.FromJson<SubscriptionStatus>(request.downloadHandler.text);
        // 转换时间格式(服务器返回ISO字符串)
        DateTime expiryDate = DateTime.Parse(status.expirationDate);
        UpdateSubscriptionUI(status.isValid, expiryDate);
        // 根据状态开启/关闭付费功能
        status.isValid ? EnablePremiumFeatures() : DisablePremiumFeatures();
    }
    else
    {
        // 网络请求失败,临时用本地缓存的状态(提示用户检查网络)
        string expiryStr = PlayerPrefs.GetString("SubExpiry");
        if (!string.IsNullOrEmpty(expiryStr) && DateTime.TryParse(expiryStr, out DateTime expiryDate))
        {
            bool isStillValid = DateTime.Now < expiryDate;
            UpdateSubscriptionUI(isStillValid, expiryDate);
            isStillValid ? EnablePremiumFeatures() : DisablePremiumFeatures();
        }
        else
        {
            UpdateSubscriptionUI(false, DateTime.MinValue);
            DisablePremiumFeatures();
        }
    }
}

// 定义订阅状态的序列化类
[System.Serializable]
public class SubscriptionStatus
{
    public bool isValid;
    public string expirationDate; // 服务器返回ISO格式字符串
    public bool autoRenew;
}

// 更新UI和功能的辅助方法
void UpdateSubscriptionUI(bool isActive, DateTime expiryDate)
{
    if (isActive)
    {
        subStatusText.text = $"订阅有效,到期时间:{expiryDate.ToLocalTime():yyyy-MM-dd HH:mm}";
    }
    else
    {
        subStatusText.text = "订阅已过期或未激活";
    }
}

4. 处理订阅更新/取消的实时通知

为了让状态更及时,你可以让服务器监听官方的实时通知:

  • Google Play的Real-Time Developer Notifications:当订阅续费、取消、过期时,谷歌会主动通知你的服务器
  • Apple的Server-to-Server Notifications:同样会推送订阅状态变化的事件
    服务器收到通知后,立刻更新数据库里的用户订阅状态,这样客户端下次校验就能拿到最新结果。

避坑提醒

  • 永远不要完全信任本地存储的到期时间,服务器验证是底线
  • 处理时间时统一用UTC格式,避免时区差异导致的判断错误
  • 测试时用官方的测试账号:谷歌用测试订阅计划,苹果用沙盒账号,不要产生真实扣费
  • 对于订阅自动续费的情况,要提醒用户在对应平台的账号设置里管理订阅

内容的提问来源于stack exchange,提问作者Santosh

火山引擎 最新活动