更新:經(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ā)提供便利。