admin管理员组

文章数量:1530987

Google Pay JAVA后端处理

前言:最近接了个需求,关于谷歌支付的处理流程。觉得有必要记录下来,在网上也找了很多资料,不 全。怎么个不全法呢?

*第一:很多人用的方法就是使用谷歌的publicKey来校验订单,然而使用publicKey去验证只是返回一个布尔值,验证购买成功或失败。拿不到订单信息,怎么处理?

*第二:文档的更新,google后台的更新,当时官方文档没有更新。所以如果第一次配置谷歌后台的话,会有很多坑。

好了废话不多说,进入正题吧。关于谷歌支付整体的流程:
①前端下单
②后台产生预订单
③前端调用谷歌进行支付
④支付成功谷歌返回凭证
⑤前端调用后台接口
⑥后台拿到凭证调用谷歌进行校验
⑦校验成功改变订单状态(最后就是处理成功后需要做的业务啦)

1、上面只是说了正常的校验。万一调用callback接口失败了,需要下一次调用,但是拿不到订单了(因为是下单的时候返回的订单号)。所以callback需要做一个补单操作!!!
2、用户要是在谷歌商店对商品的改动,比如说对订阅包的改动,进行一个升降级操作,此时需要让谷歌通知后端商品信息变更啦!所以谷歌推出实时开发者通知!

好了接下来出教程吧!!
一、在google后台创建应用,配置信息。具体配置可参照官网(详情步骤网上也是一找一大堆)
参考此博客:https://wwwblogs/kevin-zou/p/4533091.html
我用的是v3版本

接下来是代码:
商品我们有两种类型:消耗跟订阅!

//请求谷歌api,获取SubscriptionPurchase对象
            try {
                ClassPathResource classPathResource = new ClassPathResource(googleP12);
                InputStream input = classPathResource.getInputStream();
                HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
                PrivateKey privateKey = SecurityUtils.loadPrivateKeyFromKeyStore(
                        SecurityUtils.getPkcs12KeyStore(),
                        input, //文件流
                        "notasecret", "privatekey", "notasecret");
                GoogleCredential credential = new GoogleCredential.Builder()
                        .setTransport(transport).setJsonFactory(JacksonFactory.getDefaultInstance())
                        .setServiceAccountId(serviceAccountEmail) // 替换掉serviceAccountEmail
                        .setServiceAccountScopes(AndroidPublisherScopes.all())
                        .setServiceAccountPrivateKey(privateKey).build();
                AndroidPublisher publisher = new AndroidPublisher.Builder(transport,
                        JacksonFactory.getDefaultInstance(), credential).build();
                AndroidPublisher.Purchases.Subscriptions subscriptions = publisher.purchases().subscriptions();
                AndroidPublisher.Purchases.Subscriptions.Get subscription = subscriptions.get(packageName, subscriptionId, purchaseToken);
                purchase = subscription.execute();
            } catch (Exception e) {
                logger.info("[请求谷歌api出错了]");
                e.printStackTrace();
            }

以上就是请求谷歌产生订单的逻辑,获取的SubscriptionPurchase对象(此对象为订阅类型)。
如果为消耗:

 ClassPathResource classPathResource = new ClassPathResource(googleP12);
            InputStream input = classPathResource.getInputStream();
            logger.info(">>>destFilePath4"+classPathResource);
            HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
            PrivateKey privateKey = SecurityUtils.loadPrivateKeyFromKeyStore(
                    SecurityUtils.getPkcs12KeyStore(),
                    input, //文件流
                    "notasecret", "privatekey", "notasecret");
            GoogleCredential credential = new GoogleCredential.Builder()
                    .setTransport(transport).setJsonFactory(JacksonFactory.getDefaultInstance())
                    .setServiceAccountId(serviceAccountEmail) // 替换掉serviceAccountEmail
                    .setServiceAccountScopes(AndroidPublisherScopes.all())
                    .setServiceAccountPrivateKey(privateKey).build();
            AndroidPublisher publisher = new AndroidPublisher.Builder(transport,
                    JacksonFactory.getDefaultInstance(), credential).build();
            AndroidPublisher.Purchases.Products products = publisher.purchases().products();
            AndroidPublisher.Purchases.Products.Get product = products.get(body.getString("googlePackageName"), productId, purchaseToken);
            logger.info("【正在向GOOGLE消耗型商品 API发请求,请稍后...】");
            purchase = product.execute();
            logger.info(">>>purchase"+purchase.toString());

其实请求的逻辑是一样的,返回的对象不一样而已。获取ProductPurchase对象。
介绍请求参数:
googleP12:谷歌应用生成的P12文件,放在类路径下;
serviceAccountEmail:谷歌应用生成的serviceAccount;
packageName:谷歌应用的packageName;
productId:谷歌的产品Id;
purchaseToken:购买凭证;
前三个写死,后两个需要前端携带。
详情请参考官网:https://developers.google/android-publisher/api-ref/purchases/subscriptions(需要外网)没有外网的同学我直接给你copy出来啦!!!

然后根据Purchase的状态走相应的逻辑啦!

如果商品一昧的为消耗型类型的,那就结束啦!
但是订阅类型时,有很多功能:
自动续订、退订、升降级 这时需要怎么做?前端是不知道这些变化的。后台也不知道。所以需要谷歌通知。
详情请参照官方文档:https://developer.android/google/play/billing/realtime_developer_notifications.html#setup_cloud_pubsub
其实就是Pub/Sub模式订阅消费。
详情配置:https://blog.csdn/diaomeng11/article/details/103238253(其实就是拷贝官网)
配置好订阅的之后,该配置消费了

URL填写通知接口的全路径

 @RequestMapping(value = "/googleNotify")
    public String googleNotify(@RequestBody(required = false) byte[] body,HttpServletRequest request, HttpServletResponse response) {
        logger.info("【正在进入google play 实时开发通知入口,请稍后.......】");
        String paramStr = null;
        long time = System.currentTimeMillis() + 1000 * 60;
        String redisLock = String.format(RedisKeyUtil.REDIS_GOOGLE_PAY_TIME_LOCK);
        boolean isLock = redisUtils.lock(redisLock, String.valueOf(time));
        if(isLock) {
            try {
                //获取返回的内容,是一个字节数组
                paramStr = new String(body, "utf-8");
                //回调具体内容见下方
                logger.debug("googleNotify params :{}", paramStr);
                googleService.googleNotify(paramStr);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //释放锁
                redisUtils.unlock(redisLock, String.valueOf(time));
            }
        }
        return null;
    }
@Override
    @Transactional(rollbackFor = Exception.class)
    public void googleNotify(String paramStr) {
        if (StringUtils.isNotBlank(paramStr)){
            JSONObject paramJson = new JSONObject();
            try {
                paramJson = JSONObject.parseObject(URLDecoder.decode(paramStr,"utf-8"));
                /**
                 * paramJson数据格式如下
                 * "message": {
                 *         "data": "ewogICAgInZlcnNpb24iOiAiMS4wIiwKICAgICJwYWNrYWdlTmFtZSI6ICLljIXlkI0iLAogICAgImV2ZW50VGltZU1pbGxpcyI6ICLml7bpl7TmiLMo5q+r56eSKSIsCiAgICAic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjogewogICAgICAgICJ2ZXJzaW9uIjogIjEuMCIsCiAgICAgICAgIm5vdGlmaWNhdGlvblR5cGUiOiA0LAogICAgICAgICJwdXJjaGFzZVRva2VuIjogIuaUr+S7mOS7pOeJjCIsCiAgICAgICAgInN1YnNjcmlwdGlvbklkIjogIuiuoumYheeahOWVhuWTgWlkIgogICAgfQp9",
                 *         "messageId": "消息id",
                 *         "message_id": "消息id",
                 *         "publishTime": "2019-11-14T03:58:43.608Z",
                 *         "publish_time": "2019-11-14T03:58:43.608Z"
                 *     },
                 */
                JSONObject msgJson = paramJson.getJSONObject("message");
                String data = msgJson.getString("data");
                logger.debug("googleNotify data :{}",paramStr);
                //data 是由base64加密,解密即可
                String developerNotificationStr = new String(Base64.getDecoder().decode(data), "UTF-8");
                JSONObject developerNotificationJson = JSONObject.parseObject(developerNotificationStr);
                /**
                 * developerNotificationJson数据格式如下
                 *  {
                 *      "version":"1.0",
                 *       "packageName":"包名",
                 *       "eventTimeMillis":"时间戳(毫秒)",
                 *       "subscriptionNotification":{
                 *          "version":"1.0",
                 *          "notificationType":4,
                 *          "purchaseToken":"支付令牌",
                 *          "subscriptionId":"订阅的商品id"
                 *      }
                 *  }
                 */
                String packageName = developerNotificationJson.getString("packageName");
                JSONObject subscriptionNotificationJson = developerNotificationJson.getJSONObject("subscriptionNotification");
                logger.info("订阅后的收到的通知信息"+subscriptionNotificationJson.toJSONString());
                String purchaseToken = subscriptionNotificationJson.getString("purchaseToken");
                String subscriptionId = subscriptionNotificationJson.getString("subscriptionId");
                /**
                 * notificationType    int
                 * 通知的类型。它可以具有以下值:
                 * (1) SUBSCRIPTION_RECOVERED - 从帐号保留状态恢复了订阅。
                 * (2) SUBSCRIPTION_RENEWED - 续订了处于活动状态的订阅。
                 * (3) SUBSCRIPTION_CANCELED - 自愿或非自愿地取消了订阅。如果是自愿取消,在用户取消时发送。
                 * (4) SUBSCRIPTION_PURCHASED - 购买了新的订阅。
                 * (5) SUBSCRIPTION_ON_HOLD - 订阅已进入帐号保留状态(如已启用)。
                 * (6) SUBSCRIPTION_IN_GRACE_PERIOD - 订阅已进入宽限期(如已启用)。
                 * (7) SUBSCRIPTION_RESTARTED - 用户已通过“Play”>“帐号”>“订阅”重新激活其订阅(需要选择使用订阅恢复功能)。
                 * (8) SUBSCRIPTION_PRICE_CHANGE_CONFIRMED - 用户已成功确认订阅价格变动。
                 * (9) SUBSCRIPTION_DEFERRED - 订阅的续订时间点已延期。
                 * (10) SUBSCRIPTION_PAUSED - 订阅已暂停。
                 * (11) SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED - 订阅暂停计划已更改。
                 * (12) SUBSCRIPTION_REVOKED - 用户在有效时间结束前已撤消订阅。
                 * (13) SUBSCRIPTION_EXPIRED - 订阅已过期。
                 */
                int notificationType = subscriptionNotificationJson.getIntValue("notificationType");
                SubscriptionPurchase purchase = googleNotifyGetGoogleOrder(notificationType,packageName,subscriptionId,purchaseToken);
                logger.info("实时开发通知SubscriptionPurchase"+purchase.toString());
                if(!purchase.isEmpty()){
                    /** 回调后对应的购买流程 **/
                    if(4 == notificationType) {         //SUBSCRIPTION_PURCHASED - 购买了新的订阅。
                        logger.info("【购买了新订阅 不做处理】----"+purchase.toString());
                        //需要查出是哪个用户,查出哪个产品
                    }else if(3 == notificationType) {   //SUBSCRIPTION_CANCELED - 自愿或非自愿地取消了订阅。如果是自愿取消,在用户取消时发送。
                        logger.info("【取消了订阅】----"+purchase.toString());
                        //取消了自动订阅改变状态
                    }else if(12 == notificationType) {  //SUBSCRIPTION_REVOKED - 用户在有效时间结束前已撤消订阅。
                        logger.info("【撤销了订阅】----"+purchase.toString());
                    }else if(2 == notificationType) {   //SUBSCRIPTION_RENEWED - 续订了处于活动状态的订阅。  其实就是续订了
                        logger.info("【续订了订阅】----"+purchase.toString());
                        //根据现在请求谷歌的googleOrderId获取续订包的订单,获取用户跟productId
                        //续订的谷歌订单是有规则的,比如你第一次购买产生的订单是GPA.123-456-789,那第一次续订的产生的订单号是GPA.123-456-789..0,第二次GPA.123-456-789..1,依次类推。
                        //续订这里还有一个坑,当订阅包降级购买的时候,谷歌并不是通知你购买了新订阅包而是续订,比如你购买的高级订阅包是GPA.123-456-789,当你切换低级的订阅包的时候订单号为GPA.111-222-333..0,你会发现订单号变了,这时候就难办了,拿不到之前用户的信息(谷歌返回的订单参数是没有项目里面用户信息的),经过联调发现,他们两个订单的linkedPurchaseToken是一样的,可以参考上图中参数的介绍
                        //当降级和自动续订的时候都会走这个通知,所以里面有两个逻辑,需要自己去判断,这两者的不同上面都写的很清楚了。
                    }else if(7 == notificationType) {   //SUBSCRIPTION_RESTARTED - 用户已通过“Play”>“帐号”>“订阅”重新激活其订阅(需要选择使用订阅恢复功能)
                        logger.info("【重新注册了订阅】----"+purchase.toString());
                        //用户在谷歌后台重新激活的话,purchaseToken是不变的,我们只需通过Token找出之前的记录更改状态,过期时间是不变的
                    }else if(6 == notificationType) {  //订阅已进入宽限期(如已启用)。
                        logger.info("【订阅已进入宽限期】----"+purchase.toString());
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        }

以下为请求谷歌获取订单信息:

  /**
     * 访问谷歌api获取订阅订单
     * @param notificationType
     * @param packageName
     * @param subscriptionId
     * @param purchaseToken
     * @return
     */
    private SubscriptionPurchase googleNotifyGetGoogleOrder(int notificationType, String packageName, String subscriptionId, String purchaseToken){
        SubscriptionPurchase purchase = new SubscriptionPurchase();
        if(notificationType > 0) {
            //请求谷歌api,获取SubscriptionPurchase对象
            try {
                ClassPathResource classPathResource = new ClassPathResource(googleP12);
                InputStream input = classPathResource.getInputStream();
                HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
                PrivateKey privateKey = SecurityUtils.loadPrivateKeyFromKeyStore(
                        SecurityUtils.getPkcs12KeyStore(),
                        input, //文件流
                        "notasecret", "privatekey", "notasecret");
                GoogleCredential credential = new GoogleCredential.Builder()
                        .setTransport(transport).setJsonFactory(JacksonFactory.getDefaultInstance())
                        .setServiceAccountId(serviceAccountEmail) // 替换掉serviceAccountEmail
                        .setServiceAccountScopes(AndroidPublisherScopes.all())
                        .setServiceAccountPrivateKey(privateKey).build();
                AndroidPublisher publisher = new AndroidPublisher.Builder(transport,
                        JacksonFactory.getDefaultInstance(), credential).build();
                AndroidPublisher.Purchases.Subscriptions subscriptions = publisher.purchases().subscriptions();
                AndroidPublisher.Purchases.Subscriptions.Get subscription = subscriptions.get(packageName, subscriptionId, purchaseToken);
                purchase = subscription.execute();
            } catch (Exception e) {
                logger.info("[请求谷歌api出错了]");
                e.printStackTrace();
            }
        }
        return purchase;
    }

看到这里伙伴们是不是很清楚了呢?

我在项目中是写了三种方法去处理的!
⒈一个正常回调也就是callback,但他只能处理购买时的回调只针对于升级。如果是降级,高级包要在失效之后才能切换到你购买的低级包。所以,高级包在有效期内你去做降级操作,谷歌是没有产生订单的,需要等高级订阅包失效。
⒉实时开发者通知,对续订,退订,升降级操作的处理。(注意升级购买的时候实时开发者通知不处理,全都交由callback处理)
⒊定时器开发,因为每一次对谷歌订单的处理你都需要保存谷歌订单的信息,定时器会查询你所保存的购买凭证也就是linkedPurchaseToken去请求谷歌看看有没有订单生成,有没有自动续订。

当你这样做的话,就会全面覆盖谷歌支付的处理啦。
但是!没错还有但是,当自动续订的时候万一,定时器跟实时开发者通知并发了怎么办???
所以你需要加锁,谁先抢到就谁处理,(处理了不是会保存订单信息吗?所以加了锁还要进行个判断,判断该订单有没有处理)这样就就可以啦!!!!

哈哈,说到这会不会觉得茅塞顿开了呢!!!哈哈
第一次写,写的不好,不详细的请见谅,,,
如果还是不懂的可以私信我哦,记住我就是会撩头发的程序员!!!!!

本文标签: 后端GooglePayJava