出海支付實(shí)戰(zhàn):Google Play與Apple Store接入避坑指南
1. 引言
今天我們來(lái)聊聊錢(qián),一個(gè)項(xiàng)目想要掙錢(qián)就離不開(kāi)商業(yè)化,說(shuō)白了就是讓用戶(hù)掏錢(qián),所以,支付系統(tǒng)也是我們?cè)陧?xiàng)目開(kāi)發(fā)中必不可少的一個(gè)關(guān)鍵模塊。
而在 APP 出海的征途上,海外支付堪稱(chēng)"必渡之河"。
根據(jù) Statista 2024 數(shù)據(jù),Google Play 與 Apple Store 合計(jì)占據(jù)全球移動(dòng)應(yīng)用市場(chǎng) 83% 的營(yíng)收份額,其支付系統(tǒng)(IAP)如同支付寶與微信支付在國(guó)內(nèi)的地位。
本文聚焦于兩大主流平臺(tái)的接入實(shí)踐,解析跨境支付的特殊場(chǎng)景與解決方案,以及業(yè)務(wù)上可能遇到的一些坑。
2. 業(yè)務(wù)交互流程
支付系統(tǒng)的核心在于狀態(tài)機(jī)的精準(zhǔn)運(yùn)轉(zhuǎn),下圖展示典型交互鏈路:
圖片
關(guān)鍵節(jié)點(diǎn)三要素:
- 商品預(yù)配:提前在支付平臺(tái)創(chuàng)建商品ID并同步至業(yè)務(wù)數(shù)據(jù)庫(kù)(建議通過(guò) Admin API 自動(dòng)化)
- 支付憑證雙驗(yàn)證:客戶(hù)端獲取支付令牌后,需業(yè)務(wù)后臺(tái)二次驗(yàn)證(蘋(píng)果建議使用 AppStoreServerAPI)
- 原始交易ID綁定:訂閱類(lèi)訂單需持久化 original_transaction_id 作為續(xù)期憑證
由于業(yè)務(wù)需要不同的充值套餐和訂閱周期,所有我們還需要先提前在 Apple Store 及 Google Play 中配置商品,并且給商品創(chuàng)建一個(gè)唯一 ID,存儲(chǔ)在后臺(tái)數(shù)據(jù)庫(kù)中(這一步我們可以在運(yùn)營(yíng)后臺(tái)里調(diào)用 Apple Store 或者谷歌 Play 的接口,同時(shí)寫(xiě)入到 DB)。
當(dāng)用戶(hù)想要獲取商品時(shí),客戶(hù)端首先到后臺(tái)拉取商品列表,然后用戶(hù)點(diǎn)擊充值/訂閱后,客戶(hù)端就帶著商品 ID 請(qǐng)求 Google Play/Apple Store 同時(shí)拉起支付頁(yè)面,當(dāng)用戶(hù)支付完成后,客戶(hù)端把產(chǎn)品ID、支付 Token、交易 ID 等信息傳入后臺(tái)并創(chuàng)建訂單。
需注意的是,原始交易 ID 可以管理訂閱訂單中的續(xù)訂狀態(tài),后臺(tái)需要存儲(chǔ)起來(lái)。
接著,業(yè)務(wù)后臺(tái)請(qǐng)求支付網(wǎng)關(guān)接口,來(lái)驗(yàn)證訂單的有效性,確認(rèn)無(wú)誤后開(kāi)始下發(fā)用戶(hù)權(quán)益(如:充值后獲取 100 顆鉆石進(jìn)行主播打賞,或者成為尊貴的 VIP 用戶(hù))。
3. 海外支付的要點(diǎn)及難點(diǎn)
一些背景
Google 和 Apple 未提供完整的流程和狀態(tài)支持文檔,中文資料亦不完整,因此我們以官方文檔為準(zhǔn),采用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)。
同時(shí),支付平臺(tái)服務(wù)端的接口支持較落后,我們盡量選用 SDK 進(jìn)行交互。由于 Google Play 和 Apple Store 的回調(diào)通知類(lèi)型不一致,支付狀態(tài)設(shè)計(jì)成為難點(diǎn)。
支付狀態(tài)流轉(zhuǎn)
比如,Apple 訂閱的通知類(lèi)型【NOTIFICATION_TYPE】分為以下幾種:
圖片
Google 的通知類(lèi)型包括:
● 續(xù)訂開(kāi)始/續(xù)訂恢復(fù)
● 訂閱取消/續(xù)訂取消
● 訂閱下單
● 訂閱保留(相當(dāng)于停機(jī)保號(hào))
● 訂閱重新開(kāi)始
● 訂閱到期
結(jié)合實(shí)際業(yè)務(wù),我們選用了都有的其中幾種支付狀態(tài),如:完成訂單、取消、續(xù)訂失敗、退訂、續(xù)訂成功等。
一次性商品的購(gòu)買(mǎi)狀態(tài)扭轉(zhuǎn)比較簡(jiǎn)單:
圖片
訂閱類(lèi)商品更復(fù)雜一些:
圖片
如上圖所示,在訂閱類(lèi)支付時(shí)需要考慮用戶(hù)暫停/取消訂閱,也需要根據(jù)業(yè)務(wù)需要讓用戶(hù)自己覺(jué)得是否自動(dòng)續(xù)訂,自動(dòng)續(xù)訂時(shí)可能還要考慮用戶(hù)的賬戶(hù)余額是否充足等場(chǎng)景。
表結(jié)構(gòu)
商品表和訂單表記錄了業(yè)務(wù)中的商品列表和用戶(hù)購(gòu)買(mǎi)狀態(tài)。商品表需提前在三方支付平臺(tái)上配置,訂單表記錄用戶(hù)購(gòu)買(mǎi)的商品狀態(tài)和三方支付平臺(tái)的交易憑據(jù)。
商品表
以下是商品表的主要結(jié)構(gòu)(采用 Go 語(yǔ)言的結(jié)構(gòu)體形式展現(xiàn)):
type Product struct {
ID uint `gorm:"primaryKey NOT NULL AUTO_INCREMENT"`
ProductId string `json:"product_id" gorm:"index:product_id_app_id_deleted_at;type:varchar(64) DEFAULT ''"`
ProductType int `json:"product_type" gorm:"comment:商品類(lèi)型,0.未知類(lèi)型/1.消耗型購(gòu)買(mǎi)/2.非消耗型購(gòu)買(mǎi)/3.自動(dòng)訂閱/4.非自動(dòng)訂閱;type:tinyint(4) DEFAULT 0"`
Name string `json:"name" gorm:"comment:商品名稱(chēng);type:varchar(64) DEFAULT ''"`
Description string `json:"description" gorm:"comment:商品描述;type:varchar(1024) DEFAULT ''"`
Price int `json:"price" gorm:"comment:商品價(jià)格,精度2位小數(shù),用100倍存儲(chǔ);type:bigint DEFAULT 0"`
TokenType int `json:"token_type" gorm:"comment:虛擬幣類(lèi)型,0.鉆石;1.金幣;2.元寶;3.其它;type:tinyint(4) DEFAULT 0"`
TokenQuantity int `json:"token_quantity" gorm:"comment:虛擬幣數(shù)量;type:int(11) DEFAULT 0"`
SubscribeDurationDay int `json:"subscribe_duration_day" gorm:"comment:會(huì)員訂閱時(shí)長(zhǎng)(天);type:int(11) DEFAULT 0"`
Weight int `json:"weight" gorm:"comment:權(quán)重,排序時(shí)從大到小,客戶(hù)端根據(jù)此字段進(jìn)行商品排序;type:int(11) DEFAULT 0"`
ImageURL string `json:"image_url" gorm:"comment:商品圖片URL;type:varchar(1024) DEFAULT ''"`
IsAutoSubscribe int `json:"is_auto_subscribe" gorm:"comment:是否是自動(dòng)續(xù)費(fèi),0.未知/1.自動(dòng)續(xù)費(fèi)/2.非自動(dòng)續(xù)費(fèi);type:tinyint(2) DEFAULT 0"`
Platform int `json:"platform" gorm:"comment:支付平臺(tái), 0-海外,1-微信支付,2-國(guó)內(nèi)支付渠道;type:tinyint(2) DEFAULT 0"`
}
商品表主要是記錄業(yè)務(wù)中的商品列表,需要提前在三方支付平臺(tái)上進(jìn)行配置,下單時(shí)客戶(hù)端需帶著商品 ID 請(qǐng)求后臺(tái)服務(wù)器。
訂單表
訂單表的定義如下所示(采用 Go 語(yǔ)言的結(jié)構(gòu)體形式展現(xiàn)):
type Order struct {
ID uint `gorm:"primaryKey NOT NULL AUTO_INCREMENT"`
OrderId string `json:"order_id" gorm:"index:order_id_deleted_at;type:varchar(64) DEFAULT ''"`
ProductId string `json:"product_id" gorm:"index:user_uuid_app_product_id_deleted_at;type:varchar(64) DEFAULT ''"`
TransactionId string `json:"transaction_id" gorm:"comment:交易ID,訂閱/續(xù)訂時(shí)使用;type:varchar(64) DEFAULT ''"`
OriginalTransactionId string `json:"original_transaction_id" gorm:"comment:原始交易ID,訂閱/續(xù)訂時(shí)該ID一致;type:varchar(64) DEFAULT ''"`
UserUuid string `json:"user_uuid" gorm:"index:user_uuid_app_product_id_deleted_at;comment:用戶(hù)的UUID;type:varchar(64) DEFAULT ''"`
PayChannel int `json:"pay_channel" gorm:"comment:支付渠道,0/1/2,GooglePay/ApplePay/PayPal;type:tinyint DEFAULT 0"`
PaymentState int `json:"payment_state" gorm:"comment:支付狀態(tài),-1:處理中 0:初始化 1:已完成 2 取消 3 續(xù)訂失敗 4 退款 5 續(xù)訂成功;type:tinyint DEFAULT 0"`
RefundTime string `json:"refund_time" gorm:"comment:退款時(shí)間;type:varchar(32) DEFAULT ''"`
}
訂單主要記錄用戶(hù)購(gòu)買(mǎi)的商品狀態(tài),比如權(quán)益下發(fā)到哪一步了,續(xù)訂套餐是否在續(xù)費(fèi)狀態(tài),以及三方支付平臺(tái)的一些交易憑據(jù)(如交易ID)等。
5. 常見(jiàn)問(wèn)題
1)如何防止掉單
在支付系統(tǒng)中,最重要的是用戶(hù)權(quán)益。很多時(shí)候用戶(hù)明明已經(jīng)下單并且付錢(qián)了,但是 VIP 沒(méi)有充上,鉆石沒(méi)有到賬,是用戶(hù)無(wú)法接受的。
一個(gè)很常見(jiàn)的 Case 是:用戶(hù)付錢(qián)后,斷開(kāi)網(wǎng)絡(luò)連接,這時(shí)后臺(tái)系統(tǒng)沒(méi)有收到消息,該怎么處理?
這里我們做了兩步來(lái)保證:
- 客戶(hù)端補(bǔ)償策略:采用本地事務(wù)日志+斷點(diǎn)續(xù)傳設(shè)計(jì)。客戶(hù)端在劃賬請(qǐng)求的 ACK 之前先調(diào)用后臺(tái)接口生成訂單,如果用戶(hù)在支付后突然斷網(wǎng),重新打開(kāi)客戶(hù)端后會(huì)檢查當(dāng)前是否存在未確認(rèn)的劃賬請(qǐng)求,如果有就再調(diào)用一次后臺(tái)訂單再 ACK。同時(shí)后臺(tái)通過(guò)冪等性來(lái)保證用戶(hù)不會(huì)多次支付同一筆訂單;
- 業(yè)務(wù)系統(tǒng)雙保險(xiǎn)告警處理:業(yè)務(wù)平臺(tái)接收到支付網(wǎng)關(guān)回調(diào)時(shí),發(fā)現(xiàn)已有訂單就更新訂單狀態(tài);沒(méi)有訂單就發(fā)告警,進(jìn)行人工處理;
2)如何保證賬單和訂單正確性
在傳統(tǒng)的公司交易中,都會(huì)需要會(huì)計(jì)來(lái)對(duì)賬,將每月的賬單和收支金額總額對(duì)比,防止出現(xiàn)賬不對(duì)錢(qián)的壞賬。
所以,一方面為了保證訂單的有序性,我們?cè)跇I(yè)務(wù)系統(tǒng)禁止隨意扭轉(zhuǎn)訂單狀態(tài);另一方面我們?cè)谥Ц毒W(wǎng)關(guān)進(jìn)行每天的定時(shí)對(duì)賬:
- 狀態(tài)機(jī)檢查裝置:每次觸發(fā)業(yè)務(wù)回調(diào)時(shí),業(yè)務(wù)后臺(tái)都會(huì)判斷數(shù)據(jù)庫(kù)狀態(tài)和支付平臺(tái)的后臺(tái)狀態(tài)一致性,若是不一致,則判斷狀態(tài)是否可以扭轉(zhuǎn),若是不能扭轉(zhuǎn)則告警出來(lái);若是可以扭轉(zhuǎn)則更新 DB 里訂單的狀態(tài);
- 服務(wù)端哨兵系統(tǒng):每小時(shí)掃描未完結(jié)訂單與支付平臺(tái)對(duì)賬。支付網(wǎng)關(guān)每天定時(shí)比較昨日數(shù)據(jù)庫(kù)和支付平臺(tái)后臺(tái)的交易狀態(tài)差異,有差錯(cuò)的部分進(jìn)行告警。
6. 小結(jié)
海外支付的接入涉及復(fù)雜的流程和細(xì)致的狀態(tài)管理,通過(guò)合理的系統(tǒng)設(shè)計(jì)和流程優(yōu)化,可以有效解決掉單和賬單對(duì)賬的問(wèn)題,確保用戶(hù)權(quán)益得到保障。