[貝聊科技]貝聊 IAP 實戰之滿地是坑

貝聊科技發表於2019-03-04

大家好,我是貝聊科技 的 iOS 工程師 @NewPan

注意:文章中討論的 IAP 是指使用蘋果內購購買消耗性的專案。

這次為大家帶來我司 IAP 的實現過程詳解,鑑於支付功能的重要性以及複雜性,文章會很長,而且支付驗證的細節也關係重大,所以這個主題會包含三篇。

第一篇:[iOS]貝聊 IAP 實戰之滿地是坑,這一篇是支付基礎知識的講解,主要會詳細介紹 IAP,同時也會對比支付寶和微信支付,從而引出 IAP 的坑和注意點。

第二篇:[iOS]貝聊 IAP 實戰之見坑填坑,這一篇是高潮性的一篇,主要針對第一篇文章中分析出的 IAP 的問題進行具體解決。

第三篇:[iOS]貝聊 IAP 實戰之訂單繫結,這一篇是關鍵性的一篇,主要講述作者探索將自己伺服器生成的訂單號繫結到 IAP 上的過程。

不用擔心,我從來不會只講原理不留原始碼,我已經將我司的原始碼整理出來,你使用時只需要拽到工程中就可以了,下面開始我們的內容 。

原始碼在這裡。

01.題外話

今年上半年的公眾號打賞事件,大家可還記得?我們對蘋果強收過路費的行為憤懣,也為微信可惜不已,此事最後以騰訊高管團隊訪問蘋果畫上句號。顯然,協商結果兩位老闆以及他們的團隊都很滿意。

[貝聊科技]貝聊 IAP 實戰之滿地是坑

02.熟悉的支付寶和微信支付

仔細看一下下面這張圖,這是我們每次在買早餐使用支付寶支付的流程圖。下面我們來一步一步看一下每一步對應的操作原理。

[貝聊科技]貝聊 IAP 實戰之滿地是坑

第一步:我們的 APP 發起一筆支付交易,此時,第一件事,我們要去我們自己的伺服器上建立一個訂單資訊。同時伺服器會組裝好一筆交易交給我們。關於組裝交易資訊,有兩種做法,第一種就是支付寶推薦我們做的,由我們伺服器來組裝交易資訊,伺服器加密交易資訊,並儲存簽名資訊;另一種做法是,伺服器返回商品資訊給 APP,由 APP 來組裝交易資訊,並進行加密處理等操作。顯然我們應該採用第一種方式。

第二步:伺服器建立好交易資訊以後,返回給 APP,APP 不對交易資訊做處理。

第三步:APP 拿到交易資訊,開始調起支付寶的 SDK,支付寶的 SDK 把交易資訊傳給支付寶的伺服器。

第四步:驗證通過以後,支付寶伺服器會告訴支付寶 SDK 驗證通過。

第五步:驗證通過以後,我們的 APP 會調起支付寶 APP,跳轉到支付寶 APP。

第六步:在支付寶 APP 裡,使用者輸入密碼進行交易,和支付寶伺服器進行通訊。

第七步:支付成功,支付寶伺服器回撥支付寶 APP。

第八步:支付寶回到我們自己的 APP,並通過 - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation 方法處理支付寶的回撥結果,對應的進行重新整理 UI 等操作。

第九步:支付寶伺服器會回撥我們的伺服器並把收據傳給我們伺服器,如果我們的伺服器沒有確認已經收到支付寶的收據資訊,那麼支付寶伺服器就會一直回撥我們的伺服器,只是回撥時間間隔會越來越久。

第十步:我們的伺服器收到支付寶的回撥,並回撥支付寶,確認已經收到收據資訊,此時早餐買完了。

支付寶的支付流程講完了,那微信支付也講完了,因為它們流程相似。

03.坑爹的 IAP 支付

IAP 坑爹之處從以下兩個方面來理解。

第一方面,APP 不接 IAP 稽核不讓過。接不接 IAP,蘋果不是和你商量,而是強制要求,爸爸說怎麼樣,就怎麼樣。當然,這篇文章解決不了這個問題,所以也只是說說而已。上面說了微信公眾號的事情,雖然它不是 IAP 的事情,但是實質上都屬於強收過路費的行為。

第二方面,坑開發人員。下面開始數坑。

[貝聊科技]貝聊 IAP 實戰之滿地是坑

只有 8 步,比支付寶少 2 步,對不對?看起來比支付寶還簡單,有木有?

第一步:使用者開始購買,首先會去我們自己的伺服器建立一個交易訂單,返回給 APP。

第二步:APP 拿到交易資訊,然後開始調起 IAP 服務建立訂單,並把訂單推入支付佇列。

第三步:IAP 會和 IAP 伺服器通訊,讓使用者確認購買,輸入密碼。

第四步:IAP 伺服器回撥 APP,通知購買成功,並把收據寫入到 APP 沙盒中。

第五步:此時,APP 應該去獲取沙盒中的收據資訊(一段 Base 64 編碼的資料),並將收據資訊上傳給伺服器。

第六步:伺服器拿到收據以後,就應該去 IAP 伺服器查詢這個收據對應的已付款的訂單號。

第七步:我們自己的伺服器拿到這個收據對應的已付款的訂單號以後,就去校驗當前的已付款訂單中是否有要查詢的那一筆,如果有,就告訴 APP。

第八步:APP 拿到查詢結果,然後把這筆交易給 finish 掉。

04.對比支付寶和 IAP

沒啥大毛病,對吧?現在來詳細分析一下。

由於移動端所處的網路環境遠遠比服務端要複雜,所以,最大可能出現問題的是與移動端的通訊上。對於支付寶,只要移動端確實付款完成,那麼接下來的驗證工作都是伺服器於伺服器之間的通訊。這樣一來,只要使用者確實產生了一筆交易,那麼接下來的驗證就變得可靠的多,而且支付寶伺服器會一直回撥我們的伺服器,交易的可靠性得到了極大的保證。

同樣,我們再來看看 IAP,交易是一樣的。但是驗證交易這一環需要移動端來驅動我們自己的伺服器來進行查詢,這是第一個坑,先記一筆。另外一點,IAP 的伺服器遠在美國,我們的伺服器去查詢延時相當嚴重,這是其二

05.IAP 設計上的坑

上面講了兩個很大的坑,接下來看一看 IAP 本身有哪些坑。最大的一個就是,從 IAP 交易結果出來到通知 APP,只有一次。這裡有以下幾個問題:

1.如果使用者後買成功以後,網路就不行了,那麼蘋果的 IAP 也收不到支付成功的通知,就沒法通知 APP,我們也沒法給使用者發貨。

2.如果 IAP 通知我們支付成功,我們驅動伺服器去 IAP 伺服器查詢失敗的話,那就要等下次 APP 啟動的時候,才會重新通知我們有未驗證的訂單。這個週期根本沒法想象,如果使用者一個月不重啟 APP,那麼我們可能一個月沒法給使用者發貨。

3.有人反饋,IAP 通知已經交易成功了,此時去沙盒裡取收據資料,發現為空,或者出現通知交易成功那筆交易沒有被及時的寫入到沙盒資料中,導致我們伺服器去 IAP 伺服器查詢的時候,查不到這筆訂單。

4.如果使用者的交易還沒有得到驗證,就把 APP 給解除安裝了,以後要怎麼恢復那些沒有被驗證的訂單?

5.越獄手機有無數奇葩的收據丟失或無效或被替換的問題,應該怎樣酌情處理?

6.交易沒有發生變化,僅僅是重啟一下,收據資訊就會發生改變。

7.當驗證交易成功以後我們去取 IAP 的待驗證交易列表的時候,這個列表沒有資料。

好吧,算起來有九個比較大的問題了,還有沒照顧到的請各位補充。這九個問題,基本上每一個都是致命的。這麼多的不確定性,我們應該怎麼綜合處理,怎麼相互平衡?

我們先放一放這些問題,下一篇就一起來著手解決這些問題,現在我們先來看一看 IAP 支付的基本程式碼。

06.IAP 支付程式碼

我們先不去想那麼多,先把支付邏輯跑通再說。下面我們看看 IAP 的程式碼。

#import <StoreKit/StoreKit.h>

@interface BLPaymentManager ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>

@end

@implementation BLPaymentManager

- (void)dealloc {
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

- (void)init {
    self = [super init];
    if(self) {
         [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

- (void)buyProduction {
    if ([SKPaymentQueue canMakePayments]) {
        
        [self getProductInfo:nil];
        
    } else {
        NSLog(@"使用者禁止應用內付費購買");
    }
}

// 從Apple查詢使用者點選購買的產品的資訊.
- (void)getProductInfo:(NSString *)productIdentifier {
    NSSet *identifiers = [NSSet setWithObject:productIdentifier];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
    request.delegate = self;
    [request start];
}


#pragma mark - SKPaymentTransactionObserver

// 購買操作後的回撥.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    // 這裡的事務包含之前沒有完成的.
    for (SKPaymentTransaction *transcation in transactions) {
        switch (transcation.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                [self transcationPurchasing:transcation];
                break;
                
            case SKPaymentTransactionStatePurchased:
                [self transcationPurchased:transcation];
                break;
                
            case SKPaymentTransactionStateFailed:
                [self transcationFailed:transcation];
                break;
                
            case SKPaymentTransactionStateRestored:
                [self transcationRestored:transcation];
                break;
                
            case SKPaymentTransactionStateDeferred:
                [self transcationDeferred:transcation];
                break;
        }
    }
}


#pragma mark - TranscationState

// 交易中.
- (void)transcationPurchasing:(SKPaymentTransaction *)transcation {
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    if (!receipt) {
        NSLog(@"沒有收據, 處理異常");
        return;
    }
}

// 交易成功.
- (void)transcationPurchased:(SKPaymentTransaction *)transcation {
    // 儲存到本地先.
    // 傳送到伺服器, 等待驗證結果.
    [[SKPaymentQueue defaultQueue] finishTransaction:transcation];
}

// 交易失敗.
- (void)transcationFailed:(SKPaymentTransaction *)transcation {
    
}

// 已經購買過該商品.
- (void)transcationRestored:(SKPaymentTransaction *)transcation {
    
}

// 交易延期.
- (void)transcationDeferred:(SKPaymentTransaction *)transcation {
    
}


#pragma mark - SKProductsRequestDelegate

// 查詢成功後的回撥.
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray<SKProduct *> *products = response.products;
    if (!products.count) {
        NSLog(@"沒有正在出售的商品");
        return;
    }
    
    SKPayment *payment = [SKPayment paymentWithProduct:products.firstObject];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

@end
複製程式碼

程式碼大致做了如下事情,初始化的時候去新增支付結果的監聽,並在 -dealloc: 方法中移除監聽。同時可以通過 - (void)fetchProductInfoWithProductIdentifiers:(NSSet<NSString *> *)productIdentifiers 方法查詢後臺配置的商品資訊。通過 -buyProduction: 方法購買產品,購買成功以後,IAP 通過 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法通知購買進度。

相關文章