更新:經(jīng)過這幾天的用戶反饋及自己的查找渗稍,發(fā)現(xiàn)了一些問題挑宠。首先,在添加觀察者之前是獲取不到未完成訂單的郑口,只有在觀察者的updateTransaction方法中才能獲取到鸳碧,所以,我和服務(wù)端同事聯(lián)調(diào)做了如下調(diào)整:
上個版本做的內(nèi)購支付犬性,在內(nèi)購封裝方法中有過初步介紹和整理瞻离,結(jié)果在版本上線后收到用戶的反饋說是支付成功,但是充值賬戶卻不能到賬乒裆,結(jié)果引發(fā)了退款等惡性問題琐脏,下面就我在實際項目中遇到的問題以及解決方案給出詳細(xì)的介紹(上述給出的鏈接是swift版本的,由于筆者項目依舊是OC語言缸兔,所以下面依舊以O(shè)C語言來介紹)
1.封裝的內(nèi)購工具一定要設(shè)置為單例模式日裙,且在程序啟動的時候初始化并在初始化中設(shè)置觀察者模式
筆者上個版本中雖說封裝了內(nèi)購支付工具,但是由于經(jīng)驗缺乏惰蜜,內(nèi)購工具只在支付頁面中有效昂拂,結(jié)果有一個巨大的坑,用戶可能在支付完成之前就退出了支付頁面抛猖,導(dǎo)致了支付成功但是卻沒有充值成功的情形格侯,在檢查代碼之后鼻听,我將內(nèi)購支付工具做成了單例,而且联四,這個單例的初始化放在了程序入口處撑碴,這一點要說明的是,為什么放到入口處呢朝墩?是因為放到這里醉拓,如果之前有未移除的訂單,可以在這里做一些邏輯處理收苏,因為項目及實際情況亿卤,筆者是這樣處理的:
這個方法不能奏效,移除不用鹿霸,此思路就是錯的
- (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;
}
為什么要移除掉舊的訂單呢排吴?因為我之前的錯誤邏輯,導(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è)務(wù)需求瞪浸,我們不再存儲憑證,而是存儲交易id吏祸,每次判斷本地是否有交易id对蒲,如果某一條交易已經(jīng)有交易id了,就記錄到服務(wù)端犁罩,方便以后對賬齐蔽。這個時候結(jié)束交易我們選擇放到了充值成功,也就是success之中床估,同時移除掉本地存儲的交易id含滴。
2.關(guān)于何時移除訂單的問題
我之前搜索過相關(guān)的問題,網(wǎng)上給出的答案大都是在充值業(yè)務(wù)成功之后再移除訂單丐巫,這個也有一定的問題谈况,主要的就是網(wǎng)絡(luò)問題或者是用戶在充值完成之前就退出或者意外中斷的時候引發(fā)的問題,這些情況下都會造成訂單不能及時移除递胧,給支付體驗和充值風(fēng)險上帶來一定的問題碑韵。那么,怎么解決這種情況呢缎脾?當(dāng)然祝闻,我所提供的方案也只是相對自己遇到的問題上有所改善,至于全面而深入的方案遗菠,有知道的大神麻煩指點一下联喘,不勝感激。
我們都知道辙纬,如果在客戶端去處理驗證憑證的邏輯豁遭,很容易被有心人入侵做手腳,這個時候常用的保險做法就是客戶端將本次交易產(chǎn)生的憑證發(fā)給服務(wù)端贺拣,讓服務(wù)端去和蘋果服務(wù)器驗證蓖谢,在一定程度上能夠保證了安全性,那么這樣也有一個隱憂譬涡,萬一我傳給服務(wù)端了闪幽,但是服務(wù)端驗證失敗了呢?或者萬一由于網(wǎng)絡(luò)問題傳送失敗呢涡匀?這個時候再加一層保險沟使,就是客戶端在傳遞給服務(wù)端之前先將本憑證存儲下來(關(guān)于存儲方法,筆者在后面會介紹渊跋,這里也有坑
),然后服務(wù)器驗證成功腊嗡,返回到我們的success回調(diào)中去移除本地憑證着倾,而相對應(yīng)的服務(wù)端也已經(jīng)存儲了我們的憑證,當(dāng)然考慮到服務(wù)器驗證失敗的問題燕少,這個邏輯就要在服務(wù)端處理卡者,筆者這里簡單說下:就是服務(wù)器接到客戶端傳的憑證后,也是先存下來客们,直到驗證成功并充值完成后才移除崇决,否則就定時去發(fā)送驗證,知道成功為止底挫。
服務(wù)端不多做介紹恒傻,主要還是客戶端邏輯,在移除本地憑證后建邓,如果服務(wù)端正常處理盈厘,那么充值就應(yīng)該到位了。
3.關(guān)于存儲憑證的坑
筆者一開始存儲用的是NSUserDefault方法官边,在每次支付成功后都會存儲憑證到本地沸手,然后在服務(wù)器驗證成功后,將本地存儲的憑證清空注簿。這樣看似乎沒有毛病契吉,但是如果用戶頻繁操作,會導(dǎo)致創(chuàng)建兩次或者更多次訂單诡渴,那么問題來了捐晶,NSUserDefault只能覆蓋(因為存儲的憑證對應(yīng)的key是同一個),這樣會造成只能保留最后一個存儲的憑證妄辩,會產(chǎn)生一些意想不到的支付問題惑灵,所以在得知這個之后,筆者改成了用數(shù)據(jù)庫存儲到本地恩袱,這樣我就可以在驗證成功后根據(jù)當(dāng)前憑證去刪除數(shù)據(jù)庫中的數(shù)據(jù)泣棋,而且還有一個好處是胶哲,如果憑證發(fā)送失敗畔塔,在合適的地點我可以遍歷數(shù)據(jù)庫中的憑證,然后進(jì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;
//交易驗證
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ù)交易确憨,什么也不做(這個可能沒什么用译荞,不過為了財政安全和保險,加上也不錯)
if ([YGDataBase isReceiptExists:receiptString]) {
return;
}
[self buySuccess];//這個不用管休弃,是項目中的統(tǒng)計作用
//1.先將憑證存起來
[YGDataBase saveReceiptAndGoodsID:receiptString goodId:self.ID];
//移除當(dāng)前支付的交易
[self removeTransaction];
//統(tǒng)計日志
[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 {//充值成功之后將憑證移除 這一點要注意吞歼,一定是服務(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ù)驗證測試丈甸,將各種可能出現(xiàn)的奇葩操作都測試了一遍糯俗,結(jié)果充值都能夠正常進(jìn)行,希望能夠給有需要的童鞋一些幫助睦擂,有需要源碼的同學(xué)得湘,可以到我的github上查看相關(guān)的邏輯(里面附帶的一些牽扯到公司業(yè)務(wù),筆者有做了詳細(xì)的注釋),喜歡的可以給個贊或者?星哦
寫在最后:由于蘋果官方給出的驗證方法非常簡單顿仇,網(wǎng)上相關(guān)的內(nèi)購資料也大都基于官方文檔淘正,許多實際問題根本找不到方法,希望大家能多多分享些這方面的實際問題臼闻,為以后內(nèi)購的開發(fā)提供便利鸿吆。