谷歌支付汇总

google支付分为订阅和应用内购买两种,这里使用的是应用内购买这种方式,这里将整个google支付和支付验证的流程记录下来。

注意项:
前端需要商品ID调起支付
后端需要获取token,使用token去Google三方链接验证订单

前端 导入google结算库

google结算服务接入地址

def billing_version = "4.0.0"
implementation "com.android.billingclient:billing-ktx:$billing_version"

接入支付

流程:

  1. 初始化链接到google支付服务,如果不能链接到说明设备环境有问题,要么是没有翻墙,要么是google套件(google paly 、server)没有安装完整,国内手机都是阉割过的,所以需要重新安装google套件
  2. 查询上次未消费的商品,如果有未消费的商品通知服务器,然后消费掉。因为国外的支付环境和国内不一样,他们可以线上下单,然后到便利店去支付,所以有未消费的这种情况。
    这时google支付的准备工作已完成,下面就可以发起支付了
  3. 使用google后台配置商品id进行支付
  4. 支付完成后通知服务器验证订单合法性并发货
  5. 客户端消费商品

下面咋们上代码

step1
初始化并连接到google服务

    // init方法
    public synchronized void init(Activity mActivity){
        //创建BillingClient 对面,查询 消费  支付都会使用这个对象
        this.mBillingClient = BillingClient.newBuilder(mActivity)
                .setListener(new PurchasesUpdatedListener() {//设置支付回调,这里其实是商品状态发生变化时就会回调
                    @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);
                                //通知服务器支付成功,服务端验证后,消费商品
                            }
                            //TODO客户端同步回调支付成功
                        } 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服务
                payEnable = true;
                queryPurchases();
            }
        }

        @Override
        public void onBillingServiceDisconnected() {
            //未链接到google服务
            payEnable = false;
            connectBillPay();
        }
    }

setp2
查询已支付的商品,并通知服务器后消费(google的支付里面,没有消费的商品,不能再次购买)

    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);
                    //这里处理已经支付过的订单,通知服务器去验证
                }
            }
        };
        mBillingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, mPurchasesResponseListener);
    }

setp3
发起支付

    /**
     * 
     * @param cpOrder 你自己的订单号或者用户id,用于关联到对应的用户,发放道具时使用
     * @param productId google后台配置产品ID
     */
    public void pay(final String cpOrder, final String productId) {
        if(mBillingClient == null || wrActivity.get() == null || !payEnable){
             //TODO客户端同步回调支付失败,原因是为链接到google或者google的支付服务不能使用
            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())){
                                    //发起支付
                                    launchBillingFlow(cpOrder, skuDetails);
                                }
                            }
                        }
                    }
                });
    }

  //吊起google支付页面
    void launchBillingFlow(String cpOrder, SkuDetails skuDetails){
        mBillingClient.launchBillingFlow(
        wrActivity.get(),
        BillingFlowParams
                .newBuilder()
                .setSkuDetails(skuDetails)
                .setObfuscatedAccountId(cpOrder)//这里本来的意思存放用户信息,类似于国内的透传参数,我这里传的我们的订单号。老版本使用DeveloperPayload字段,最新版本中这个字段已不可用了
                .build()
        );
    }

服务器支付验证操作较为复杂,咋们在下面单独提出来做一个小节

setp5
消费商品

    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) {
                    //消费失败将商品重新放入消费队列
                    OrderManager.getInstance().consumeFinal(purchase);
                    return;
                }
                LogUtils.d("消费成功");
            }
        };
        mBillingClient.consumeAsync(consumeParams, listener);
    }

服务端验证

做服务端验证前,需要做一下准备工作

大致流程:
  1. 创建api项目这个和登录用的项目不是同一个
  2. 开启Google Play Android Developer API
  3. 设置oauth同意屏幕(就是拉起开发者授权账号登录时的登录页面)
  4. 创建web应用的oauth客户端ID
  5. 拉起授权页面,使用google开发者账号给项目授权,得到code
  6. 创建谷歌账号,并开通开发者权限,需要支付25美元
  7. google play开发者后台,API权限菜单中关联刚刚创建的项目,一个google play账号只需要也只能关联一个api项目就行了,这个项目可以查询关联账号中的所有应用的订单
  8. 通过code,拿到refreshToken,这个token只有第一次才会返回需要永久储存(这个refreshtoken很重要,需要保存下来),如果弄丢,只有重新创建一个oauth客户端ID,然后重复步骤5,拿到新的refreshtoken(如果确认信息没有填写错误,仍旧提示clinet错误,可以新创建一个客户端ID,重复步骤5)
  9. 创建付款资料 完善收款信息
  10. 查询订单状态前,需要先去申请产品ID和支付金额,支付是根据产品ID进行支付 。在此之前,需要先创建付款资料
  11. 刷新refreshToken, 得到accessToken,通过accesstoken就可以去查询订单状态了
  12. 回调查询订单状态

上操作截图

setp1
创建api项目google api console

image

setp2
开启Google Play Android Developer API

image
image

搜索“Google Play Android Developer API”

image

开启“Google Play Android Developer API”

image

setp3
开启同意屏幕

image

这里填上必填项就行了,这个授权同意屏幕,请求code时拉起来给咋们开发人员开的,填啥都无所谓

setp4
创建oauth2客户端id

image

创建页面和创建成功后的修改页面可以获取到clientId和clientSecret

image

到这里api项目就已经创建好了

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={创建的clientId}
将上面的{XX}替换成创建api项目时填写的重定向地址,和clientId,然后将连接放到浏览器中打开,就会吊起授权界面,使用你的开发者账号授权登录
请求方式:浏览器中打开

image
image

这里可以看到,重定向地址上有两个参数code和scope,我们只需要code就行了,这里的code是urlencode后的,使用时需要decode

setp6
创建谷歌账号,并开通开发者权限(https://play.google.com/console/signup),需要支付25美元

setp7
google play后台关联api项目

image

setp8
使用code换取refreshToken
地址:https://accounts.google.com/o/oauth2/token
请求方式:post
参数:grant_type=authorization_code
code=获取到的code(需要看看code中是否有%号,如果有需要urldecode)
client_id=创建api项目是的clientId(客户端ID)
client_secret=创建api项目时的clientSecret(客户端密钥)
redirect_uri=创建api项目时的重定向地址

image

这里就获取到refreshToken了,重点重点重点,refreshToken保存下来,它只会在第一次请求中返回,后续用在发一样的请求不会返回refreshtoken,如果不慎弄丢了,需要去重新创建一个WebClientId

setp9
进入gooleplay管理后台 创建付款资料 完善收款信息

lQLPDhtBotn-LwzNA0nNBkSwF1WGbraApR8CPDYhHUBGAA_1604_841.png

image.png

setp10

image.png

setp11
使用refreshToken获取accessToken
地址:https://accounts.google.com/o/oauth2/token
请求方式:post
参数:grant_type=refresh_token
refresh_token=刚刚获取到的refreshToken
client_id=创建api项目是的clientId(客户端ID)
client_secret=创建api项目时的clientSecret(客户端密钥)

image

setp12
查询订单状态
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}?access_token={access_token}
packageName:app包名,必须是创建登录api项目时,创建android客户端Id使用包名
productId:对应购买商品的商品ID
token:购买成功后Purchase对象的getPurchaseToken()
access_token:上面咋们获取到的accessToken
请求方式:get
返回值:

{
  "purchaseTimeMillis": "1623980699933",//购买产品的时间,自纪元(1970 年 1 月 1 日)以来的毫秒数。
  "purchaseState": 0,//订单的购买状态。可能的值为:0\. 已购买 1\. 已取消 2\. 待定
  "consumptionState": 0,//产品的消费状态。可能的值为: 0\. 尚未消耗 1\. 已消耗
  "developerPayload": "",
  "orderId": "GPA.3398-6726-1036-80298",//google订单号
  "purchaseType": 0,
  "acknowledgementState": 0,
  "kind": "androidpublisher#productPurchase",
  "obfuscatedExternalAccountId": "SDK2106180944530041",//上面客户支付时的透传字段,google指导是用来存放用户信息的,不能过长,否则客户端不能支付
  "obfuscatedExternalProfileId": "",
  "regionCode": "HK"
}

下面附上代码服务端获取accessToken和验证订单代码

<?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 = '';
    /**
     * 驗證google內購訂單
     * @param $parsedJson 來自客戶端,一個訂單數據的JSON字符串
     * @return array
     */
    public function google($post)
    {
        $parsedJson = $post['data'];
        if(empty($parsedJson))   return Apireturn::r(-1,'','数据为空');
        $parsedJson = json_decode($parsedJson, true);
        if(empty($parsedJson))   return Apireturn::r(-1,'','数据为空');
        //谷歌订单号
        $transactionId = $parsedJson['orderId'];
        //订单号
        $oid = $parsedJson['obfuscatedAccountId'];
        if(empty($transactionId))   return Apireturn::r(-1,'google订单号空');
        if(empty($oid))   return Apireturn::r(-1,'订单号空');
        if (!empty($transactionId)) {
            //记录支付信息
            Order::I()->updateTransId($oid, $transactionId);
        }
        //商品id
        $pid = $parsedJson['productId'];
        $orderToken = $parsedJson['purchaseToken'];
        $packageName = $parsedJson['packageName'];
        //获取accesstoken
        $accessToken = $this->getAccessToken();
        if ($accessToken) {
             //验证链接
            $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,'请求的身份验证无效');
                }
                if($contents['consumptionState'] === 0 && $contents['purchaseState'] === 0){
                    //驗證成功  購買成功並且沒有消耗  google支付中客戶端如果沒有進行消耗是不能再次購買該商品
                    $res = Order::I()->finishPayIos($oid,$transactionId);
                    if($res === true){
                        return Apireturn::r(200,'购买成功');
                    }else {
                        return Apireturn::r(-1,$res);
                    }
                }else{
                    //訂單驗證失敗
                    return Apireturn::r(-1,'订单状态有误');
                }
            }
        } 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提交请求
    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(),
        );
        /* 根据请求类型设置特定参数 */
        switch (strtoupper($method)) {
            case 'GET':
                // $opts[CURLOPT_URL] = $url . '?' . http_build_query($params);  链接后拼接参数  &  非?
                $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,'','不支持的请求方式');
                throw new Exception('不支持的请求方式!');
        }
        /* 初始化并执行curl请求 */
        $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,'','请求发生错误'.$error);
            throw new Exception('请求发生错误:' . $error);
        }
        return $data;
    }
}

写在最后

  • 这是本人借鉴顶部几位 并对接后总结出来的大致文档
  • 如有什么描述不够清楚 可以回到顶部查看引用的其他几位的文章

错误1:此版本的应用未配置为通过GooglePlay结算。有关详情,请访问帮助中心。
app内错误截图

出现问题原因
是打包的时候,versionCode的值比提交到google play后台的版本要高。
打包的时候,和google play后台上的包的签名不一致,APK与发布证书一起签署。(重要提示:使用“Google Play应用程序签名”时,只有直接从Google PlayStore下载才有效!
登录测试机已登录谷歌账号,并已添加到谷歌测试账号中,添加地址 https://play.google.com/apps/testing/包名
确认账号信息里可用于测试的Gmail账号里,已添加测试账号。

错误2:应用内无法查询商品id,无法吊起支付窗口
一定要等你的应用为Published状态之后,并且创建了应用内商品为已激活状态,在app里面才能查到商品id,执行支付等操作,否则怎么样都查不到

应用已经发布了,但是还是吊不起支付窗口
首先app需要安装google三件套,不知道怎么安装的,可以安装一个YouTube,进入app后会自动提示你需要安装google play,需要vpn,然后一定注意了,需要在权限设置里面,把google play的##允许应用在后台弹窗界面##这个权限打开,一定记得要打开

当一切准备就绪之后,报:目前还无法在您所在的国家/地区购买Google Play中的内容。这个因为在中国不允许。
网页登录你的google账户:在设置里面,把你的地址改成美国或其他支持的国家,然后清除app内google play的数据,重新进入,重新选择地区(刚才你修改的地区),选完之后会提示你切换到美国的商店,然后再添加付款信息(中国的卡就行,我用的是招商银行的信用卡),这样就万事大吉了,之后就可以正常支付了。

错误3:In-app billing API version 3 is not supported on this device.
该手机登录Google账号的问题,或者说是Google账号的归属地问题,如果之前是ok的,突然报这样的的问题,重启,重启,重启 !!!包括电脑。至于哪些地方不能使用,我没有足够的账号数据支撑。或许咱们能做的是在产品角度,给出相应提示,例如更换账号啥的。

错误4:signInResult:failed code=12501
先检查网络是否墙了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容