網(wǎng)站設(shè)計美工多少網(wǎng)絡(luò)推廣電話
作者:業(yè)志陳
現(xiàn)如今,App 出海熱度不減,是很多公司和個人開發(fā)者選擇的一個市場方向。App 為了實現(xiàn)盈利,除了接入廣告這種最常見的變現(xiàn)方式外,就是通過提供各類虛擬商品或者是會員服務(wù)來吸引用戶付費了,此時 Google Play 結(jié)算系統(tǒng)(Google Play’s billing system)就是 Android 端應(yīng)用必須使用到的一個支付渠道了
Google 對 Google Play 結(jié)算系統(tǒng)的簡介:Google Play’s billing system is a service that enables you to sell digital products and content in your Android app, whether you want to monetize through one-time purchases or offer subscriptions to your services. Google Play offers a full set of APIs for integration with both your Android app and your server backend that unlock the familiarity and safety of Google Play purchases for your users.
也就是說:Google Play 結(jié)算系統(tǒng)是一項可以讓我們在 Android 應(yīng)用中銷售數(shù)字商品和內(nèi)容的服務(wù)。無論是要通過一次性購買交易創(chuàng)收,還是要為用戶提供訂閱服務(wù),它都能幫我們搞定。Google Play 提供了一整套 API,可集成到 Android 應(yīng)用和服務(wù)器后端中,從而為用戶提供熟悉又安全的 Google Play 購買交易服務(wù)
在最近的一年多時間里,我一直在負責一個海外項目的開發(fā)工作,這個過程中也接入了 Google Play 結(jié)算系統(tǒng)。在剛開始時,由于對當中的各個概念不夠了解,其整體支付流程又和國內(nèi)常用的各類支付服務(wù)相差挺大的,導致我走了不少的彎路
這里我就來寫一篇文章,對 Google Play 結(jié)算系統(tǒng)進行詳細介紹,希望對你有所幫助
一、概述
想要通過 Google Play 結(jié)算系統(tǒng)向用戶展示并售賣商品,自然需要先創(chuàng)建商品,創(chuàng)建商品的方式有兩種:
- 在 Google Play Console 手動創(chuàng)建
- 通過 Google Play Developer API 以代碼的方式創(chuàng)建
在 Google Play 中創(chuàng)建的商品都屬于虛擬商品,每個商品代表的都是 App 給用戶提供的一種權(quán)益,而每個商品都包含一個唯一標識,也即 ProductId,我們在業(yè)務(wù)上就需要根據(jù) ProductId 的命名規(guī)則來定義商品所代表的具體權(quán)益類型
每個商品又可以分為兩種類型:
- 一次性商品。用戶通過單次付費獲得的商品,屬于買斷制,對應(yīng) Google Play 結(jié)算庫中的
BillingClient.ProductType.INAPP
- 訂閱型商品。用戶以固定周期不斷重復付費的商品,屬于訂閱制,對應(yīng) Google Play 結(jié)算庫中的
BillingClient.ProductType.SUBS
當用戶購買了商品后,App 還需要對這筆訂單進行核銷。處理流程和商品類型有關(guān),分為兩種:
- 確認交易。不管購買的商品是什么類型,App 都需要先對這筆交易進行 確認,如果在限定的時間內(nèi)未完成確認,Google Play 就會自動撤銷這筆交易并向用戶退款。“確認交易” 這個操作應(yīng)該是 Google Play 為了讓 App 確定已經(jīng)向用戶提供了權(quán)益,盡量避免出現(xiàn)用戶已付款但 App 沒有向用戶下發(fā)權(quán)益這種情況。確認操作可以由服務(wù)端或者移動端來實現(xiàn),對應(yīng)
acknowledgePurchase
操作 - 消耗商品。消耗商品針對的是一次性商品中的消耗型商品,也即對其執(zhí)行 消耗 操作。通過執(zhí)行消耗操作,使得用戶后續(xù)可以再次購買此商品。消耗操作可以由服務(wù)端或者移動端來實現(xiàn),對應(yīng)
consumePurchase
操作
二、一次性商品
一次性商品也稱為應(yīng)用內(nèi)商品,屬于一次性買斷的商品,具體又可以細分為兩種子類型:
- 消耗型商品。也即是說,此商品在購買后可以被消耗,從而使得用戶可以重復購買。例如,該商品可以用于表示游戲中的金幣,用戶在使用完金幣后該商品代表的權(quán)益就失效了,用戶需要再次購買商品才能再次獲得金幣
- 非消耗型商品。也即是說,此商品在購買后是不可消耗的,用戶可以永久獲得該商品代表的權(quán)益。例如,該商品可以用于表示某課程的觀看權(quán)益,用戶只要購買商品后,就可以永久享有該課程的觀看權(quán)益
一次性商品到底屬于 消耗型 還是 非消耗型 都取決于 App 在業(yè)務(wù)上的定義,在 Google Play Console 中都統(tǒng)一將其稱為 應(yīng)用內(nèi)商品,在創(chuàng)建一次性商品時也沒有區(qū)分子類型的選項
假設(shè)我們對一件一次性商品在業(yè)務(wù)上的定義是消耗型的,那么就可以在適當?shù)臅r候通過執(zhí)行 consumePurchase
來對其執(zhí)行 “消耗” 操作。例如,用戶通過購買某個一次性商品獲得了游戲金幣,用戶在后續(xù)過程中使用這些金幣來購買游戲道具,那么開發(fā)者就需要同時執(zhí)行 consumePurchase
來消耗掉商品,從而使得該商品變?yōu)闊o效狀態(tài),這樣用戶后續(xù)也可以再次購買此商品
而對于非消耗型商品,在業(yè)務(wù)上代表的是用戶可以永久享有的某個權(quán)益,只要買了該商品權(quán)益就不會丟失,因此用戶也不應(yīng)該再次購買,自然也就不需要也不能執(zhí)行消耗操作了
三、訂閱型商品
訂閱型商品,也即需要用戶以固定周期定期進行付費的商品,在付費周期內(nèi)用戶均能享有該商品代表的權(quán)益。最常見的應(yīng)用場景就是各類會員服務(wù):用戶按月付費,App 在每個訂閱周期內(nèi)向用戶提供會員獨有的功能,直至用戶取消訂閱
訂閱型商品包含四個比較重要的概念:
- 基礎(chǔ)方案
- 續(xù)訂類型
- 優(yōu)惠
- 定價階段
基礎(chǔ)方案
基礎(chǔ)方案,也稱為 BasePaln,每個訂閱型商品都必須包含一個或多個基礎(chǔ)方案才能讓用戶購買
基礎(chǔ)方案就用于定義商品的售賣規(guī)則,包括結(jié)算周期、續(xù)訂類型、訂閱價格、優(yōu)惠策略等。例如,一個訂閱型商品可以同時提供 按月付費 和 按年付費 這兩個基礎(chǔ)方案供用戶選擇,每個周期分別設(shè)定不同的價格,用戶根據(jù)喜好來選擇不同的方案進行訂閱
續(xù)訂類型
每個基礎(chǔ)方案均需要指定續(xù)訂類型,用于指定用戶的付費方式
續(xù)訂類型分為兩種:
- 自動續(xù)訂。在每個結(jié)算周期即將結(jié)束時主動向用戶扣款,從而自動延長權(quán)益使用權(quán)的期限。付費操作對于用戶來說是被動的
- 預付費。不會自動續(xù)訂和扣款,用戶需要通過主動付款來推遲權(quán)益使用權(quán)的結(jié)束日期,以此保持不間斷地享有訂閱內(nèi)容。付費操作對于用戶來說是主動的
優(yōu)惠
優(yōu)惠,也稱為 Offer,只有 自動續(xù)訂型 的基礎(chǔ)方案才能設(shè)定優(yōu)惠
每個自動續(xù)訂型的基礎(chǔ)方案可以同時設(shè)定多個優(yōu)惠,讓用戶可以在訂閱初期享受一定的價格折扣或者是直接就免費使用,從而吸引用戶購買
Offer 的類型分為三種,也即分為三種優(yōu)惠策略。例如,假設(shè)現(xiàn)在有一個按月訂閱的基礎(chǔ)方案,我們就可以為其添加以下三個 Offer 供用戶選擇:
- 免費試訂。用戶在前七天內(nèi)免費試用,在七天后再正式進行按月付費
- 單次付款。用戶一次性預付三個月的訂閱費用,總價享受七折折扣,三個月后再按原價進行按月訂閱
- 周期性付款折扣。用戶還是按月訂閱,但前三個月每次付費時均能享受八折折扣,三個月后再按原價進行按月訂閱
價格階段
價格階段,也稱為 PricingPhases,可以看做是 Offer 的一個內(nèi)部屬性
由于一個 Offer 可以同時包含多個優(yōu)惠策略,所以當用戶在享用某個 Offer 時,其需要支出的價格就會隨時間發(fā)生多次變動,每個時間段分別對應(yīng)的不同的價格,PricingPhases 就用于表示 Offer 在每一個時間段的收費規(guī)則
例如,某個按月自動續(xù)訂的基礎(chǔ)方案包含一個 Offer,此 Offer 包含一個七天免費試訂的優(yōu)惠策略。那么,此 Offer 的價格階段就分別是:
- 用戶先享受七天的免費試訂
- 七天后,用戶再按原價按月付費
假如為這個 Offer 再添加一個 “折扣為七折,為期一個月的周期性付款” 的優(yōu)惠策略,此時 Offer 的價格階段就變成了:
- 用戶先享受七天的免費試訂
- 七天后,用戶按原價的七折進行付費,獲得一個月的訂閱期
- 一個月后,用戶再按原價按月付費
所以說,價格階段就決定了用戶在不同時間段下所需要支出的費用,每個 Offer 最多允許添加兩個價格階段,也即最多發(fā)生三次價格變動,用戶會按順序來接收價格變化
總結(jié)
Google Play 設(shè)定 BasePlan 和 Offer 的自由度很高。自動續(xù)訂的 BasePlan 的付費周期可以從一周到一年,預付費的 BasePlan 的付費周期可以從一天到一年。每種優(yōu)惠策略的優(yōu)惠周期和優(yōu)惠價也都可以很靈活地設(shè)定。我們可以通過設(shè)定多種不同的周期時長和優(yōu)惠策略供用戶選擇,從而盡量提高用戶的付費率
此外,每個訂閱型商品最多可以創(chuàng)建 250 個基礎(chǔ)方案和優(yōu)惠,但同時啟用的基礎(chǔ)方案和優(yōu)惠不能超過 50 個,多出的基礎(chǔ)方案和優(yōu)惠必須處于草稿或未啟用狀態(tài)
四、Billing SDK
了解了以上的基礎(chǔ)概念后,再來看這些概念如何和 Billing SDK 對應(yīng)起來
本文所有的代碼示例使用的均是當前 Google Play 結(jié)算系統(tǒng)在 Android 端最新版本的 SDK,且是協(xié)程版本,讀者需要對協(xié)程有一定了解
dependencies {val billingVersion = "6.0.1"implementation("com.android.billingclient:billing-ktx:$billingVersion")
}
整個支付流程可以總結(jié)為以下幾點:
- 通過 BillingClient 和 Google Play 建立連接,同時綁定用于回調(diào)支付結(jié)果的 PurchasesUpdatedListener 接口
- 通過 BillingClient 查詢到本地化處理的商品信息,也即 ProductDetails,從而拿到 商品描述、基礎(chǔ)方案、價格信息、優(yōu)惠策略 等屬性
- 根據(jù)查到的 ProductDetails,向 BillingClient 發(fā)起支付請求,調(diào)起支付彈窗
- 在 PurchasesUpdatedListener 里拿到支付結(jié)果,判斷用戶的支付狀態(tài)
- 當確定用戶支付成功后,根據(jù)商品類型擇機對商品進行 確認 或 消耗
BillingClient
BillingClient 是 Google Play 結(jié)算庫與 App 進行通信的主接口,App 在執(zhí)行任何與支付相關(guān)的操作之前,都需要先通過 BillingClient 和 Google Play 建立連接。在初始化 BillingClient 實例時,需要同時綁定 PurchasesUpdatedListener,以便得到支付結(jié)果的回調(diào)通知。也正因為如此,App 在同一時間段最多只能保持一個活躍的 BillingClient 連接,以免同一個支付事件同時回調(diào)多個 PurchasesUpdatedListener
private val purchasesUpdatedListener =PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->}private lateinit var billingClient: BillingClientsuspend fun startConnection(context: Context) {billingClient = buildBillingClient(context = context, purchasesUpdatedListener)startConnection(billingClient = mBillingClient)
}private fun buildBillingClient(context: Context,listener: PurchasesUpdatedListener
): BillingClient {return BillingClient.newBuilder(context).setListener(listener).enablePendingPurchases().build()
}private suspend fun startConnection(billingClient: BillingClient): BillingResult? {return withContext(context = Dispatchers.Default) {if (billingClient.isReady) {return@withContext null}return@withContext suspendCancellableCoroutine { continuation ->billingClient.startConnection(object : BillingClientStateListener {override fun onBillingSetupFinished(billingResult: BillingResult) {if (!continuation.isCompleted) {continuation.resume(value = billingResult)}}override fun onBillingServiceDisconnected() {if (!continuation.isCompleted) {continuation.resume(value = null)}}})}}
}
ProductDetails
ProductDetails 也即商品詳情,不管是一次性商品還是訂閱型商品,都通過 ProductDetails 來承載具體的商品信息
查詢 ProductDetails 需要兩個查詢參數(shù):ProductId 和 商品類型,商品類型也即 一次性商品 INAPP 和 訂閱型商品 SUBS 兩種
private suspend fun queryProductDetails() {//查詢一次性商品queryProductDetails(billingClient = mBillingClient,productIdList = setOf("1", "2"),productType = BillingClient.ProductType.INAPP)//查詢訂閱型商品queryProductDetails(billingClient = mBillingClient,productIdList = setOf("1", "2"),productType = BillingClient.ProductType.SUBS)
}private suspend fun queryProductDetails(billingClient: BillingClient,productIdList: Set<String>,productType: String
): List<ProductDetails>? {return withContext(context = Dispatchers.Default) {if (!billingClient.isReady || productIdList.isEmpty()) {return@withContext null}val productDetailParamsList = productIdList.map {QueryProductDetailsParams.Product.newBuilder().setProductId(it).setProductType(productType).build()}val queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productDetailParamsList).build()val productDetailsResult = billingClient.queryProductDetails(queryProductDetailsParams)productDetailsResult.productDetailsList}
}
ProductDetails 的數(shù)據(jù)結(jié)構(gòu)如下所示,我們可以依靠這些信息來向用戶展示商品詳情。oneTimePurchaseOfferDetails 和 subscriptionOfferDetails 這兩個字段就分別用來承載一次性商品和訂閱型商品的價格信息
{"productId": "","productType": "","title": "","name": "","description": "","oneTimePurchaseOfferDetails": {},"subscriptionOfferDetails": []
}
oneTimePurchaseOfferDetails
oneTimePurchaseOfferDetails 對應(yīng)的是一次性商品的詳情,數(shù)據(jù)結(jié)構(gòu)比較簡單,主要就是價格信息了
{"priceAmountMicros": 548000000,"priceCurrencyCode": "HKD","formattedPrice": "HK$548.00"
}
需要注意,Google Play 返回的價格信息都是做了本地化處理的,會自動根據(jù)當前設(shè)備的 Google Play 賬號所對應(yīng)的國家地區(qū)來返回詳情,所以商品的價格貨幣代號 priceCurrencyCode
和格式化好的商品價格 formattedPrice
都會因?qū)嶋H情況而變化
subscriptionOfferDetails
subscriptionOfferDetails 對應(yīng)的是訂閱型商品的詳情
由于訂閱型商品是可以包含多個 BasePlan 的,每個 BasePlan 又可以包含多個 Offer,所以 subscriptionOfferDetails 字段在 ProductDetails 中對應(yīng)的數(shù)據(jù)類型是 List<SubscriptionOfferDetails>
。每個 SubscriptionOfferDetails 都對應(yīng)一個 Offer,每個 Offer 又關(guān)聯(lián)一個 BasePlan,Google Play 以 Offer 為單位來返回價格信息
[{"basePlanId": "yearly","offerId": null,"offerToken": "xxx","pricingPhases": {"pricingPhaseList": [{"formattedPrice": "HK$469.00","priceAmountMicros": 469000000,"priceCurrencyCode": "HKD","billingPeriod": "P1Y","billingCycleCount": 0,"recurrenceMode": 1}]}},{"basePlanId": "yearly","offerId": "xxx","offerToken": "xxx","pricingPhases": {"pricingPhaseList": [{"formattedPrice": "免費","priceAmountMicros": 0,"priceCurrencyCode": "HKD","billingPeriod": "P1W","billingCycleCount": 1,"recurrenceMode": 2},{"formattedPrice": "HK$469.00","priceAmountMicros": 469000000,"priceCurrencyCode": "HKD","billingPeriod": "P1Y","billingCycleCount": 0,"recurrenceMode": 1}]}}
]
上文有講到,Offer 是包含價格階段 PricingPhases 這個概念的,這個概念就體現(xiàn)在以上 Json 中,當中就可以解讀出以下商品信息:
- 該商品包含一個 Id 為 yearly 的 basePlan,一共包含兩個 Offer
- offerToken 用于唯一標識每一個 Offer,具有唯一性
- billingPeriod 用于表示計費周期,以 ISO 8601 格式來指定。例如,P1W 表示一周,P1Y 表示一年,P1M3D 表示一個月加三天
- billingCycleCount 用于表示計費周期的周期數(shù)。例如,以上的第二個 Offer 的第一個 PricingPhases,就表示允許用戶免費試用一周;假如 billingCycleCount 是 2,就表示允許用戶免費試用兩周
- recurrenceMode 用于表示價格階段的重復模式,當值為 1 或 3 時,billingCycleCount 值都會是 0
- 值為 1 就表示將在無限的計費周期內(nèi)重復進行,除非用戶主動取消
- 值為 2 就表示將在 billingCycleCount 指定的周期內(nèi)重復扣費
- 值為 3 表示是一次性收費,不會重復
- 第一個 Offer 的 offerId 為 null,說明此 Offer 不包含實際的優(yōu)惠策略,代表的其實是 BasePlan 的原價,所以 pricingPhaseList 也會只有一個值。且由于 billingPeriod 是 P1Y,說明關(guān)聯(lián)的 BasePlan 的付費周期是一年。選中此 Offer 后用戶就需要直接付 HK$469.00 的原價來進行訂閱
- 第二個 Offer 的 offerId 不為 null,說明此 Offer 包含真實的優(yōu)惠策略,所以 pricingPhaseList 的大小就會大于一。該 Offer 允許用戶先免費試用一周,然后再和第一個 Offer 同樣的價格和周期來進行訂閱
所以說,想要解讀出 BasePlan 的定價策略和 Offer 的優(yōu)惠策略,就需要結(jié)合所有字段來進行解析。首先,不管我們在創(chuàng)建 BasePlan 時有沒有為其指定優(yōu)惠策略,Google Play 都會將 BasePlan 的原價視為一個 Offer 并返回,這種情況下 Offer 也只會有一個定價階段。而對于真實的優(yōu)惠策略,其 offerId 是必須設(shè)定的,自然也就不會為 null,也會有最多三個定價階段。我們要區(qū)分出 “虛假的” Offer 和 "真實的” Offer。然后,再通過 pricingPhases 來解析出 BasePlan 的訂閱周期和價格、Offer 的優(yōu)惠策略、Offer 的價格階段具體是如何設(shè)定的。這樣我們才能向用戶完整展示整個商品的價格信息
launchBillingFlow
launchBillingFlow 用于調(diào)起支付彈窗發(fā)起支付操作,根據(jù)商品類型,其調(diào)用方式分為兩種
假如要購買的是一次性商品,支付參數(shù)僅需要 ProductDetails 即可
private suspend fun launchBilling(activity: Activity,billingClient: BillingClient,productDetails: ProductDetails
): BillingResult {return withContext(context = Dispatchers.Main.immediate) {val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetails).build()val billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(listOf(productDetailsParams)).build()billingClient.launchBillingFlow(activity, billingFlowParams)}
}
假如要購買的是訂閱型商品,則需要同時傳遞 ProductDetails 和 offerToken
由于一個訂閱型商品可能同時包含多個 BasePlan 和多個 Offer,每個 Offer 的優(yōu)惠策略又各不相同。因此 App 在發(fā)起支付操作時,就需要通過 offerToken 來標明用戶想要購買的到底是哪個 BasePlan,選中的又是哪個 Offer。而由于 Google Play 也會將 BasePlan 的原價視為一個 Offer 并返回,所以我們是可以自主選擇要不要讓用戶享用優(yōu)惠的,自由度還是比較高的
private suspend fun launchBilling(activity: Activity,billingClient: BillingClient,productDetails: ProductDetails,offerToken: String
): BillingResult {return withContext(context = Dispatchers.Main.immediate) {val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetails).setOfferToken(offerToken).build()val billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(listOf(productDetailsParams)).build()billingClient.launchBillingFlow(activity, billingFlowParams)}
}
之后,我們在 PurchasesUpdatedListener 回調(diào)里來獲取用戶的支付結(jié)果
假如用戶已支付成功,Purchase 就包含了此筆訂單的具體信息,包括 ProductId、OrderId、Quantity、PurchaseTime 等
private val purchasesUpdatedListener =PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->when (billingResult.responseCode) {BillingClient.BillingResponseCode.OK -> {if (!purchases.isNullOrEmpty()) {purchases.forEach {when (it.purchaseState) {Purchase.PurchaseState.PURCHASED -> {//用戶支付成功}Purchase.PurchaseState.PENDING -> {//用戶僅是預創(chuàng)建了訂單,還未真正付款}Purchase.PurchaseState.UNSPECIFIED_STATE -> {//未知}}}}}BillingClient.BillingResponseCode.USER_CANCELED -> {//用戶取消支付}else -> {}}}
acknowledgePurchase
用戶支付成功后,就需要對訂單進行確認了,否則 Google Play 會在限定時間內(nèi)退款給用戶
private suspend fun acknowledgePurchase(billingClient: BillingClient,purchase: Purchase
): Boolean {return withContext(context = Dispatchers.Default) {if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {return@withContext false}if (purchase.isAcknowledged) {return@withContext true}if (!billingClient.isReady) {return@withContext false}val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()val acknowledgePurchase = billingClient.acknowledgePurchase(acknowledgePurchaseParams)acknowledgePurchase.responseCode == BillingClient.BillingResponseCode.OK}
}
consumePurchase
如果用戶購買的是消耗型的一次性商品,那么就需要根據(jù)實際業(yè)務(wù)擇機對訂單執(zhí)行消耗操作了
private suspend fun consumePurchase(billingClient: BillingClient,purchase: Purchase
): Boolean {return withContext(context = Dispatchers.Default) {if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {return@withContext false}if (!billingClient.isReady) {return@withContext false}val consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()val consumeResult = billingClient.consumePurchase(consumeParams)consumeResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK}
}
五、鑒權(quán)
當用戶購買商品后,就需要來考慮如何對用戶進行鑒權(quán)了。如果鑒權(quán)失敗或者是鑒權(quán)錯了,不僅會給用戶帶來不良體驗,引來用戶投訴,也有可能會給項目帶來不可估量的資金損失
按照一般情況,App 在供用戶使用時,App 都會為當前用戶創(chuàng)建一個自己賬戶體系下的用戶身份,我們可以稱之為 appUser。當用戶購買商品后,這筆訂單也會和當前設(shè)備付款的 Google Play 賬號綁定在一起,我們可以稱之為 gpUser
如此一來,這筆訂單就會和兩個不同角度下的用戶產(chǎn)生關(guān)聯(lián)。這也就連鎖帶來一個問題:商品代表的權(quán)益應(yīng)該掛載在哪個用戶的名下?appUser 還是 gpUser ?
這兩個選擇都各有優(yōu)缺點
掛載在 appUser 名下:
- 優(yōu)點:用戶權(quán)益清晰明確,可以精準隔離用戶的權(quán)益狀態(tài)
- 缺點:在國外,以游客身份來購買虛擬商品是很常見的情況,假如 App 只允許正式用戶(綁定了郵箱或者電話號碼)才能購買商品的話,很有可能會流失大部分的潛在付費用戶。因此,如果 appUser 是游客的話,當用戶卸載應(yīng)用、更換或者重置設(shè)備后,就有可能導致已付費的用戶再也找不回這筆訂單了
掛載在 gpUser 名下:
- 優(yōu)點:即使用戶卸載應(yīng)用、更換或者重置設(shè)備,只要當前設(shè)備登錄的就是付款時的 Google Play 賬號,App 都能通過 Billing SDK 的
queryPurchasesAsync
方法重新找回該賬號名下所有的訂單信息,不用擔心出現(xiàn)權(quán)益丟失的情況。同個 Google Play 賬號在不同設(shè)備上也能共同享有 App 的權(quán)益,用戶體驗是最好的 - 缺點:App 是無法拿到 gpUser 的唯一身份標識的,容易出現(xiàn)賬號倒賣的情況,多個用戶通過共享同一個 Google Play 賬號來一起享有同一筆訂單的權(quán)益
所以說,App 需要根據(jù)自己的業(yè)務(wù)類型和用戶屬性,來決定是否要允許游客也能進行購買操作,用戶應(yīng)該以哪種維度來進行身份鑒權(quán),當發(fā)現(xiàn)同筆訂單在多臺設(shè)備上生效時,又應(yīng)該如何避免資產(chǎn)損失
六、最后
本文主要是以移動端的角度來進行闡述,雖然 Google Play 結(jié)算系統(tǒng)也允許在沒有 App 后端服務(wù)參與的情況下就直接完成整個支付流程并完成用戶鑒權(quán),但為了安全性考慮,最好還是需要將訂單信息同步保存到服務(wù)端,并由服務(wù)端對訂單進行校驗后再決定是否要下發(fā)權(quán)益。此外,用戶是可以在不經(jīng)過 App 的情況下,直接從 Google Play 中取消訂閱或者恢復訂閱,App 無法實時獲知該筆訂單的狀態(tài)變化,此時 Google Play 也只會通過 開發(fā)者實時通知 將這種變化通知給服務(wù)端,這種情況下也需要服務(wù)端的參與才能完整記錄下用戶的整個付費狀態(tài)變化
Android 學習筆錄
Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學習筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內(nèi)含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開發(fā)崗位面試習題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap