圖片來源:https://unsplash.com/photos/r...
本文作者:zoulp
近年來中國移動應用出海勢頭良好。對於涉及到交易業務的出海應用來說,Google 應用內購買是必不可少的支付渠道。不同於國內相對完善的移動支付體系,即使官方文件中對如何接入 Google 應用內購買做了基本闡述,但是在接入的過程中,還是會遇到很多問題。本文將介紹交易的重點流程和核心技術要點,以及需要注意的問題。
接入過程
名詞解釋
首先解釋三對概念來幫助理解 Google 支付的邏輯。
一次性商品 vs 訂閱型商品
一次性商品是通過單次購買獲得的商品。一次性商品又分為消耗型商品和非消耗型商品,消耗型商品顧名思義是可以被消耗的商品,例如 App 提供的金幣或虛擬貨幣,使用者可以重複購買。非消耗型商品是通過一次購買可以得到的永久權益,例如付費升級的內容。
訂閱型商品是指會定期發生購買行為的商品,如會員服務等,訂閱會自動續期,直至取消。
本文的討論僅限於消耗型商品,不涉及其他型別的商品。
Consume vs Acknowledge
Consume 和 Acknowledge 均有完成支付後進行確認的含義,但兩者並不完全相同。
Acknowledge 是實際意義上的確認操作,進行了 Acknowledge 會使得訂單不被退款,Acknowledge 可由客戶端 API acknowledge()) 執行,也可由 Google 服務端 API acknowledge() 完成。 Google 會對已支付但未確認的訂單在三天後進行自動退款處理。
Consume 是專門針對消耗型商品的操作,Consume 不僅包含確認的含義,並使得商品可以重複購買。Consume 可以看成是包含了 Acknowledge 操作,Consume 僅可由客戶端 API consumeAsync() 完成,不能通過服務端 API 進行。
業務服務端 vs Google 服務端
本文中將多次提到關於服務端的操作,分別使用業務服務端和 Google 服務端來進行區分,避免混淆。業務服務端指的是 App 業務邏輯的服務端。Google 服務端在本文特指 Google 應用內購買的服務端,由 Google 提供。
交易流程概覽
從業務的角度出發,一次交易流程可以大體用下圖表示:
但在實際交易的過程中,充滿了不確定的因素,如使用者的網路環境不穩定、誤操作等等。由於交易業務的敏感性,既不能讓使用者多付錢,也不能少付、錯付。因此需要全面考慮各種可能出現的情況,對可能導致訂單和支付狀態異常的因素做充分的考量和處置。站在技術的視角,完整的交易流程如下:
交易重點流程詳解
建立訂單
建立訂單這裡指的是建立業務服務端的訂單。訂單建立完成後,會將業務訂單 ID 和 對應的 Google 商品 ID 傳遞到後續的步驟中。業務服務端會管理維護自身的商品以及訂單,這與 Google 的商品和訂單並不相同,且需要建立和維護它們之間的關聯關係。
建立連線
在進行 Google 支付前,需要與 Google Play 建立連線,連線的橋樑是 BillingClient。BillingClient 是 Google Play Billing Library 提供的重要工具,支付相關的操作都與之有關。倘若由於網路等原因連線斷開,必須要重連才能繼續後續操作。
查詢商品
對於一個商品,需要提前在 Google 後臺完成建立。查詢商品是查詢 Google 後臺配置的商品資訊,確認商品的資訊無誤,取得商品詳情,為發起支付提供必要的資料。
發起支付
發起支付是通過 BillingClient 的 API launchBillingFlow()
呼叫 Google 的支付介面,此時對應的 Google 支付的服務端訂單被建立。業務服務端訂單和 Google 服務端訂單需關聯起來,常規的方式是客戶端在後續發起訂單校驗時,告知業務服務端對應 Google 訂單 ID,現實中客戶端可能由於某些原因沒有收到支付結果,在由 Google 開發者實時通知回撥業務服務端的場景下,同樣需要足夠的資訊來關聯。在這裡將業務服務端 ID 通過混淆的方式傳入,目的是使得業務服務端憑藉混淆 ID 將 Google 訂單和業務服務端訂單關聯起來,完成後續的確認和履約。
訂單確認
收到支付成功的回撥後,客戶端主動呼叫 BillingClient 提供的 consumeAsync()
方法,確保訂單已確認不被退款,並且可再次被購買。對於未確認的訂單,Google 會在三天後自動退款。此外,需要主動向業務服務端發起訂單的檢查。
訂單履約
按照流程,客戶端發起訂單的校驗,服務端確認了訂單的有效無誤後需要進行履約,業務上通常表現為發放金幣、餘額增加等。服務端必須保證此操作的冪等性,履約一次且僅且履約一次,這是訂單補償機制的前提。
訂單補償
一個支付的流程可能被打斷,需要重點關注的是是使用者支付完成但是沒有收到權益的情況,此過程容易造成客訴,需要通過 訂單補償 作為兜底。
訂單補償分為兩個場景,一個是業務服務端通過 Google 提供的實時開發者通知,接收到 Pub/Sub 訊息,得知訂單支付狀態發生變化,此時檢查訂單的狀態,若是未履約態,會進行履約保障使用者的權益,並且呼叫服務端 acknowledge API 確認訂單,確保不出現自動退款而造成資損的情況。
另一個場景是由客戶端主動發起訂單補償機制,在合適的時機,獲取已支付但未確認的訂單進行後續的 Consume 和履約過程。主動補償可以在啟動 App、進入充值購買頁面等多個時機進行,可根據業務場景自行決定。同時也會將第一種場景邏輯完善,第一個場景下服務端 Acknowledge 完成,權益得到發放,但是該商品卻無法再次購買,此時由客戶端完成 Consume,使得商品可重複購買,形成整體邏輯的閉環。
技術實現
交易流程的一個重要的特點是事件驅動。在技術上表現為,需要在大量的回撥方法中決定下一步的操作。按照程式導向的方式,程式碼會層層回撥巢狀。這樣的程式碼邏輯不夠清晰、難以理解,無數的回撥會導致異常處理複雜,排查問題也比較困難。為了解決這個問題,可以將整個交易流程看成是一個 pipeline,將每一步抽象成子模組。
Google Play Billing Library 直接提供的回撥是無法組成 pipeline 的,需要進行一層轉換。Kotlin Coroutine 提供的 CallbackFlow 是一個 Flow 構建器,可以將基於回撥的 API 轉換成 Flow。
邏輯封裝
整個交易的流程拆分層若干的子模組,使得子模組是可銜接和可複用的。每個子模組有特定的輸入和輸出,上一個子模組的輸出是下一個子模組的輸入,例如查詢商品子模組的輸出是商品詳情資訊,而商品詳情作為發起支付的輸入,展示在輸入皮膚上。並且,每個子模組需保證自身邏輯內聚,僅關心當前流程需要完成的工作,不關心接下來的流程。
我們將拆分後的獨立的子模組包裝成 CallbackFlow。操作成功後通過 offer 方法在協程外傳送資料到後續的 Flow 中,出現錯誤可以終止 close() 或取消 cancel() 當前 Flow。拆分的顆粒度由操作和回撥共同決定,原則是模組功能單一且內聚。此外,在整個操作過程中,都需要用到 BillingClient 的例項,輸入時將傳遞該例項。
fun queryPurchasesFlow(client: BillingClient?): Flow<List<Purchase>> =
callbackFlow {
client?.queryPurchasesAsync(
BillingClient.SkuType.INAPP
) { p0, p1 ->
when (p0.responseCode) {
BillingClient.BillingResponseCode.OK -> {
// emit the value to the flow
offer(p1)
}
else -> {
// close the flow
close()
}
}
}
awaitClose {
// log & release resources
}
}
pipeline 組建
在完成單個操作的封裝後,需要將這些 Flow 組裝起來。CallbackFlow 提供了操作符用於串聯和轉換 Flow,其中 flatMapConcat 是對上游的元素進行轉換、拍扁並返回新的 Flow。flatMapConcat 是適用於當前場景的。一個串聯的例子如下,通過建立 Google 連線後,獲取商品的資訊,校驗無誤後呼叫支付皮膚。
startConnectionFlow(client).flatMapConcat {
querySkuDetailFlow(client, request)
}.flatMapConcat {
launchBillingFlow(activity, client, it, request)
}.catch { e ->
// catch exception
e.printStackTrace()
}.collect {
processNext(it)
}
此外,還可以靈活的編配 Flow,以實現不同的業務邏輯。訂單補償流程和正常的支付流程不完全相同,需要重新編排對應的 Flow,相同的流程可以與支付邏輯複用,無需重新開發。
整體設計
Google 支付作為一個核心業務元件提供給 App 的其他模組使用,其特點是接入方便、介面簡單。其元件的架構如下:
產品層:支付的呼叫方可以是不同形態的收銀臺,Web 收銀臺和 Native 收銀臺。
介面層:支付元件對外提供介面簡單,支付發起和訂單補償。
管理層:控制器下有資料管理和連線管理。資料管理是管理訂單和商品相關的資料。連線管理是管理 Google 提供的 BillingClient,在合適的時機斷開連線。
核心層:核心層包含業務的核心邏輯,包括建立連線、獲取商品等。並且包含支付模組的日誌與監控。
支撐層:依託於 App 現有的底層基礎功能。
踩坑總結
獲取商品詳情為空
billingClient.querySkuDetailsAsync(params) { p0, p1 ->
when (p0.responseCode) {
BillingClient.BillingResponseCode.OK -> {
// p1 is empty
}
}
}
在早期除錯的過程中,出現通過 google sku id 使用 querySkuDetailsAsync() 獲取到商品資訊為空的情況,經過排查詢到了幾個影響點。
- 需要釋出內測版本,根據 Google Console 的要求釋出一個內測版本,無需等待稽核通過。
- 商品建立存在一定的延遲,在從未成功除錯支付的初期,會出現短暫的延遲,即建立商品的當下無法獲取,間隔一段時間後成功獲取。這個情況後續未出現。
- 包名、簽名不匹配,需要使用上傳 PlayStore 配置的包名和簽名,不需使用提交的同一個包,但是包名和簽名一定要匹配。
無法購買您要買的商品
出現無法購買您要買的商品提示,可能的原因有:
- 檢查測試使用者是否在內測使用者列表和許可測試列表。
- 需要登入內測的賬號,點選接受測試邀請連結,接受邀請。
必要條件
成功完成一次支付,總結起來需要注意以下這些點。
- 測試機的 Google Services Framework 已安裝。
- Google PlayStore 檢測 IP 非國區。依賴於網路環境,也可在 Google PlayStore 設定國家(不是必選項)。
- 測試賬號已新增到谷歌後臺,包括內測使用者新增和許可測試新增,兩者都需要新增。
- 接受測試邀請,找到內測連結,點選接受內測邀請,這一步很重要,容易遺漏。
- 釋出內測版本,應用釋出狀態,無需等待稽核通過。
- 安裝包簽名和包名與提交到 Google 後臺的簽名一致。
- Google PlayStore 的版本更新。
最後
交易業務的特殊性和重要性不言而喻。無論是保障 App 的營收,還是準確執行使用者的交易意願,避免客訴,都要求交易業務儘可能把各種場景考慮完善。對開發者來說,不僅要對整個下單、支付的業務流程瞭然於心,還要通過技術手段,將業務流程抽象,將複雜多變的業務場景和邏輯分支,實現為靈活的編排,提高系統整體的可維護性、健壯性。
本文首先對交易流程進行簡明扼要的介紹,之後藉助 Kotlin Coroutine 的 CallbackFlow 來實現業務鏈路的靈活編排,將層層巢狀的回撥和事件通知轉為流式、線性的寫法。但即便如此,還是不能覆蓋到真實場景中的全部異常情況,除了訂單補償的機制外,還需要通過自動化、智慧化的手段,分析每一個交易失敗的 case,並建設完備的監控和報警機制,不斷總結提高,這是後續需要持續投入的地方。
受限於篇幅,不能將每一個點進行詳盡的介紹,歡迎讀者留言探討。
參考連結
- Google Play’s billing system overview
- Integrate the Google Play Billing Library into your app
- play-billing-samples
- Is it possible acknowledge consumable products from server side?
- 圖解 Monad
- Functors, Applicatives, And Monads In Pictures
- CompletableFuture
- ListenableFutureExplained
- Kotlin flows on Android - Convert callback-based APIs to flows
- Simplifying APIs with coroutines and Flow
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!