-
非常感謝簡書用戶Ray206 借鑒文章Google支付和服務(wù)端驗(yàn)證
-
非常感謝CSDN用戶kamiy_rong 借鑒文章PHP谷歌支付[APP]
-
非常感謝CSDN用戶L竹軒沐雨 借鑒文章Google支付從創(chuàng)建商品到支付成功踩過的坑
google支付分為訂閱和應(yīng)用內(nèi)購買兩種侣诵,這里使用的是應(yīng)用內(nèi)購買這種方式,這里將整個(gè)google支付和支付驗(yàn)證的流程記錄下來。
注意項(xiàng):
前端需要商品ID調(diào)起支付
后端需要獲取token歇父,使用token去Google三方鏈接驗(yàn)證訂單
前端 導(dǎo)入google結(jié)算庫
def billing_version = "4.0.0"
implementation "com.android.billingclient:billing-ktx:$billing_version"
接入支付
流程:
- 初始化鏈接到google支付服務(wù)河狐,如果不能鏈接到說明設(shè)備環(huán)境有問題库说,要么是沒有翻墻,要么是google套件(google paly 躏结、server)沒有安裝完整,國內(nèi)手機(jī)都是閹割過的狰域,所以需要重新安裝google套件
- 查詢上次未消費(fèi)的商品媳拴,如果有未消費(fèi)的商品通知服務(wù)器,然后消費(fèi)掉兆览。因?yàn)閲獾闹Ц董h(huán)境和國內(nèi)不一樣屈溉,他們可以線上下單,然后到便利店去支付抬探,所以有未消費(fèi)的這種情況子巾。
這時(shí)google支付的準(zhǔn)備工作已完成,下面就可以發(fā)起支付了 - 使用google后臺(tái)配置商品id進(jìn)行支付
- 支付完成后通知服務(wù)器驗(yàn)證訂單合法性并發(fā)貨
- 客戶端消費(fèi)商品
下面咋們上代碼
step1
初始化并連接到google服務(wù)
// init方法
public synchronized void init(Activity mActivity){
//創(chuàng)建BillingClient 對(duì)面小压,查詢 消費(fèi) 支付都會(huì)使用這個(gè)對(duì)象
this.mBillingClient = BillingClient.newBuilder(mActivity)
.setListener(new PurchasesUpdatedListener() {//設(shè)置支付回調(diào)线梗,這里其實(shí)是商品狀態(tài)發(fā)生變化時(shí)就會(huì)回調(diào)
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
LogUtils.d("call onPurchasesUpdated");
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {//支付成功
for (Purchase purchase : purchases) {
if(purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) continue;
OrderManager.getInstance().paySuccess(purchase);
//通知服務(wù)器支付成功,服務(wù)端驗(yàn)證后怠益,消費(fèi)商品
}
//TODO客戶端同步回調(diào)支付成功
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {//支付取消
} else {//支付失敗
}
}
})
.enablePendingPurchases()
.build();
//鏈接到google play
this.connectBillPay();
}
private void connectBillPay(){
mBillingClient.startConnection(new BillConnectListener());
}
class BillConnectListener implements BillingClientStateListener {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//鏈接到google服務(wù)
payEnable = true;
queryPurchases();
}
}
@Override
public void onBillingServiceDisconnected() {
//未鏈接到google服務(wù)
payEnable = false;
connectBillPay();
}
}
setp2
查詢已支付的商品仪搔,并通知服務(wù)器后消費(fèi)(google的支付里面,沒有消費(fèi)的商品蜻牢,不能再次購買)
private void queryPurchases(){
PurchasesResponseListener mPurchasesResponseListener = new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> purchasesResult) {
if(billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchasesResult == null) return;
for (Purchase purchase : purchasesResult) {
if(purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) continue;
OrderManager.getInstance().paySuccess(purchase);
//這里處理已經(jīng)支付過的訂單烤咧,通知服務(wù)器去驗(yàn)證
}
}
};
mBillingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, mPurchasesResponseListener);
}
setp3
發(fā)起支付
/**
*
* @param cpOrder 你自己的訂單號(hào)或者用戶id,用于關(guān)聯(lián)到對(duì)應(yīng)的用戶抢呆,發(fā)放道具時(shí)使用
* @param productId google后臺(tái)配置產(chǎn)品ID
*/
public void pay(final String cpOrder, final String productId) {
if(mBillingClient == null || wrActivity.get() == null || !payEnable){
//TODO客戶端同步回調(diào)支付失敗煮嫌,原因是為鏈接到google或者google的支付服務(wù)不能使用
return;
}
//查詢商品詳情
querySkuDetailsAsync(cpOrder, productId);
}
//查詢商品詳情
void querySkuDetailsAsync(final String cpOrder, final String productId){
List<String> skuList = new ArrayList<>();
skuList.add(productId);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
if (skuDetailsList != null && billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
for(SkuDetails skuDetails : skuDetailsList){
if(productId.equals(skuDetails.getSku())){
//發(fā)起支付
launchBillingFlow(cpOrder, skuDetails);
}
}
}
}
});
}
//吊起google支付頁面
void launchBillingFlow(String cpOrder, SkuDetails skuDetails){
mBillingClient.launchBillingFlow(
wrActivity.get(),
BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.setObfuscatedAccountId(cpOrder)//這里本來的意思存放用戶信息,類似于國內(nèi)的透傳參數(shù)镀娶,我這里傳的我們的訂單號(hào)立膛。老版本使用DeveloperPayload字段,最新版本中這個(gè)字段已不可用了
.build()
);
}
服務(wù)器支付驗(yàn)證操作較為復(fù)雜,咋們?cè)谙旅鎲为?dú)提出來做一個(gè)小節(jié)
setp5
消費(fèi)商品
public void consumePurchase(final Purchase purchase){
if(mBillingClient == null || purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) return;
LogUtils.d("消耗商品:\n商品id:" + purchase.getSkus() + "\n商品OrderId:" + purchase.getOrderId() + "\ntoken:" + purchase.getPurchaseToken());
LogUtils.d("消耗商品:" + purchase.getAccountIdentifiers().getObfuscatedAccountId());
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
//消費(fèi)失敗將商品重新放入消費(fèi)隊(duì)列
OrderManager.getInstance().consumeFinal(purchase);
return;
}
LogUtils.d("消費(fèi)成功");
}
};
mBillingClient.consumeAsync(consumeParams, listener);
}
服務(wù)端驗(yàn)證
做服務(wù)端驗(yàn)證前宝泵,需要做一下準(zhǔn)備工作
大致流程:
- 創(chuàng)建api項(xiàng)目這個(gè)和登錄用的項(xiàng)目不是同一個(gè)
- 開啟Google Play Android Developer API
- 設(shè)置oauth同意屏幕(就是拉起開發(fā)者授權(quán)賬號(hào)登錄時(shí)的登錄頁面)
- 創(chuàng)建web應(yīng)用的oauth客戶端ID
- 拉起授權(quán)頁面好啰,使用google開發(fā)者賬號(hào)給項(xiàng)目授權(quán),得到code
- 創(chuàng)建谷歌賬號(hào)儿奶,并開通開發(fā)者權(quán)限框往,需要支付25美元。
- google play開發(fā)者后臺(tái)闯捎,API權(quán)限菜單中關(guān)聯(lián)剛剛創(chuàng)建的項(xiàng)目椰弊,一個(gè)google play賬號(hào)只需要也只能關(guān)聯(lián)一個(gè)api項(xiàng)目就行了,這個(gè)項(xiàng)目可以查詢關(guān)聯(lián)賬號(hào)中的所有應(yīng)用的訂單
- 通過code瓤鼻,拿到refreshToken秉版,這個(gè)token只有第一次才會(huì)返回需要永久儲(chǔ)存(這個(gè)refreshtoken很重要,需要保存下來)茬祷,如果弄丟清焕,只有重新創(chuàng)建一個(gè)oauth客戶端ID,然后重復(fù)步驟5祭犯,拿到新的refreshtoken(如果確認(rèn)信息沒有填寫錯(cuò)誤秸妥,仍舊提示clinet錯(cuò)誤,可以新創(chuàng)建一個(gè)客戶端ID沃粗,重復(fù)步驟5)
- 創(chuàng)建付款資料 完善收款信息
- 查詢訂單狀態(tài)前粥惧,需要先去申請(qǐng)產(chǎn)品ID和支付金額,支付是根據(jù)產(chǎn)品ID進(jìn)行支付 最盅。在此之前突雪,需要先創(chuàng)建付款資料
- 刷新refreshToken, 得到accessToken檩禾,通過accesstoken就可以去查詢訂單狀態(tài)了
- 回調(diào)查詢訂單狀態(tài)
上操作截圖
setp1
創(chuàng)建api項(xiàng)目google api console
setp2
開啟Google Play Android Developer API
搜索“Google Play Android Developer API”
開啟“Google Play Android Developer API”
setp3
開啟同意屏幕
這里填上必填項(xiàng)就行了挂签,這個(gè)授權(quán)同意屏幕,請(qǐng)求code時(shí)拉起來給咋們開發(fā)人員開的盼产,填啥都無所謂
setp4
創(chuàng)建oauth2客戶端id
創(chuàng)建頁面和創(chuàng)建成功后的修改頁面可以獲取到clientId和clientSecret
到這里api項(xiàng)目就已經(jīng)創(chuàng)建好了
setp5
獲取code
地址:https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri={填寫的重定向地址}&client_id={創(chuàng)建的clientId}
將上面的{XX}替換成創(chuàng)建api項(xiàng)目時(shí)填寫的重定向地址饵婆,和clientId,然后將連接放到瀏覽器中打開戏售,就會(huì)吊起授權(quán)界面侨核,使用你的開發(fā)者賬號(hào)授權(quán)登錄
請(qǐng)求方式:瀏覽器中打開
這里可以看到,重定向地址上有兩個(gè)參數(shù)code和scope灌灾,我們只需要code就行了搓译,這里的code是urlencode后的,使用時(shí)需要decode
setp6
創(chuàng)建谷歌賬號(hào)锋喜,并開通開發(fā)者權(quán)限(https://play.google.com/console/signup)些己,需要支付25美元豌鸡。
setp7
google play后臺(tái)關(guān)聯(lián)api項(xiàng)目
setp8
使用code換取refreshToken
地址:https://accounts.google.com/o/oauth2/token
請(qǐng)求方式:post
參數(shù):grant_type=authorization_code
code=獲取到的code(需要看看code中是否有%號(hào),如果有需要urldecode)
client_id=創(chuàng)建api項(xiàng)目是的clientId(客戶端ID)
client_secret=創(chuàng)建api項(xiàng)目時(shí)的clientSecret(客戶端密鑰)
redirect_uri=創(chuàng)建api項(xiàng)目時(shí)的重定向地址
這里就獲取到refreshToken了段标,重點(diǎn)重點(diǎn)重點(diǎn)涯冠,refreshToken保存下來,它只會(huì)在第一次請(qǐng)求中返回逼庞,后續(xù)用在發(fā)一樣的請(qǐng)求不會(huì)返回refreshtoken蛇更,如果不慎弄丟了,需要去重新創(chuàng)建一個(gè)WebClientId
setp9
進(jìn)入gooleplay管理后臺(tái) 創(chuàng)建付款資料 完善收款信息
setp10
setp11
使用refreshToken獲取accessToken
地址:https://accounts.google.com/o/oauth2/token
請(qǐng)求方式:post
參數(shù):grant_type=refresh_token
refresh_token=剛剛獲取到的refreshToken
client_id=創(chuàng)建api項(xiàng)目是的clientId(客戶端ID)
client_secret=創(chuàng)建api項(xiàng)目時(shí)的clientSecret(客戶端密鑰)
setp12
查詢訂單狀態(tài)
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}?access_token={access_token}
packageName:app包名赛糟,必須是創(chuàng)建登錄api項(xiàng)目時(shí)派任,創(chuàng)建android客戶端Id使用包名
productId:對(duì)應(yīng)購買商品的商品ID
token:購買成功后Purchase對(duì)象的getPurchaseToken()
access_token:上面咋們獲取到的accessToken
請(qǐng)求方式:get
返回值:
{
"purchaseTimeMillis": "1623980699933",//購買產(chǎn)品的時(shí)間,自紀(jì)元(1970 年 1 月 1 日)以來的毫秒數(shù)璧南。
"purchaseState": 0,//訂單的購買狀態(tài)掌逛。可能的值為:0\. 已購買 1\. 已取消 2\. 待定
"consumptionState": 0,//產(chǎn)品的消費(fèi)狀態(tài)穆咐〔鳎可能的值為: 0\. 尚未消耗 1\. 已消耗
"developerPayload": "",
"orderId": "GPA.3398-6726-1036-80298",//google訂單號(hào)
"purchaseType": 0,
"acknowledgementState": 0,
"kind": "androidpublisher#productPurchase",
"obfuscatedExternalAccountId": "SDK2106180944530041",//上面客戶支付時(shí)的透傳字段字旭,google指導(dǎo)是用來存放用戶信息的对湃,不能過長,否則客戶端不能支付
"obfuscatedExternalProfileId": "",
"regionCode": "HK"
}
下面附上代碼服務(wù)端獲取accessToken和驗(yàn)證訂單代碼
<?php
namespace app\api\controller\v1;
use app\api\controller\Base;
use service\ApiReturn;
/**
* 支付簽名接口
* Class UserLabel
* @package app\api\controller\v1
*/
class Googlepay extends Base
{
protected $refreshToken = '';
protected $client_id = '';
protected $client_secret = '';
/**
* 驗(yàn)證google內(nèi)購訂單
* @param $parsedJson 來自客戶端遗淳,一個(gè)訂單數(shù)據(jù)的JSON字符串
* @return array
*/
public function google($post)
{
$parsedJson = $post['data'];
if(empty($parsedJson)) return Apireturn::r(-1,'','數(shù)據(jù)為空');
$parsedJson = json_decode($parsedJson, true);
if(empty($parsedJson)) return Apireturn::r(-1,'','數(shù)據(jù)為空');
//谷歌訂單號(hào)
$transactionId = $parsedJson['orderId'];
//訂單號(hào)
$oid = $parsedJson['obfuscatedAccountId'];
if(empty($transactionId)) return Apireturn::r(-1,'google訂單號(hào)空');
if(empty($oid)) return Apireturn::r(-1,'訂單號(hào)空');
if (!empty($transactionId)) {
//記錄支付信息
Order::I()->updateTransId($oid, $transactionId);
}
//商品id
$pid = $parsedJson['productId'];
$orderToken = $parsedJson['purchaseToken'];
$packageName = $parsedJson['packageName'];
//獲取accesstoken
$accessToken = $this->getAccessToken();
if ($accessToken) {
//驗(yàn)證鏈接
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$packageName.'/purchases/products/'.$pid.'/tokens/'.$orderToken.'?access_token='.$accessToken;
$result = $this->httpCurl($url,'','GET');
$contents = json_decode($result,true);
if (!empty($contents)) {
if (isset($contents['error'])) {
return Apireturn::r(-1,'請(qǐng)求的身份驗(yàn)證無效');
}
if($contents['consumptionState'] === 0 && $contents['purchaseState'] === 0){
//驗(yàn)證成功 購買成功並且沒有消耗 google支付中客戶端如果沒有進(jìn)行消耗是不能再次購買該商品
$res = Order::I()->finishPayIos($oid,$transactionId);
if($res === true){
return Apireturn::r(200,'購買成功');
}else {
return Apireturn::r(-1,$res);
}
}else{
//訂單驗(yàn)證失敗
return Apireturn::r(-1,'訂單狀態(tài)有誤');
}
}
} else {
return Apireturn::r(-1,'TOKEN丟失');
}
}
/**
* google token
* @return array
*/
private function getAccessToken()
{
$cacheKey = "google.kanshu.access_token";
$accessToken = cache($cacheKey);
if ($accessToken) return $accessToken;
$url = 'https://accounts.google.com/o/oauth2/token';
$data['refresh_token'] = $this->refreshToken;
$data['client_id'] = $this->client_id;
$data['client_secret'] = $this->client_secret;
$data['grant_type'] = 'refresh_token';
$response = $this->httpCurl($url,$data,'POST');
$result = json_decode($response, true);
if ($result) {
if (isset($result['access_token'])) {
cache($cacheKey,$result['access_token'],$result['expires_in']-10);
return $result['access_token'];
}
}
return false;
}
//curl提交請(qǐng)求
private function httpCurl($url, $params, $method = 'GET', $header = array(), $multi = false)
{
date_default_timezone_set('PRC');
$opts = array(
CURLOPT_TIMEOUT => 30,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => $header,
CURLOPT_COOKIESESSION => true,
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_COOKIE => session_name() . '=' . session_id(),
);
/* 根據(jù)請(qǐng)求類型設(shè)置特定參數(shù) */
switch (strtoupper($method)) {
case 'GET':
// $opts[CURLOPT_URL] = $url . '?' . http_build_query($params); 鏈接后拼接參數(shù) & 非拍柒?
$opts[CURLOPT_URL] = $url . '?' . http_build_query($params);
break;
case 'POST': //判斷是否傳輸文件
$params = $multi ? $params : http_build_query($params);
$opts[CURLOPT_URL] = $url;
$opts[CURLOPT_POST] = 1;
$opts[CURLOPT_POSTFIELDS] = $params;
break;
default:
return ApiReturn::r(-1,'','不支持的請(qǐng)求方式');
throw new Exception('不支持的請(qǐng)求方式!');
}
/* 初始化并執(zhí)行curl請(qǐng)求 */
$ch = curl_init();
curl_setopt_array($ch, $opts);
$data = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ApiReturn::r(-1,'','請(qǐng)求發(fā)生錯(cuò)誤'.$error);
throw new Exception('請(qǐng)求發(fā)生錯(cuò)誤:' . $error);
}
return $data;
}
}
寫在最后
- 這是本人借鑒頂部幾位 并對(duì)接后總結(jié)出來的大致文檔
- 如有什么描述不夠清楚 可以回到頂部查看引用的其他幾位的文章
錯(cuò)誤1:此版本的應(yīng)用未配置為通過GooglePlay結(jié)算屈暗。有關(guān)詳情拆讯,請(qǐng)?jiān)L問幫助中心。
app內(nèi)錯(cuò)誤截圖
出現(xiàn)問題原因
是打包的時(shí)候养叛,versionCode的值比提交到google play后臺(tái)的版本要高种呐。
打包的時(shí)候,和google play后臺(tái)上的包的簽名不一致弃甥,APK與發(fā)布證書一起簽署爽室。(重要提示:使用“Google Play應(yīng)用程序簽名”時(shí),只有直接從Google PlayStore下載才有效淆攻!
登錄測試機(jī)已登錄谷歌賬號(hào)阔墩,并已添加到谷歌測試賬號(hào)中,添加地址 https://play.google.com/apps/testing/包名
確認(rèn)賬號(hào)信息里可用于測試的Gmail賬號(hào)里瓶珊,已添加測試賬號(hào)啸箫。
錯(cuò)誤2:應(yīng)用內(nèi)無法查詢商品id,無法吊起支付窗口
一定要等你的應(yīng)用為Published狀態(tài)之后伞芹,并且創(chuàng)建了應(yīng)用內(nèi)商品為已激活狀態(tài)忘苛,在app里面才能查到商品id,執(zhí)行支付等操作,否則怎么樣都查不到
應(yīng)用已經(jīng)發(fā)布了扎唾,但是還是吊不起支付窗口
首先app需要安裝google三件套蜀肘,不知道怎么安裝的,可以安裝一個(gè)YouTube稽屏,進(jìn)入app后會(huì)自動(dòng)提示你需要安裝google play扮宠,需要vpn,然后一定注意了狐榔,需要在權(quán)限設(shè)置里面坛增,把google play的##允許應(yīng)用在后臺(tái)彈窗界面##這個(gè)權(quán)限打開,一定記得要打開
當(dāng)一切準(zhǔn)備就緒之后薄腻,報(bào):目前還無法在您所在的國家/地區(qū)購買Google Play中的內(nèi)容收捣。這個(gè)因?yàn)樵谥袊辉试S。
網(wǎng)頁登錄你的google賬戶:在設(shè)置里面庵楷,把你的地址改成美國或其他支持的國家罢艾,然后清除app內(nèi)google play的數(shù)據(jù),重新進(jìn)入尽纽,重新選擇地區(qū)(剛才你修改的地區(qū))咐蚯,選完之后會(huì)提示你切換到美國的商店,然后再添加付款信息(中國的卡就行弄贿,我用的是招商銀行的信用卡)春锋,這樣就萬事大吉了,之后就可以正常支付了差凹。
錯(cuò)誤3:In-app billing API version 3 is not supported on this device.
該手機(jī)登錄Google賬號(hào)的問題期奔,或者說是Google賬號(hào)的歸屬地問題,如果之前是ok的危尿,突然報(bào)這樣的的問題呐萌,重啟,重啟谊娇,重啟 7喂隆!邮绿!包括電腦渠旁。至于哪些地方不能使用,我沒有足夠的賬號(hào)數(shù)據(jù)支撐船逮」死埃或許咱們能做的是在產(chǎn)品角度,給出相應(yīng)提示挖胃,例如更換賬號(hào)啥的杂靶。
錯(cuò)誤4:signInResult:failed code=12501
先檢查網(wǎng)絡(luò)是否墻了