可过期的积分系统

很多产品都会有积分系统:用户通过一定的行为累积积分,积分可以兑换各种权益,积分可能
会有增加(获取)、扣减(消耗)、查询余额、查看历史、过期、冻结等行为。

需求分析

1、积分过期的两种场景

积分过期是一种很常见的产品功能,因为一般都不希望用户累计大量积分,造成预算不可控。
过期有2种方式:

  • 统一时间过期:比如今年所有获得的积分,都在今年年末过期
  • 单笔时间过期:比如“A渠道获取的积分,3个月后过期”、“B渠道获取的积分,7天后过期”。

2、积分冻结应该存在吗

冻结举个具体的场景就是,大家都用使用积分来竞拍一件商品,此时积分未被消耗但随时可以消耗,因此提前冻结不让使用,如果竞拍失败则解冻,如果竞拍成功则消耗。但实际上,冻结这个功能应该去掉。理由:

  • 竞拍这种场景并不常见
  • 即使有竞拍,冻结可以用“扣减积分”代替,解冻可以用“增加积分”代替。
  • 冻结和过期有诸多矛盾,容易导致设计复杂。比如,冻结期间,积分过期了怎么办?

所以,如果产品提出要有冻结功能,在实现上最好使用“扣减”、“增加”代替。

3、增加、扣减积分的细节需求

如果存在积分过期,通常产品还会要求“优先扣除即将过期的积分”。如果发生退还积分的情况(竞拍失败退还、淘金币下单后退单等),需要增加积分。而且通常要求退还的积分保持原来的过期时间。

4、查询余额的方案

查询余额有两种方式,一种是有一个累积值,直接展示;另一种是只记录行为(增加、扣减、过期),总余额就是行为加起来的值。

5、极限临期消耗

积分将在0点过期,但是用户在23:59:59.999进行了消耗。此时通常认为最后结果消耗、不消耗都是合理的,但需要保证用户的余额不能被扣减为负数,积分消耗和积分过期只能执行1种操作。

最终需求

我们拟定一个最复杂场景的需求:积分可以过期,每笔都有自己的过期时间,优先扣除即将过期的积分,支持退单退换积分这种场景,支持竞拍活动冻结积分(可以自行设计底层逻辑,不一定是冻结)。

挑战分析

可过期的积分系统难点主要在于:

  1. 用户的资产不能简单设置一个数值进行加减,而是需要每笔都记录,因为过期时间不同。
    expire_time.jpg

  2. 增加和扣减不能简单地改变记录的“状态列”,因为增减的数值不一定对等。
    比如签到+11分,购物+20分,然后用户兑换了一个价值15分的商品。
    unequal_inc_and_dec.jpg

方案设计

方案一最小单位

对于用户获得的积分,永远只记录最小单位,比如1分。当用户获得11分时,会产生11条记录。这种方案最简单,而且增减也都很方便,但会产生大量无效数据。这里只是提出思路,不做详述。

方案二 拆分明细表和可用表

为了能够处理任意面额的积分,我们设计两个表:

  • 明细表:“获取、扣减、过期”的明细
  • 可用积分表:用户当前可以使用的积分
    用户的余额,就是可用积分表的累加和。
  1. 用户获得11分、20分,两边都增加积分。用户余额31。
    detail_and_available.jpg

  2. 用户消耗21分,可用表扣减积分,剩余1个条目。用户余额10分。
    11=1+10.jpg

方案二是可行的常见方法,好处是足够简单,但是有2个隐藏的坏处:

  • 频繁删除,容易造成数据库不稳定,以及未知问题出现
  • 一定条件下,最终会演变成方案1,存在大量小积分

方案三 拆分总额列和可用列

还是明细和可用的思路,但是将两个表合并成一个表,通过增加“总额列”和“可用列”来区分。

  1. 获得2个不同过期时间的100分,余额100+100=200分
    total_and_available.jpg

  2. 消费120分:插入一条记录,总额为-120,可用为0,同时按照过期时间扣减积分,第1条
    cost_available.jpg

  3. 第1条积分过期:不做任何操作
    expire_1.jpg

  4. 第2条积分过期:重新执行sum操作,此时余额为0。
    expire_2.jpg

这样避免了方案2的删除问题。但仍然存在大量的小积分条目,比如签到通常都是+1分这种,兑换通常都是-10000分,更新时需要操作的条目太多。

方案四 拆分总额列和可用列 抵扣延迟到过期阶段

为了避免更新缓慢,更新时我们只操作余额(保证积分足够扣减),把抵扣过程延迟到过期阶段。

  1. 获得2个不同过期时间的100分,余额100+100=200分
    total_and_available.jpg

  2. 消费120积分:余额200够扣,简单插入一条负值-120,不做任何积分抵扣动作。
    only_record.jpg

  3. 第1条积分过期:根据expire从前往后抵扣消费,找到-120的记录,抵扣100,结束。重新计算余额,余额100-20=80。
    expire_trigger_cost_1.jpg

  4. 第2条积分过期:根据expire从前往后抵扣消费

  • 顺序找到第1条消费记录,抵扣消费,100-20=剩余80。
  • 找不到任何消费记录了,说明这个80积分是被过期的而不是被消费的。更新余额,此时都为0。
    expire_trigger_cost_2.jpg

其他小问题实现思路

1、如何保证积分获取、过期、消耗都是顺序的?

以用户维度加锁。

2、如何保证优先消耗即将过期的积分?

扣减时根据过期时间从前往后扣

3、余额积分是累计值还是明细加和?

同时使用。
正确数据使用明细加和,sum操作数据库性能消耗不大,但业务一致性会很高,然后将加和值写入累计值。展示给用户看的数据使用累计值,因为查询频繁。

4、如何实现积分过期?

分布式定时任务。
时间不用那么细,比如虽然条目过期是按照秒级别的,但是扫描可以按照5秒、30秒、甚至1分钟级别,即使稍微延迟了一点,也属于“极限临期消耗”的情况,不用太在意,产品也不会在意说如果“7天后过期”的话,“7天30秒过期”就不行。

5、退单怎么处理?

找到退单积分全部扣减记录的原始过期时间,类似“增加”操作插入积分和对应过期时间,如果已经过期了就不插入了,最后重新计算余额。

作者

香蕉微波炉

发布于

2023-04-30

更新于

2023-04-30

许可协议