1. 程式人生 > >正確處理ipv6和內購(IAP)及掉單問題

正確處理ipv6和內購(IAP)及掉單問題

最近開發一個專案涉及到內購, 也遇到過一些問題. 這裡拿出來分享一下, 避免一些人走彎路.
開頭先聊一聊最近蘋果關於2017年新的稽核機制和沸沸揚揚的微信和蘋果的撕逼

1. 2017新的稽核機制:

  • ipv6: 使用國內阿里雲的app上架, 大都會遇到ipv6被拒的郵件:
    解決方案:
    方案1. 服務端解決: 配置阿里雲ECS支援IPv6, 新增AAAA解析
    方案2. 客戶端解決: 手機端配置ipv6環境測試, 錄製APP內的操作視訊, 上傳到YouTobe, 將網址傳送給稽核人員即可通過稽核 (ps: 錄製時候一定要錄製APP所在的網路環境: 設定中->無線網路->DNS: 2001:2:0:aab1: :: 1 ,DNS為這種格式則為ipv6)

  • 內購:
    說一說這個專案內購有趣的事情:
    a. 首先做這個專案的時候, 我們充值虛擬幣方案定的是: 後臺做一個開關, app在稽核期間走蘋果內購, 在上線後, 走微信和支付寶支付, 並向低版本相容. 達到繞過蘋果稽核的目的. 結果被拒了, 郵件中提到了支付寶, 當時很懵逼, 就留下了老大的聯絡方式和蘋果溝通, 第二天蘋果打來電話: 說內購的同時不可以使用第三方支付. 由此看來: 第三方支付的相關相關程式碼或SDK被掃描到了. 遂移除掉, 只使用內購方式
    b. 稽核期間, 蘋果發來一封郵件大概意思是問: 你們確定內購的最高價格是你們期望的嗎? 回覆以後才可以繼續稽核, 這裡我的理解是: 我們的內購的最高價格定得很高149美元

    的那一檔, 所以蘋果要確認一下, 經過回覆郵件說明了一下這個最高價格確定是我們自己定的最高價格, 沒有錯誤, 第二天蘋果又恢復了稽核, 變成了稽核中…
    c. app被拒後, 內購專案變成了需要開發人員操作, 盜圖一張:


需要開發人員操作

這時候一般只需要進入需要開發人員操作的內購專案中, 修改一下描述, 重新提交即可, 然後重新提交app. (ps: 一般這裡我只是將描述中新增或刪除空格, 就可以重新提交了)

d. 關於專案中: app內購商品返回列表為空, 返回的都是無效產品
即: [response.products count]始終為0, [response.invalidProductIdentifiers] 有值
這個的原因是: 協議、稅務和銀行業務中必須通過才可以(盜圖一張):


協議、稅務和銀行業務

2. 談一談微信和蘋果的撕逼

新的稽核協議將打賞列為了內購
我的觀點和這個仁兄一樣

3. 閒話扯完了, 看一下怎麼做內購併處理掉單問題:

蘋果官方提供的內購的正確姿勢
蘋果這一文中說明兩點:
a. 在appdelegate中新增觀察者, 在購買成功後提交給自己的伺服器, 由自己伺服器提交憑證到蘋果伺服器驗證正確後, 返回給客戶端之後, 這筆交易才完成, 這時候再queue.finishTransaction(transaction), 如果這期間蘋果的伺服器還沒返回結果 或者 購買成功了,我們提交憑證給自己伺服器的時候網斷掉了(錢空了, 但是虛擬物品沒有到賬, 丟單了), 則這筆交易都沒有完成, 方法queue.finishTransaction(transaction)都沒有呼叫, 所以再次開啟app的時候, 因為appdelegate中添加了觀察者, 就會再次呼叫
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])方法

b. 蘋果推薦進入內購專案表單頁面的時候先請求appstore,根據返回的可銷售商品來進行展示(但是很多app的做法都是呼叫自己的介面取得商品價格列表進行展示, 但是我們不能確定我們自己的伺服器返回的和蘋果返回的不同), 這裡非常抱歉的說明一下: 我們的app也是按照自己伺服器的api返回的資料展示的商品價格列表, 哈哈哈

c. 關於內購和服務端的介面引數, 我們設定為:

  1. 此次交易的使用者的唯一標示符(accountID):
  2. 交易成功的憑證
  3. 此次交易的訂單號
  4. 服務端也要處理重複請求該介面的情況(不要每次請求成功都給使用者加錢..)

說明: 使用者的唯一標示符的作用: 如果使用者購買成功, 但是將憑證給自己服務端的時候斷掉了, 然後自己切換了賬號, 下次開啟app的時候檢測, 我們需要這個表示符知道誰買的..不要將虛擬貨幣充錯使用者

ios7 蘋果增加了一個屬性applicationusername,SKMutablepayment的屬性,所以使用者在發起支付的時候可以指定使用者的username及自己生成的訂單,這樣使用者再下次得到回撥的時候就知道,此交易是哪個訂單發起的了進而完成交易。回撥中獲取username。

上程式碼: (內購工具類)

import Foundation
import StoreKit

enum InpurchaseError: Error {
    /// 沒有內購許可
    case noPermission
    /// 不存在該商品: 商品未在appstore中\商品已經下架
    case noExist
    /// 交易結果未成功
    case failTransactions
    /// 交易成功但未找到成功的憑證
    case noReceipt
}

typealias Order = (productIdentifiers: String, applicationUsername: String)

class Inpurchase: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {

    static let `default` = Inpurchase()

    /// 掉單/未完成的訂單回撥 (憑證, 交易, 交易佇列)
    var unFinishedTransaction: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())?

    private var sandBoxURLString = "https://sandbox.itunes.apple.com/verifyReceipt"
    private var buyURLString = "https://buy.itunes.apple.com/verifyReceipt"

    private var isComplete: Bool = true
    private var products: [SKProduct] = []
    private var failBlock: ((InpurchaseError) -> ())?
    /// 交易完成的回撥 (憑證, 交易, 交易佇列)
    private var receiptBlock: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())?
    private var successBlock: (() -> Order)?

    private override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }

    deinit {
        SKPaymentQueue.default().remove(self)
    }


    /// 開始向Apple Store請求產品列表資料,併購買指定的產品,得到Apple Store的Receipt,失敗回撥
    ///
    /// - Parameters:
    ///   - productIdentifiers: 請求指定產品
    ///   - successBlock: 請求產品成功回撥,這個時候可以返回需要購買的產品ID和使用者的唯一標識,預設為不購買
    ///   - receiptBlock: 得到Apple Store的Receipt和transactionIdentifier,這個時候可以將資料傳回後臺或者自己去post到Apple Store
    ///   - failBlock: 失敗回撥
    func start(productIdentifiers: Set<String>,
               successBlock: (() -> Order)? = nil,
               receiptBlock: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())? = nil,
               failBlock: ((InpurchaseError) -> ())? = nil) {

        guard isComplete else { return }
        defer { isComplete = false }

        let request = SKProductsRequest(productIdentifiers: productIdentifiers)
        request.delegate = self
        request.start()

        self.successBlock = successBlock
        self.receiptBlock = receiptBlock
        self.failBlock = failBlock
    }

    //MARK: - SKProductsRequestDelegate
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        products = response.products
        guard let order = successBlock?() else { return }
        buy(order)
    }

    /// 購買給定的order的產品
    private func buy(_ order: Order) {

        let p = products.first { $0.productIdentifier == order.productIdentifiers }
        guard let product = p else { failBlock?(.noExist); return }
        guard SKPaymentQueue.canMakePayments() else { failBlock?(.noPermission); return }

        let payment = SKMutablePayment(product: product)
        /// 發起支付時候指定使用者的username, 在掉單時候驗證防止切換賬號導致充值錯誤
        payment.applicationUsername = order.applicationUsername
        SKPaymentQueue.default().add(payment)
    }

    //MARK: - SKPaymentTransactionObserver
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // appStoreReceiptURL iOS7.0增加的,購買交易完成後,會將憑據存放在該地址
                guard let receiptUrl = Bundle.main.appStoreReceiptURL,
                      let receiptData = NSData(contentsOf: receiptUrl) else { failBlock?(.noReceipt);return }

                let receiptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))

                if let receiptBlock = receiptBlock {
                    receiptBlock(receiptString, transaction, queue)
                }else{ // app啟動時恢復購買記錄
                    unFinishedTransaction?(receiptString, transaction, queue)
                }
                isComplete = true
            case .failed:
                failBlock?(.failTransactions)
                queue.finishTransaction(transaction)
                isComplete = true
            case .restored: // 購買過 對於購買過的商品, 回覆購買的邏輯
                queue.finishTransaction(transaction)
                isComplete = true
            default:
                break
            }
        }
    }
}

appdelegate中的監聽使用方式:

appdelegate中: 

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        Inpurchase.default.unFinishedTransaction = {(receipt, transaction, queue) in
            // 如果存在掉單情況就會走這裡
            let data = InpurchaseAPIData(accountID: transaction.payment.applicationUsername, //使用者唯一標示
                                         transactionID: transaction.transactionIdentifier, //交易流水
                                         receiptData: receipt)// 憑證
            LPNetworkManager.request(Router.verifyReceipt(data)).showToast().loading(in: self.view).success {[weak self] in
                showToast("恢復購買成功")
                // 記住一定要請求自己的伺服器成功之後, 再移除此次交易
                queue.finishTransaction(transaction)

                }.fail {
                    print("向伺服器傳送憑證失敗")
            }
        }

        return true
    }

點選購買的程式碼:

       // 點選購買
        let productIdentifiers: Set<String> = ["a", "b", "c"]

        Inpurchase.default.start(productIdentifiers: productIdentifiers, successBlock: { () -> Order in
            return (productIdentifiers: "a", applicationUsername: "該使用者的id或改使用者的唯一識別符號")
        }, receiptBlock: { (receipt, transaction, queue) in
            //交易成功返回了憑證
            let data = InpurchaseAPIData(accountID: transaction.payment.applicationUsername,
                                         transactionID: transaction.transactionIdentifier,
                                         receiptData: receipt)
            LPNetworkManager.request(Router.verifyReceipt(data)).showToast().loading(in: self.view).success {[weak self] in
                showToast("購買成功")
                // 記住一定要請求自己的伺服器成功之後, 再移除此次交易
                queue.finishTransaction(transaction)

                }.fail {
                    print("向伺服器傳送憑證失敗")
            }
        }, failBlock: { (error) in
            print(error)
        })

demo地址 能點個star也是極好的, 打不打賞無所謂, 能幫到你就好

還有一種實踐方式, 個人並不推薦, 因為太繁瑣了:
思路: 購買成功後在本地將訂單的使用者, 憑證等資訊儲存到本地(UserDefaults, 資料庫,keyChain等), 將憑證傳送給自己伺服器成功之後再移除此條交易記錄, 每次開啟app的時候, 在本地掃描是否有未完成的訂單, 迴圈傳送給自己的伺服器進行二次驗證

補充:

  1. 關於上線:
    錯誤做法: 上線稽核的時候使用沙箱測試地址, 稽核通過後, 手動釋出上線, 上線後讓伺服器切換到蘋果的正式測試地址

    說明: 這種做法第一次上架可以使用, 但是到第二次迭代稽核的時候, 蘋果測試員使用的是沙盒環境, 但是我們伺服器是正式環境, 會導致報錯誤碼: 21007
    正確的做法: 判斷蘋果正式驗證伺服器的返回code,如果是21007 表示環境不對,則再一次連線測試伺服器進行驗證即可.. (這一步驟即: 先判斷蘋果的環境, 根據蘋果環境切換沙盒地址還是正式地址)

  2. 關於蘋果二次驗證返回的引數:
    服務端\客戶端對蘋果傳送請求進行驗證有時會返回多個交易記錄

說明: 蘋果驗證會返回: 一個未完成交易的陣列(一般只有一個, 就是當前操作購買的這個), 如果有多個為完成的交易,就會返回多個 (這種情況一般是程式碼寫的不對造成的), 服務端根據transactionIdentifier找到當前購買的交易或者取最後一個也是當前購買的交易來做判斷和驗證….經過測試發現如果在當前手機請求發現出現多個未完成的交易, 則換另外一部手機和賬號等, 仍然會返回那些未完成的交易, 看來每次對商品進行購買, 蘋果會把所有未完成的交易都返回(不管這個商品是其他使用者的還是其他手機的)