likes
comments
collection
share

Google 支付订阅补坑之路

作者站长头像
站长
· 阅读数 41

目前我们 Google 订阅支付功能已经上线几个月,随着优化以及修改BUG,渐渐的趋于稳定了,所以这时候来总结一下,我遇到的坑。

回顾一下,之前的一篇文章:Google 支付订阅商品服务端设计方案

遇到的问题

根据之前的设计方案我遇到如下问题:

  • VIP发放延迟;
  • 出现重复发放;
  • 用户在 Google play 看到的到期时间和app内的不一致
  • 用户订阅之后取消订阅等骚操作问题;

分析解决问题

VIP发放延迟

我们运营收到客诉,说是购买的VIP发放有延迟,我当时人都傻了,服务端的创建订单和发放会员不是走的消息或者异步啊,怎么会出现延迟呢?

然后经过我吭哧吭哧查看日志,发现客户端调用校验Google收据的接口延迟了。

客户端说这种情况是存在的,比如用户购买会员成功后还没来得及调用服务端校验 Google 收据的接口,app进程杀掉了。。。。。

哎,那怎么办呢? 只能在谷歌回调服务端的时候处理了,因为当用户购买订阅之后,服务端会收到 Google的回调,而且一般来说,Google回调会比客户端调用服务端校验收据接口来的早(也可能同时)。

Google 支付订阅补坑之路

原来我们处理成功回调时,如果续订是空就不处理了,现在可不行了,因为 vipRenewalOrder 等于null,可能是一个新的订单,那走新订单处理的流程了。

if(vipRenewalOrder == null){
    // 续订单不存在 处理新创建的订单
    log.info("google 订阅回调通过订阅号:{} 发现续订订单不存在,那么就是用户下单google回调先于客户端调用", subscriptionPurchase.getObfuscatedExternalProfileId());

}

那么流程就是这样了

Google 支付订阅补坑之路

好这样子,会员就不会出现延迟发放了。

不过这里需要注意的是,回调时拿不到用户相关的信息,比如设备号,IP等,而且我们的业务需要通过设备判断是否首购的,所以这时候连订单价格都不能确定,所以当客户端调用校验收据接口时再补充到订单上即可。

讨论:有了 Google 回调是不是不需要客户端调用校验收据接口了呢?

我个人觉得需要的,主要是我们目前业务也是需要的

  • 第一,需要客户端调校验收据接口补充订单完整数据(上面注意事项提到了);
  • 第二,我们的一次性购买商品(单月,单季)没有创建在订阅里面,所以购买一次性商品不会有回调;

会员重复发放

运营发现,有极少数订单出现重复2次,会员发放重复的问题。

重复发放那就是幂等和并发没有控制好了,我查了一下日志,发现调用服务端校验订单的接口,一秒钟调用了几十次,我擦,被人搞了?

我一想,不对啊,我做了并发控制啊,redis 分布式锁保证一个线程处理的啊,好,把代码来出来鞭尸:

@Override
@Transactional
public void verifyInapp(GooglePayVerifyRequest request) throws UserClientException {
     // ----- 业务逻辑
    // 分布式锁
    RLock lock = null;
    try {
        lock = redisUtils.lock(String.format(Constant.GOOGLE_PAY_VERIFY_LOCK, signtureData.getOrderId()));
       // ----- 业务逻辑
       // 根据 订单ID判断数据库里是否已经处理订单了,保证幂等性
       
    } finally {
        if (lock != null) {
            lock.unlock();
        }
    }

}

为了让大家看的清楚些,我把其他代码都删了。怎么样,看到这,我相信聪明的你们肯定发现了代码的BUG。

这个锁放在 Service 里面,当释放锁的时候,事务并没有提交,极端的时候会出现,多个线程执行这个方法,一个线程拿到锁,执行业务逻辑,然后释放锁,可这时候事务还没有提交呢,另一个线程钻了空子,从而没有控制好并发。。。。。

所以,为了解决这个问题,我把锁移到控制器里面了,确保先执行事务,再释放锁

用户在 Google play 看到的到期时间和app内的不一致

我们原来的逻辑是,会员商品和产品绑定,而一个产品创建时就确定是包月还是包季的(包月就是发放一个自然月,包季就是发放三个自然月),当发放会员时,根据商品找到对应发放的天数,直接给用户加VIP就好了。

其实这样设计我感觉挺合理的,但是这样有可能存在一个问题,就是 Google Play 管理订阅里面的过期时间和我们VIP会员的过期时间不相等。

并且还发现了另外一个问题,后面再说

要解决这个问题,那就利用谷歌订阅回调的订阅开始时间(startTimeMillis),和订阅结束时间(expiryTimeMillis)来计算VIP会员发放多少天,来看看一下google 回调的数据(注意,以下数据是google回调数据经过解码以及调用google SDK 订阅接口拿到的)

{  
    "acknowledgementState":1,  
    "autoRenewing":true,  
    "countryCode":"ID",  
    "developerPayload":"",  
    "expiryTimeMillis":1678952457401,  
    "kind":"androidpublisher#subscriptionPurchase",  
    "obfuscatedExternalAccountId":"27968459",  
    "obfuscatedExternalProfileId":"a0c5d023806244ea8799ad6d0890da38",  
    "orderId":"GPA.3399-6993-0095-71914..0",  
    "paymentState":1,  
    "priceAmountMicros":99000000000,  
    "priceCurrencyCode":"IDR",  
    "startTimeMillis":1673761260721  
}

这里要注意的是,startTimeMillis 是最先开始订阅时间,expiryTimeMillis 是订阅过期时间,比如:用户 1-20号开始订阅,包月的话,过期时间就是2-20号,但是第二次收到的回调是 startTimeMillis 为 1-20,expiryTimeMillis 是 3-20,所以计算发放时间不能用 expiryTimeMillis - startTimeMillis。

所以,续订表(vip_renewal_order)需要增加一个字段

`next_order_time` datetime DEFAULT NULL COMMENT '理论上下次扣款时间(过期时间)',

那么计算发放时间

第一期: expiryTimeMillis - startTimeMillis;

后面每一期:expiryTimeMillis - next_order_time;

并且把 expiryTimeMillis 写入 next_order_time。

注意:历史遗留问题,由于 next_order_time 是后面增加的,所以老订单这个字段是没有数据,那用户续订的话,计算发放时间,expiryTimeMillis - next_order_time,会报空指针错误,所以需要把这个数据补充上去,那怎么补充呢? 幸好我续订单保罗订单的返回信息, 只要通过脚本把这个 expiryTimeMillis 写入 next_order_time就OK了 Google 支付订阅补坑之路

用户订阅之后取消订阅等骚操作问题

Google 订阅在未过期之前,用户取消订阅,然后在我们app内购买订阅,Google 会产生一笔费用为0的订单,其实也就是说这个时候应该是不扣款,也不发会员。

但是我们订阅设计的缺陷,导致出现一个问题:

Google 支付订阅补坑之路

先回顾一下之前的设计,我们的签约号是通过客户端生成的,对于处理回调,会根据客户端提供的签约号获取续订记录,然而由于用户从APP中购买订阅,客户端会生成新的签约号,根据新的签约号无法获取签约记录,被认为是一个新的订阅。。。。。。那么用户没有花钱,就能享受发放VIP会员(卧槽,那找到这个BUG,还不得使劲薅啊)

怎么解决这个问题呢? 签约号是无法识别了,那根据用户来识别?

显然也是不行的,如果这个用户购买多种续订产品,你无法识别是哪种产品的签约记录发生改变。

那有人说,通过用户和产品ID来确定是哪个签约记录啊(一个用户同一个商品只能有一个签约),其实也不行;

假如有这样的场景:用户A 购买了连续包月产品 P,那么会生成一个订阅记录,签约号为:GAP.12334467

用户商品签约号
APGAP.12334467

如果用 用户和产品好像确实可以找到是哪个签约记录,然后假如,用户A切换app账号为用户B,然后先在 Google 的订阅管理里面取消订阅,之后在我们app中购买包月产品P,那么这个时候会生成一个签约号:GAP.878234534

用户商品签约号
BPGAP.878234534

这时对于Google来讲,还是同一个用户(Google账号没变),你没法通过用户和产品找到是哪个签约了。那怎么办,没办法实现这个功能了啊,别急,其实人家Google早就解决这个问题了 可以参考 Google Play Billing 系列分享: 订阅取消后的那些事儿——恢复订阅和重新订阅 这篇文章。

所以,对于回调的设计改为下图: Google 支付订阅补坑之路

部分代码:

/**google 订阅回调通过订阅号
 *  订阅自动扣款
 * @param  subscriptionPurchase
 */
private void subscriptionDeductionSucceeded(SubscriptionPurchase subscriptionPurchase, GoogleSubscribeCallback.DataDecode dataDecode, String purchaseToken) {
   if(Constant.GooglePay.checkSubscriptionPurchase(subscriptionPurchase)){
        // 校验失败
        log.info("Google 回调校验订单失败,结果:{}",  subscriptionPurchase);
        return;
    }

    VipOrder vipOrder = vipOrderRepository.findByThirdOrderSn(subscriptionPurchase.getOrderId());
    if(vipOrder != null){
        // 可能是第一次订阅 ,订单已经生成
        log.info("google 订阅回调订单:{} 已经生成,不需要再次处理", vipOrder.getId());
        return;
    }

    GrantVipRequest request;
    // 获取续订
    VipRenewalOrder vipRenewalOrder = renewalOrderRepository.findBySignNoAndSignChannel(subscriptionPurchase.getObfuscatedExternalProfileId(), RenewalSignChannelEnum.GOOGLE.getCode());
    if(vipRenewalOrder == null){
        // 续订单不存在
        log.info("google 订阅回调通过订阅号:{} 发现续订订单不存在,那么就是用户下单google回调先于客户端调用", subscriptionPurchase.getObfuscatedExternalProfileId());

        Long userId = Long.valueOf(subscriptionPurchase.getObfuscatedExternalAccountId());

        if(StringUtils.isNotEmpty(subscriptionPurchase.getLinkedPurchaseToken())) {

            // 拿本次的 token 记录中查上次的 token 如果有记录 那么这是重复请求 不再处理
            VipRenewalOrder checkVipRenewalOrder = renewalOrderRepository.findByLinkedPurchaseTokenAndPurchaseToken(subscriptionPurchase.getLinkedPurchaseToken(), purchaseToken);
            if(checkVipRenewalOrder != null){
                log.info("通过 purchaseToken :{} 和 LinkedPurchaseToken :{} 存在续订,说明token已经替换了,是重复请求", purchaseToken, subscriptionPurchase.getLinkedPurchaseToken());
                return;
            }

            // 表示重新订阅 修改订阅
            vipRenewalOrder = renewalOrderRepository.findByPurchaseToken(subscriptionPurchase.getLinkedPurchaseToken());

            if(vipRenewalOrder != null){
                // 判断 订阅是否过期
                int diffDays = Math.abs(DateUtils.diffDays(new Date(subscriptionPurchase.getExpiryTimeMillis()), vipRenewalOrder.getNextOrderTime()));
                if(diffDays == 0){
                    log.info("收到新订阅,用户:{} google 订阅过期时间毫秒:{},续订单过期时间:{}, 没有新加的日期,因此本次订阅无效,是用户取消后重新订阅的操作", vipRenewalOrder.getUserId(),
                            subscriptionPurchase.getExpiryTimeMillis(), vipRenewalOrder.getNextOrderTime());

                    vipRenewalOrder.setPurchaseToken(purchaseToken);
                    vipRenewalOrder.setLinkedPurchaseToken(subscriptionPurchase.getLinkedPurchaseToken());
                    vipRenewalOrder.setSignNo(subscriptionPurchase.getObfuscatedExternalProfileId());
                    vipRenewalOrder.setNextOrderTime(new Date(subscriptionPurchase.getExpiryTimeMillis()));
                    renewalOrderRepository.save(vipRenewalOrder);
                    log.info("用户:{} 出现重复订阅,修改 LinkedPurchaseToken :{} =》{}, 订阅号:{} =》{}", userId, subscriptionPurchase.getLinkedPurchaseToken(), purchaseToken,
                            vipRenewalOrder.getSignNo(), subscriptionPurchase.getObfuscatedExternalProfileId());
                    return;
                } else {
                    log.info("收到用户:{} 重新订阅,该订阅已经过期,google 订阅过期时间毫秒:{},续订单过期时间:{}", vipRenewalOrder.getUserId(),
                            subscriptionPurchase.getExpiryTimeMillis(), vipRenewalOrder.getNextOrderTime());
                }
            }
        }

        MarketVipGoodsVo marketVipGoodsVo = commodityFeign.findMarketGoodsByGoodsCode(dataDecode.getSubscriptionNotification().getSubscriptionId()).getData();
        RequestHeaderDTO build = new RequestHeaderDTO();
        build.setUserId(userId);
        build.setCountryIsoCode(subscriptionPurchase.getCountryCode());
        vipOrder = vipOrderService.createFirstOrderByGooglePayCallBack(build, dataDecode.getPackageName(), PayMethodEnum.GOOGLE_PAY_PRIMORDIAL.getCode(),
                marketVipGoodsVo, subscriptionPurchase);

        // 创建续订订单
        vipRenewalOrder = vipOrderService.createSubscribeOrder(vipOrder, subscriptionPurchase.getObfuscatedExternalProfileId(),
                PayMethodEnum.generateVipOrderSn(PayMethodEnum.GOOGLE_PAY_PRIMORDIAL), RenewalSignStatusEnum.SUCCESS, RenewalSignChannelEnum.GOOGLE,
                JSON.toJSONString(subscriptionPurchase));

        updateRenewalOrder(vipRenewalOrder, subscriptionPurchase, purchaseToken);

        // 计算发放天数
        Long diffTime = (subscriptionPurchase.getExpiryTimeMillis() - subscriptionPurchase.getStartTimeMillis());
        Long days = diffTime / 86400000;
        log.info("用户:{},第0次订阅过期:{},开始:{},发放毫秒:{},天数:{}",userId, subscriptionPurchase.getExpiryTimeMillis() , subscriptionPurchase.getStartTimeMillis(), diffTime, days);

        request = Constant.buildGrantUserVipRequest(marketVipGoodsVo.getVipLevelId(), Arrays.asList(userId), days.intValue());

    } else {

        int diffDays = Math.abs(DateUtils.diffDays(new Date(subscriptionPurchase.getExpiryTimeMillis()), vipRenewalOrder.getNextOrderTime()));
        if(diffDays == 0){
            log.info("以为收到正常的新订阅,用户:{} google 订阅过期时间毫秒:{},续订单过期时间:{}, 没有新加的日期,因此本次订阅无效,是用户取消后重新订阅的操作", vipRenewalOrder.getUserId(),
                    subscriptionPurchase.getExpiryTimeMillis(), vipRenewalOrder.getNextOrderTime());
            return;
        }

        // 找到自动续费上一个单子
     //   VipOrder latestOrder = vipOrderRepository.findByOrderSn(vipRenewalOrder.getLatestOrderSn());
        VipOrder latestOrder = vipOrderRepository.findByOrderSnAndUserId(vipRenewalOrder.getLatestOrderSn(), vipRenewalOrder.getUserId());
        MarketVipGoodsVo marketVipGoodsVo = JSON.parseObject(vipRenewalOrder.getGoodsInfo(), MarketVipGoodsVo.class);

        // 根据上一个订单 创建一个新的订单
        vipOrder = vipOrderService.createOrderByGooglePayCallBack(dataDecode.getPackageName(), PayMethodEnum.GOOGLE_PAY_PRIMORDIAL.getCode(),
                marketVipGoodsVo, latestOrder, subscriptionPurchase);

        // 计算发放天数
        Long diffTime = (subscriptionPurchase.getExpiryTimeMillis() - vipRenewalOrder.getNextOrderTime().getTime());
        Long days = diffTime / 86400000;
        log.info("用户:{},第{}次订阅过期:{},开始:{},发放毫秒:{},天数:{}", vipOrder.getUserId(), vipRenewalOrder.getSubscriptionIndex() + 1, subscriptionPurchase.getExpiryTimeMillis() , vipRenewalOrder.getNextOrderTime().getTime(), diffTime, days);
        request = Constant.buildGrantUserVipRequest(marketVipGoodsVo.getVipLevelId(), Arrays.asList(vipOrder.getUserId()), days.intValue());

        // 修改续订
        if(!RenewalSignStatusEnum.SUCCESS.getCode().equals(vipRenewalOrder.getSignStatus())){
            vipRenewalOrder.setSignStatus(RenewalSignStatusEnum.SUCCESS.getCode());
            vipRenewalOrder.setStatusReason(null);
        }
        vipRenewalOrder.setLatestOrderSn(vipOrder.getOrderSn());
        vipRenewalOrder.setLatestOrderTime(vipOrder.getCreateTime());
        vipRenewalOrder.setDeductStatus(DeductStatusEnum.DEDUCTED_SUCCESS.getCode());
        vipRenewalOrder.setRenewalContent(JSON.toJSONString(subscriptionPurchase));
        vipRenewalOrder.setSubscriptionIndex(Optional.ofNullable(vipRenewalOrder.getSubscriptionIndex()).orElse(0) + 1);
        vipRenewalOrder.setNextOrderTime(new Date(subscriptionPurchase.getExpiryTimeMillis()));
        vipRenewalOrder.setPurchaseToken(purchaseToken);
        vipRenewalOrder.setLinkedPurchaseToken(subscriptionPurchase.getLinkedPurchaseToken());
        renewalOrderRepository.save(vipRenewalOrder);
        // 消耗订单
        acknowledgeGoogleOrder(subscriptionPurchase, dataDecode);
    }

    // 通知发放会员
    boolean grantUserVipSuccess = vipOrderService.grantUserVip(request);
    if(grantUserVipSuccess){
        vipOrderService.updateOrderDeliverSuccess(vipOrder);
    }

}

对于上面代码的处理,简单解释一下:

  1. 校验订单:主要是检查订单是否支付,这一步很重要,调用Google SDK获取订单。
  2. 通过订单号检查订单是否已经处理:解决幂等问题,防止重复调用。
  3. 通过签约号获取订单
    • 没有获取到订单,这时候有两种情况:1.新的续订订单,2 用户取消订阅后从App中重新购买VIP订单,那怎么识别呢? 新订单,Google回调的数据(这里指通过Google SDK获取的数据)中没有 linkedPurchaseToken,取消再购买的有这个字段,目的就是用来关联之前的那个订单的。

      • 通过 linkedPurchaseToken 和 purchaseToken 检查是否处理过续订单了(因为存在重试,这也是幂等操作)
      • 通过 linkedPurchaseToken 获取签约记录,找到之后,在检查发放日期,如果发放日期为0,表示是用户取消,然后重新购买的操作,这个时候,google不扣钱,也不创建订单,不发放会员。如果发放日期不为0,则表示用户重新购买,原来的订阅已经过期了,那么就需要创建订单,发放会员。
    • 通过签约号获取到订单,这也有两种情况,1:正常续订,2 通过Google Play中订阅管理里面重新订阅商品,我这里的区别方式是,检查google 的发放日期是否为0来区分

如上面说的,其实对于是否发放会员,我都通过Google回调数据里面的过期时间来判断的,如果我不用Google的时间发放会员,用自然月发放那就不好判断是否需要发放了。

转载自:https://juejin.cn/post/7202211586675687483
评论
请登录