Apple内购订阅基本流程梳理

364 阅读3分钟

一、优化后的方案架构


1. 数据流设计

复制

App → 发起订阅 → 苹果服务器 → 返回收据 → 本地存储
App → 发送收据 → 服务器 → 验证收据 → 返回订阅状态
App ← 定期检查 ← 服务器 ← 订阅状态更新

2. 防止丢单策略

  • 本地收据缓存
  • 失败重试机制
  • 定期状态同步
  • 服务器端状态备份

二、代码实现(Swift)


1. 订阅管理器核心类

import StoreKit

class SubscriptionManager: NSObject {
    static let shared = SubscriptionManager()
    
    private var availableSubscriptions = [SKProduct]()
    private let receiptRefreshQueue = DispatchQueue(label: "com.yourapp.receiptRefresh")
    
    // 订阅商品ID
    let subscriptionIds: Set<String> = [
        "com.yourapp.monthly",
        "com.yourapp.yearly"
    ]
    
    override init() {
        super.init()
        SKPaymentQueue.default().add(self)
        startReceiptMonitoring()
    }
    
    deinit {
        SKPaymentQueue.default().remove(self)
    }
    
    private func startReceiptMonitoring() {
        // 每12小时检查一次订阅状态
        Timer.scheduledTimer(withTimeInterval: 43200, repeats: true) { _ in
            self.validateReceiptWithServer()
        }
    }
}

2. 订阅状态管理

extension SubscriptionManager {
    enum SubscriptionStatus {
        case active
        case expired
        case inGracePeriod
        case unknown
    }
    
    struct SubscriptionInfo {
        let productId: String
        let expiresDate: Date
        let isInTrial: Bool
        let isAutoRenewing: Bool
    }
    
    private var currentSubscription: SubscriptionInfo?
    
    func getSubscriptionStatus() -> SubscriptionStatus {
        guard let subscription = currentSubscription else { return .unknown }
        
        if subscription.isAutoRenewing {
            return .active
        }
        
        if Date() < subscription.expiresDate {
            return .inGracePeriod
        }
        
        return .expired
    }
}

3. 收据验证与防丢单

extension SubscriptionManager {
    private func validateReceiptWithServer() {
        guard let receiptData = fetchReceiptData() else {
            refreshReceipt()
            return
        }
        
        let receiptString = receiptData.base64EncodedString()
        
        // 发送到服务器验证
        sendReceiptToServer(receiptString) { [weak self] result in
            switch result {
            case .success(let subscriptionInfo):
                self?.currentSubscription = subscriptionInfo
                self?.storeReceiptLocally(receiptData)
            case .failure(let error):
                self?.handleValidationError(error)
            }
        }
    }
    
    private func fetchReceiptData() -> Data? {
        guard let receiptURL = Bundle.main.appStoreReceiptURL else { return nil }
        return try? Data(contentsOf: receiptURL)
    }
    
    private func storeReceiptLocally(_ data: Data) {
        // 存储到UserDefaults或Keychain
        UserDefaults.standard.set(data, forKey: "latestReceipt")
    }
    
    private func handleValidationError(_ error: Error) {
        // 错误处理策略
        if let storedReceipt = UserDefaults.standard.data(forKey: "latestReceipt") {
            // 使用本地缓存的收据重试
            let receiptString = storedReceipt.base64EncodedString()
            sendReceiptToServer(receiptString)
        }
    }
}

4. 服务器通信

extension SubscriptionManager {
    private func sendReceiptToServer(_ receipt: String, completion: @escaping (Result<SubscriptionInfo, Error>) -> Void) {
        let requestBody: [String: Any] = [
            "receipt-data": receipt,
            "password": "your_shared_secret", // 从App Store Connect获取
            "exclude-old-transactions": true
        ]
        
        let url = URL(string: "https://your-server.com/validate-receipt")!
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data,
                  let response = try? JSONDecoder().decode(ServerResponse.self, from: data) else {
                completion(.failure(ValidationError.invalidResponse))
                return
            }
            
            // 解析订阅信息
            if let subscription = self.parseSubscription(from: response) {
                completion(.success(subscription))
            } else {
                completion(.failure(ValidationError.invalidSubscription))
            }
        }.resume()
    }
    
    private struct ServerResponse: Codable {
        let latestReceiptInfo: [ReceiptInfo]
        let pendingRenewalInfo: [RenewalInfo]
    }
    
    private struct ReceiptInfo: Codable {
        let productId: String
        let expiresDate: String
        let isTrialPeriod: String
    }
    
    private struct RenewalInfo: Codable {
        let autoRenewProductId: String
        let autoRenewStatus: String
    }
}

5. 订阅生命周期管理

extension SubscriptionManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased, .restored:
                handleSuccessfulTransaction(transaction)
            case .failed:
                handleFailedTransaction(transaction)
            default:
                break
            }
        }
    }
    
    private func handleSuccessfulTransaction(_ transaction: SKPaymentTransaction) {
        // 立即验证收据
        validateReceiptWithServer()
        
        // 标记交易完成
        SKPaymentQueue.default().finishTransaction(transaction)
        
        // 记录交易ID防止重复处理
        UserDefaults.standard.set(transaction.transactionIdentifier, forKey: "lastProcessedTransactionId")
    }
    
    private func handleFailedTransaction(_ transaction: SKPaymentTransaction) {
        if let error = transaction.error as? SKError {
            switch error.code {
            case .paymentCancelled:
                print("用户取消支付")
            default:
                // 记录失败交易以便重试
                saveFailedTransaction(transaction)
            }
        }
        SKPaymentQueue.default().finishTransaction(transaction)
    }
    
    private func saveFailedTransaction(_ transaction: SKPaymentTransaction) {
        // 将失败交易存储到本地
        let failedTransaction = [
            "productId": transaction.payment.productIdentifier,
            "transactionId": transaction.transactionIdentifier ?? "",
            "date": Date().timeIntervalSince1970
        ] as [String : Any]
        
        var failedTransactions = UserDefaults.standard.array(forKey: "failedTransactions") as? [[String: Any]] ?? []
        failedTransactions.append(failedTransaction)
        UserDefaults.standard.set(failedTransactions, forKey: "failedTransactions")
    }
    
    func retryFailedTransactions() {
        guard let failedTransactions = UserDefaults.standard.array(forKey: "failedTransactions") as? [[String: Any]] else { return }
        
        for transaction in failedTransactions {
            if let productId = transaction["productId"] as? String,
               let product = availableSubscriptions.first(where: { $0.productIdentifier == productId }) {
                let payment = SKPayment(product: product)
                SKPaymentQueue.default().add(payment)
            }
        }
        
        // 清空失败记录
        UserDefaults.standard.removeObject(forKey: "failedTransactions")
    }
}

三、服务器端验证流程


1. 验证接口设计

POST /validate-receipt
{
  "receipt-data": "base64_receipt",
  "password": "shared_secret"
}

2. 响应处理

{
  "status": 0,
  "latest_receipt_info": [
    {
      "product_id": "com.yourapp.monthly",
      "expires_date": "2023-12-31 23:59:59 Etc/GMT",
      "is_trial_period": "false"
    }
  ],
  "pending_renewal_info": [
    {
      "auto_renew_product_id": "com.yourapp.monthly",
      "auto_renew_status": "1"
    }
  ]
}

3. 状态码处理

  • 0: 成功
  • 21000-21010: 各种验证错误
  • 21100-21199: 内部服务器错误

四、防止丢单策略实现


1. 本地缓存机制

  • 存储最新收据
  • 记录最后处理的交易ID
  • 保存失败交易记录

2. 重试机制

  • 网络错误自动重试
  • 定期检查未完成交易
  • 应用启动时验证订阅状态

3. 服务器端备份

  • 存储用户订阅历史
  • 定期与苹果服务器同步
  • 提供状态查询接口

五、测试与调试建议


  1. 使用StoreKit Testing进行本地测试
  2. 模拟各种网络错误场景
  3. 测试订阅续期和过期流程
  4. 验证恢复购买功能
  5. 测试跨设备同步

这个优化后的方案专注于订阅管理,提供了完整的防丢单机制,并通过服务器端验证确保订阅状态的准确性。实际部署时,请根据具体业务需求调整细节。