內(nèi)購支付踩過的坑以及自己的解決途徑

更新:經(jīng)過這幾天的用戶反饋及自己的查找,發(fā)現(xiàn)了一些問題。首先,在添加觀察者之前是獲取不到未完成訂單的,只有在觀察者的updateTransaction方法中才能獲取到,所以,我和服務(wù)端同事聯(lián)調(diào)做了如下調(diào)整:

上個版本做的內(nèi)購支付,在內(nèi)購封裝方法中有過初步介紹和整理,結(jié)果在版本上線后收到用戶的反饋說是支付成功,但是充值賬戶卻不能到賬,結(jié)果引發(fā)了退款等惡性問題,下面就我在實(shí)際項(xiàng)目中遇到的問題以及解決方案給出詳細(xì)的介紹(上述給出的鏈接是swift版本的,由于筆者項(xiàng)目依舊是OC語言,所以下面依舊以O(shè)C語言來介紹)

1.封裝的內(nèi)購工具一定要設(shè)置為單例模式,且在程序啟動的時候初始化并在初始化中設(shè)置觀察者模式

筆者上個版本中雖說封裝了內(nèi)購支付工具,但是由于經(jīng)驗(yàn)缺乏,內(nèi)購工具只在支付頁面中有效,結(jié)果有一個巨大的坑,用戶可能在支付完成之前就退出了支付頁面,導(dǎo)致了支付成功但是卻沒有充值成功的情形,在檢查代碼之后,我將內(nèi)購支付工具做成了單例,而且,這個單例的初始化放在了程序入口處,這一點(diǎn)要說明的是,為什么放到入口處呢?是因?yàn)榉诺竭@里,如果之前有未移除的訂單,可以在這里做一些邏輯處理,因?yàn)轫?xiàng)目及實(shí)際情況,筆者是這樣處理的:

這個方法不能奏效,移除不用,此思路就是錯的

- (void)removeOldTransaction {

/*
    NSArray *tansactions = [SKPaymentQueue defaultQueue].transactions;
    //如果沒有移除過訂單信息
    BOOL result = NO;
    
    if ( ![kUserDefaults boolForKey:@"hasFinishOldTransaction"] && tansactions.count > 0) {
        for (SKPaymentTransaction *transaction in tansactions) {
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
        result = YES;
    }
    [kUserDefaults setBool:YES forKey:@"hasFinishOldTransaction"];
    if (result) {
        return;
    }
*/
}

+ (instancetype)sharedInstance {

    static YGIAPTool *tool;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool = [[YGIAPTool alloc] init];
    });
    return  tool;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
       // [self removeOldTransaction];移除不用
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

為什么要移除掉舊的訂單呢?因?yàn)槲抑暗腻e誤邏輯,導(dǎo)致一些訂單就算支付成功而且成功充值,也沒有移除訂單,這個時候如果設(shè)置了觀察者,蘋果提供的系統(tǒng)API中會自動去查詢有沒有未移除的訂單,這樣就會繼續(xù)執(zhí)行充值邏輯,可能會造成重復(fù)充值的情形,為了避免這種情況帶來的損失,筆者就只能硬性要求在版本升級后啟動時移除舊的訂單,這樣就不會有這種隱憂了。

更新:此處描述有誤,硬性移除訂單是不可取的,會給用戶造成一定的損失,這里只需要指定updateTranscation方法,按照正確邏輯走就可以了

didFinishLaunching中調(diào)用初始化方法 [YGIAPTool sharedInstance];

更新,關(guān)于何時移除訂單的問題,之前想著本地存取憑證可以管理訂單,后來偶然間發(fā)現(xiàn),盡管是同一個訂單,如果有未完成的,每次啟動app,執(zhí)行到updateTransaction方法后,走到Purchased狀態(tài)后,取出的憑證都是不一樣的,而交易的transactionIdentifier是一樣的,所以在訂單移除的問題上做了一些調(diào)整,首先,本地不用管理憑證,因?yàn)楣芾硪矝]有用。因?yàn)闃I(yè)務(wù)需求,我們不再存儲憑證,而是存儲交易id,每次判斷本地是否有交易id,如果某一條交易已經(jīng)有交易id了,就記錄到服務(wù)端,方便以后對賬。這個時候結(jié)束交易我們選擇放到了充值成功,也就是success之中,同時移除掉本地存儲的交易id。

2.關(guān)于何時移除訂單的問題

我之前搜索過相關(guān)的問題,網(wǎng)上給出的答案大都是在充值業(yè)務(wù)成功之后再移除訂單,這個也有一定的問題,主要的就是網(wǎng)絡(luò)問題或者是用戶在充值完成之前就退出或者意外中斷的時候引發(fā)的問題,這些情況下都會造成訂單不能及時移除,給支付體驗(yàn)和充值風(fēng)險(xiǎn)上帶來一定的問題。那么,怎么解決這種情況呢?當(dāng)然,我所提供的方案也只是相對自己遇到的問題上有所改善,至于全面而深入的方案,有知道的大神麻煩指點(diǎn)一下,不勝感激。

我們都知道,如果在客戶端去處理驗(yàn)證憑證的邏輯,很容易被有心人入侵做手腳,這個時候常用的保險(xiǎn)做法就是客戶端將本次交易產(chǎn)生的憑證發(fā)給服務(wù)端,讓服務(wù)端去和蘋果服務(wù)器驗(yàn)證,在一定程度上能夠保證了安全性,那么這樣也有一個隱憂,萬一我傳給服務(wù)端了,但是服務(wù)端驗(yàn)證失敗了呢?或者萬一由于網(wǎng)絡(luò)問題傳送失敗呢?這個時候再加一層保險(xiǎn),就是客戶端在傳遞給服務(wù)端之前先將本憑證存儲下來(關(guān)于存儲方法,筆者在后面會介紹,這里也有),然后服務(wù)器驗(yàn)證成功,返回到我們的success回調(diào)中去移除本地憑證,而相對應(yīng)的服務(wù)端也已經(jīng)存儲了我們的憑證,當(dāng)然考慮到服務(wù)器驗(yàn)證失敗的問題,這個邏輯就要在服務(wù)端處理,筆者這里簡單說下:就是服務(wù)器接到客戶端傳的憑證后,也是先存下來,直到驗(yàn)證成功并充值完成后才移除,否則就定時去發(fā)送驗(yàn)證,知道成功為止。
服務(wù)端不多做介紹,主要還是客戶端邏輯,在移除本地憑證后,如果服務(wù)端正常處理,那么充值就應(yīng)該到位了。

3.關(guān)于存儲憑證的坑

筆者一開始存儲用的是NSUserDefault方法,在每次支付成功后都會存儲憑證到本地,然后在服務(wù)器驗(yàn)證成功后,將本地存儲的憑證清空。這樣看似乎沒有毛病,但是如果用戶頻繁操作,會導(dǎo)致創(chuàng)建兩次或者更多次訂單,那么問題來了,NSUserDefault只能覆蓋(因?yàn)榇鎯Φ膽{證對應(yīng)的key是同一個),這樣會造成只能保留最后一個存儲的憑證,會產(chǎn)生一些意想不到的支付問題,所以在得知這個之后,筆者改成了用數(shù)據(jù)庫存儲到本地,這樣我就可以在驗(yàn)證成功后根據(jù)當(dāng)前憑證去刪除數(shù)據(jù)庫中的數(shù)據(jù),而且還有一個好處是,如果憑證發(fā)送失敗,在合適的地點(diǎn)我可以遍歷數(shù)據(jù)庫中的憑證,然后進(jìn)行憑證驗(yàn)證,這樣用戶支付過的訂單就很難出現(xiàn)充值不對等的問題(到賬延遲問題是必然的,這個不知道有什么好方法沒)

4.關(guān)于觀察者方法updatedTransactions對應(yīng)狀態(tài)的處理問題。

SKPaymentTransactionStatePurchased:充值成功

SKPaymentTransactionStateFailed:充值失敗

SKPaymentTransactionStateRestored:恢復(fù)內(nèi)購

SKPaymentTransactionStatePurchasing:正在采購

對于這四種狀態(tài)對應(yīng)的處理情況,我這里簡單介紹一下:
正在采購:只要添加訂單,第一步就會走到這里,這里可以不作處理,要注意的是千萬不能在這里移除訂單,否則會崩潰,提示不能再采購狀態(tài)移除訂單。

至于恢復(fù)內(nèi)購,筆者倒沒有遇到,不過這里主要進(jìn)行以下操作

- (void)removeTransaction {

    [[SKPaymentQueue defaultQueue] finishTransaction:self.currentTransaction];
}

只需要移除訂單就好了

充值失?。何阌怪靡桑@時候訂單交易失敗,就是廢訂單了,所以同樣要移除

充值成功:能進(jìn)入到這里,說明用戶支付成功,錢已經(jīng)扣掉了,那么它之后的相關(guān)處理就比較重要了,為了說明清晰,筆者用代碼來展示:

更新

- (void)requestValidReceipt:(SKPaymentTransaction *)transaction {
    
    self.currentTransaction = transaction;

    //交易驗(yàn)證
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];
    
    if(!receiptData){
        [kWindow showLoadingView:@"獲取支付憑證為空"];
        return;
    }
    //轉(zhuǎn)化為base64字符串
    NSString *receiptString= [receiptData base64EncodedStringWithOptions:0];;
    NSString *source = @"";
    if ([YGDataBase isReceiptExists:self.currentTransaction.transactionIdentifier]) {
        self.buyId = [YGDataBase getBuyIdWithReceipt:self.currentTransaction.transactionIdentifier];
        source = @"self.buyId = [YGDataBase getBuyIdWithReceipt:receiptString];";
    }else {
        source = @"購買界面";
        [self buySuccess];
        //1.先將交易id存起來
        [YGDataBase saveReceiptAndGoodsID:self.currentTransaction.transactionIdentifier goodId:self.buyId];
    }
    [self startValidReceipt:receiptString source:source];

    //2.傳給服務(wù)端憑證數(shù)據(jù)
    [kWindow showLoadingView];
    [[YGNetWorkTool sharedInstance] ApplePayReceiptVerifyBuyId:self.buyId buyType:1 receipt:receiptString success:^(id responseObj) {
        [kWindow hideLoadingView];
        if ([responseObj[@"code"] intValue] != 200 ) {
            [kWindow showLoadingView:responseObj[@"msg"]];
        }else {//充值成功之后將憑證移除
             [self removeTransaction];
            [YGDataBase removeReceipt:self.currentTransaction.transactionIdentifier];
        }
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        [self showAlert];
        self.buyId = nil;
       
        
    } failure:^(NSError *error) {
        [kWindow hideLoadingView];
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        self.buyId = nil;
    }];

}

- (void)requestValidReceipt:(SKPaymentTransaction *)transaction {
    
    self.currentTransaction = transaction;

    //獲取交易的憑證
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];
    
    if(!receiptData){
        [kWindow showLoadingView:@"獲取支付憑證為空"];
        return;
    }
    //轉(zhuǎn)化為base64字符串
    NSString *receiptString= [receiptData base64EncodedStringWithOptions:0];
    //判斷本地是否已經(jīng)有過這個憑證,如果有,為了避免重復(fù)交易,什么也不做(這個可能沒什么用,不過為了財(cái)政安全和保險(xiǎn),加上也不錯)
    if ([YGDataBase isReceiptExists:receiptString]) {
        return;
    }

    [self buySuccess];//這個不用管,是項(xiàng)目中的統(tǒng)計(jì)作用

    //1.先將憑證存起來
    [YGDataBase saveReceiptAndGoodsID:receiptString goodId:self.ID];
//移除當(dāng)前支付的交易
    [self removeTransaction];
//統(tǒng)計(jì)日志
    [self startValidReceipt:receiptString];
    
    //2.傳給服務(wù)端憑證數(shù)據(jù)
    [kWindow showLoadingView];
    [[YGNetWorkTool sharedInstance] ApplePayReceiptVerifyBuyId:self.ID buyType:1 receipt:receiptString success:^(id responseObj) {
        [kWindow hideLoadingView];
        if ([responseObj[@"code"] intValue] != 200 ) {
            [kWindow showLoadingView:responseObj[@"msg"]];
        }else {//充值成功之后將憑證移除 這一點(diǎn)要注意,一定是服務(wù)端返回200的時候才能將本地憑證移除,否則會造成支付后沒到賬的丟單問題
            
            [YGDataBase removeReceipt:receiptString];
        }
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        [self showAlert];
        self.ID = nil;
        
    } failure:^(NSError *error) {
        [kWindow hideLoadingView];
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        self.ID = nil;
    }];

}

按照這個邏輯走下來,一般的內(nèi)購支付問題應(yīng)該能夠解決了,筆者也是花了兩天的時間,反復(fù)驗(yàn)證測試,將各種可能出現(xiàn)的奇葩操作都測試了一遍,結(jié)果充值都能夠正常進(jìn)行,希望能夠給有需要的童鞋一些幫助,有需要源碼的同學(xué),可以到我的github上查看相關(guān)的邏輯(里面附帶的一些牽扯到公司業(yè)務(wù),筆者有做了詳細(xì)的注釋),喜歡的可以給個贊或者?星哦

寫在最后:由于蘋果官方給出的驗(yàn)證方法非常簡單,網(wǎng)上相關(guān)的內(nèi)購資料也大都基于官方文檔,許多實(shí)際問題根本找不到方法,希望大家能多多分享些這方面的實(shí)際問題,為以后內(nèi)購的開發(fā)提供便利。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內(nèi)容

  • 《非銀行支付機(jī)構(gòu)網(wǎng)絡(luò)支付業(yè)務(wù)管理辦法》條款釋義 - 中國支付網(wǎng) - 中國支付行業(yè)第一門戶網(wǎng)站2016年7月1日...
    菜菜苔閱讀 7,626評論 1 44
  • 最近開發(fā)一個項(xiàng)目涉及到內(nèi)購, 也遇到過一些問題. 這里拿出來分享一下, 避免一些人走彎路.開頭先聊一聊最近蘋果關(guān)于...
    東方_未明閱讀 6,220評論 16 56
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,825評論 18 139
  • 九月份的時候到公司,開始制作一個游戲的sdk,包括了登錄注冊,初始化,信息收集等功能... 這些相對來說簡單些,最...
    DovYoung閱讀 2,761評論 5 7
  • 【讀經(jīng)】 箴言5 【金句】 恐怕將你的尊榮給別人,將你的歲月給殘忍的人;(箴言 5:9 和合本) 【感動】 經(jīng)文講...
    chanor閱讀 669評論 0 0