Google 支付订阅补坑之路
目前我们 Google 订阅支付功能已经上线几个月,随着优化以及修改BUG,渐渐的趋于稳定了,所以这时候来总结一下,我遇到的坑。
回顾一下,之前的一篇文章:Google 支付订阅商品服务端设计方案
遇到的问题
根据之前的设计方案我遇到如下问题:
- VIP发放延迟;
- 出现重复发放;
- 用户在 Google play 看到的到期时间和app内的不一致
- 用户订阅之后取消订阅等骚操作问题;
分析解决问题
VIP发放延迟
我们运营收到客诉,说是购买的VIP发放有延迟,我当时人都傻了,服务端的创建订单和发放会员不是走的消息或者异步啊,怎么会出现延迟呢?
然后经过我吭哧吭哧查看日志,发现客户端调用校验Google收据的接口延迟了。
客户端说这种情况是存在的,比如用户购买会员成功后还没来得及调用服务端校验 Google 收据的接口,app进程杀掉了。。。。。
哎,那怎么办呢? 只能在谷歌回调服务端的时候处理了,因为当用户购买订阅之后,服务端会收到 Google的回调,而且一般来说,Google回调会比客户端调用服务端校验收据接口来的早(也可能同时)。
原来我们处理成功回调时,如果续订是空就不处理了,现在可不行了,因为 vipRenewalOrder 等于null,可能是一个新的订单,那走新订单处理的流程了。
if(vipRenewalOrder == null){
// 续订单不存在 处理新创建的订单
log.info("google 订阅回调通过订阅号:{} 发现续订订单不存在,那么就是用户下单google回调先于客户端调用", subscriptionPurchase.getObfuscatedExternalProfileId());
}
那么流程就是这样了
好这样子,会员就不会出现延迟发放了。
不过这里需要注意的是,回调时拿不到用户相关的信息,比如设备号,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 订阅在未过期之前,用户取消订阅,然后在我们app内购买订阅,Google 会产生一笔费用为0的订单,其实也就是说这个时候应该是不扣款,也不发会员。
但是我们订阅设计的缺陷,导致出现一个问题:
先回顾一下之前的设计,我们的签约号是通过客户端生成的,对于处理回调,会根据客户端提供的签约号获取续订记录,然而由于用户从APP中购买订阅,客户端会生成新的签约号,根据新的签约号无法获取签约记录,被认为是一个新的订阅。。。。。。那么用户没有花钱,就能享受发放VIP会员(卧槽,那找到这个BUG,还不得使劲薅啊)
怎么解决这个问题呢? 签约号是无法识别了,那根据用户来识别?
显然也是不行的,如果这个用户购买多种续订产品,你无法识别是哪种产品的签约记录发生改变。
那有人说,通过用户和产品ID来确定是哪个签约记录啊(一个用户同一个商品只能有一个签约),其实也不行;
假如有这样的场景:用户A 购买了连续包月产品 P,那么会生成一个订阅记录,签约号为:GAP.12334467
用户 | 商品 | 签约号 |
---|---|---|
A | P | GAP.12334467 |
如果用 用户和产品好像确实可以找到是哪个签约记录,然后假如,用户A切换app账号为用户B,然后先在 Google 的订阅管理里面取消订阅,之后在我们app中购买包月产品P,那么这个时候会生成一个签约号:GAP.878234534
用户 | 商品 | 签约号 |
---|---|---|
B | P | GAP.878234534 |
这时对于Google来讲,还是同一个用户(Google账号没变),你没法通过用户和产品找到是哪个签约了。那怎么办,没办法实现这个功能了啊,别急,其实人家Google早就解决这个问题了 可以参考 Google Play Billing 系列分享: 订阅取消后的那些事儿——恢复订阅和重新订阅 这篇文章。
所以,对于回调的设计改为下图:
部分代码:
/**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);
}
}
对于上面代码的处理,简单解释一下:
- 校验订单:主要是检查订单是否支付,这一步很重要,调用Google SDK获取订单。
- 通过订单号检查订单是否已经处理:解决幂等问题,防止重复调用。
- 通过签约号获取订单
-
没有获取到订单,这时候有两种情况: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