踩坑笔记 —- 使用LocalDateTime开通会员到期时间与自动续费业务某天用户突然为0

  • Post author:
  • Post category:其他




写在前面


使用LocalDateTime的同学需要注意下,这东西的plusMonth可能会有点点超出你的认知,如果不慎掉坑里,希望这篇笔记可以给你提供思路



业务背景

此业务场景非常简单,自动续费业务,需要在用户会员到期前24小时执行扣款服务,为用户开通续费会员



踩坑描述

这是一个组合坑,续费job无任何问题,会员管理端也没有任何问题,但是他们组合在一起,就是问题。

某日自动续费用户突然为0,相关负责同事让我排查问题,经排查发现,续费日后一天权益到期的用户数为0,因此导致不能扣费,具体排查过程简介如下



业务实现逻辑



续费端



定时任务

使用 elastic-job 配置定时器,在每日固定时间7点执行扣费任务



业务步骤

  • 扫描全部具有签约协议的用户
  • 查询每个用户的会员到期时间,与当前时间做对比,判断是否在24小时内,如果在24小时内,则进行扣费
  • 扣费记录入库,预订单入库,预开通权限入库,通知扣款服务商进行扣费,等待异步扣费结果通知

续费端业务逻辑并不复杂,按照每天扫描一次的方式执行,有到期的用户就进行续费,



会员管理端(踩坑点)



开通权限流程

  • 接受异步扣款通知
  • 验证通知中的签名等基本安全认证信息
  • 根据通知中的订单记录id,查找订单和预开通权限
  • 根据预开通权限的内容,为当前订单用户开通相关权益
  • 更新记录,订单完毕,续费表记录完毕



踩坑点细节

主要问题在开通权益这里,权益表存储数据包括了续费周期(1),和续费单位(MONTH),因此需要为用户开通一个月权益,考虑到用户权益可能未到期,因此需要把用户的最大到期时间,和当前时间做对比,取最大值,然后根据最大值开通一个月会员即可,伪代码如下

// 查询用户到期时间
LocalDateTime dbEndTime = fromDbEndTime(userId);
// 当前时间
LocalDateTime now = LocalDateTime.now();
// 二者取最大值作为权限的开始时间
LocalDateTime startTime = maxTime(now,dbEndTime);
// 计算用户应该开通的最大到期时间(异常点在这里)
LocalDateTime endTime = startTime.plusMonths(duration).minusDays(1).toLocalDate().atTime(LocalTime.MAX)
// 数据入库
save(userId,startTime,endTime);


异常说明

LocalDateTime的plusMonths这个api,他会计算本月延后一个月的天数,是否在下个月存在,如果不存在,他会保留下个月的最大值,比如

你是在1.31日任意时间点开通权限,向后延长一个月,保留时间为2.27 23:59:59

在这里插入图片描述

如果你在1.30 开通,也是这个时间

在这里插入图片描述

也就是说:


大月份(31天的月份)衔接小月份(不足31天的月份),对于自动续费业务来说,会在某一天的用户总量会翻倍

你以为这就完了?没,小月份衔接大月份,某一天会没用户

比如:

4.30日开通权益的用户,到期时间是 5.29. 23:59:59

5.1日开通权益的用户,到期时间是 5.31 23:59:59

在这里插入图片描述

在这里插入图片描述

看出问题没?


5.30那天,没人能到期,因此5.29日的扣费程序就不会有任何人被扣款

查看LocalDateTime的plusMonths 说明,并没有说明小月份衔接大月份的问题

在这里插入图片描述



总结

这个不能算JDK的bug,没有踩过这个坑的人,可能也想不到这个问题,如果使用这个api,你的程序大概也会这样,希望这个笔记能对你有帮助,在有人找你排查问题的时候,能快速定位并解决问题



版权声明:本文为weixin_42321034原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。