[貝聊科技]貝聊 IAP 實戰之訂單繫結

貝聊科技發表於2017-12-21

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

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

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

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

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

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

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

原始碼在這裡。

上兩篇文章已經針對 IAP 的九個大的問題中的八個問題進行了詳細的講解,如果你沒有看上一篇文章,建議你先去看一下再回來,因為這三篇文章是循序漸進的。上一篇文章解決了第一篇文章提出的九個問題中的八個,還剩下一個,這一個問題相當關鍵,所以單獨用一篇文章來講解。

01.為什麼如此關鍵?

到現在為止,是不是感覺所有的問題都運籌帷幄,心裡有數了?

那只是假象,show me the code,程式設計不是紙上談兵,而是需要親自動手實踐,細節是魔鬼。有位前輩說:“同樣是一個 for 迴圈,你寫在這裡只值 5 毛錢,但是我寫在那裡就值 5 萬塊”。當然這不是炫耀,而是想誇張的表達程式設計中細節的重要性。

前兩篇講的內容已經可以串起來一個相對嚴謹的支付流程了。但是要把整個流程串起來,還差了關鍵的一步,而這一步並非易事,至少作者走這一步就非常不容易。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

這一步是什麼呢?就是要將公司伺服器生成的訂單號 orderNo 繫結到蘋果的交易 paymentTransaction 上。第一篇文章中說了,蘋果的規範是用一個 product 生成一個 payment,然後將這個 payment 推入到 paymentQueue 之中,最後我們成為交易事務的監聽者,在監聽方法裡拿到交易的 paymentTransaction,我們放進去一個蘋果的 payment 例項,最後得到的是一個 paymentTransaction

問題來了,我們最後拿到的是一個 paymentTransaction,蘋果只告訴我們 哪一個 paymentTransaction 成功了,而我們根本就沒法將我們自己的訂單號繫結到這個成功的 paymentTransaction 上,從而建立對映,正確的去後臺驗證這個訂單。

而將我們自己的訂單對映到 paymentTransaction 又是必須的,下面就一起來看看這揪心的最後一步是怎麼走的。

02. 堪當大任的 applicationUsername?

我不相信蘋果會連這個問題都沒想到,於是就去找文件, paymentTransaction 裡有一個 payment ,這個 payment 就是我們自己用 product 建立的,但是 payment 的所有屬性都是 readonly 的,沒法更改。好在有一個 SKMutablePayment,這個傢伙的有些屬性是 readwrite 的,其中有一個屬性叫做 applicationUsername

var applicationUsername: String
An opaque identifier for the user’s account on your system.
複製程式碼

這是一個 iOS 7 以後才有的屬性,可以允許我們自己往 payment 裡儲存一個字串型別的資料。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

這不就剛好嘛,我就說蘋果不可能連這麼簡單的需求都想不到。好,就用這個屬性就 OK 了。當使用者點選購買的時候,首先去後臺生成一筆交易,然後拿到交易訂單號 orderNo,然後將這個訂單號儲存到 payment 上面,然後在蘋果支付成功的回撥中獲取到 paymentTransacion,然後從這個 paymentTransacionpayment 中將儲存的訂單號取出來,那麼就能實現我們自己的訂單號和蘋果的訂單一一對映,perfect!

作者剛開始就是按照這個原理去實現的,直到功虧一簣。

事情是這樣的,作者公司的測試發現一旦某個訂單未推入 keychain 中持久化,而是等重啟的時候再去檢查未持久化的交易然後將其推入持久化佇列的時候,就會產生崩潰,從 bugly 後臺看到的資料顯示,是因為取 applicationUsername 的時候取不到。然後我就連上電腦測試,發現只要將 APP kill 掉,再次去取之前儲存的 applicationUsername 的時候就是 nil。說到底就是蘋果根本就沒有給我們存進去的資訊做持久化,蘋果自己的屬性都有持久化,唯獨 applicationUsername 沒有。

“雞肋雞肋,食之無肉,棄之有味”,形象的表達了 applicationUsername 這個屬性的尷尬。show must go on,還是得繼續尋找這關鍵一環的解決方案。

03.充分利用 purchasing?

接下來我就嘗試,既然蘋果不給我們的 applicationUsername 屬性做持久化,那能不能我們自己來做呢?

所有的交易都是有唯一的交易標識的,我們如果能將所有的交易在 purchasing 狀態就存起來,那麼當某筆交易是 purchased 的時候,我們就能以交易標識為引子去一堆之前儲存的 purchasing 狀態的 paymentTransaction 中找到對應的交易,然後取到我們之前持久化的 applicationUsername。如果這樣能行得通,那我們就又能把整個過程串起來了。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

“理想很豐滿,現實很骨感”。某筆交易狀態還是 purchasing 時,支付系統還沒有為這筆交易分配交易標識,所以就算是存了,也沒有辦法在那筆交易的狀態變為 purchased 時從之前持久化的資料中找到存的資料。

這個方案也只能作罷。

04.粗放式驗證?

從以上兩個嘗試再結合蘋果後臺不對賬的風格,我們大致能體會到,IAP 的設計思想就是不想讓我們能夠將自己的訂單關聯到 IAP 的訂單,這也符合蘋果一貫想控制一切的作風。

在真正的解決方案浮出水面之前,作者規劃了一種**“粗放式的驗證”**來應對這種窘況,下面我們來講一下什麼叫做“粗放式驗證”。

我們將進入 purchasing 的所有訂單都持久化起來,然後此時雖然沒有分配交易標識,但是產品標識還是有的。等某筆交易到了 purchased 的時候,我們用這個 purchased 的交易的產品標識去持久化的交易中將所有是這個產品標識的交易都取出來組成一個陣列,然後任一取一筆進行驗證,只要驗證成功了,就算交易成功。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

如果難以理解,那我們就對著上面這個圖來看看。我們將自己的訂單號存到交易裡,然後將交易存起來,那麼自己的訂單號也得到了持久化。以後在 purchased 的時候去取任意一筆交易的時候(指定產品標識的),其實取的是我們後臺生成的任意一個交易訂單號(指定產品標識的),然後將已經完成的 IAP 交易和我們的訂單號拼接組合起來進行驗證。

這種方案確實是能達到我們驗證的目的。但是對於有潔癖的同學來說,這個方案只能算是過渡方案,稱不上完美,更談不上優雅,所以只能叫做“粗放式的”。而且有一個沒法避免的問題是,我們存的那麼多 purchasing 狀態的交易,只有少數能在使用以後刪除,大多數都是無效的。但是我們又沒有一個契機能去清理這個持久化資料,因為我們根本無從知道那個交易是有用的,哪個是無用的。所以我們只能全部儲存,不敢清理,這樣導致這個持久化資料越來越多,卻沒有清理的可能。

05.打破思維慣性

現在想明白了就會知道,以上的嘗試迂迂迴回,都是掉進了思維慣性裡了。我們嚴苛遵循了古老的傳統:先去自己伺服器建立訂單,再使用 IAP 交易。其實突破點就在這裡,我們後端的一個同事提出,先去蘋果那裡交易,交易完成以後再去我們自己的伺服器建立訂單是否可行?

還記得第一篇文章中的這張圖嗎?

[貝聊科技]貝聊 IAP 實戰之訂單繫結

我們調轉支付流程以後,應該變成下面這樣。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

我不做解釋了,聰明的你一定知道這個微妙的區別帶來的極大的便利。至此,訂單繫結得到了優雅的解決。

06.方案缺陷分析

如果是按照這個邏輯來走的話,有一個很顯而易見的邏輯缺陷,從 IAP 支付到我們去後臺建立訂單這個過程有蘋果支付的和我們建立訂單的延時。現在情景是使用者 A 發起了支付,然後還未購買就退出了登入,然後用 B 賬號登入了,然後 IAP 支付成功,我們將支付資訊存進了以 B 的 userid 為 key 的賬戶中,這樣就會導致我們去後臺驗證的時候會把錢充到 B 賬戶中,如下圖所示。

[貝聊科技]貝聊 IAP 實戰之訂單繫結

所以我們在使用者退出登入的時候需要去檢查他是否有未完成交易,如果有就要給個警告。但是還是沒辦法徹底解決掉這個問題,但是考慮到這個結果是使用者的行為導致的,而且出現這個問題的機率不大,暫時就這樣處理。

如果你確實有這方面的擔心,那就應該採用上面說的粗放式的驗證,粗放式的驗證是不存在這個問題的。

相關文章