大营销平台核心流程梳理
第一阶段-抽奖策略¶
活动开始之前会进行策略的装配,在活动工单审批通过后,活动正式开始之前。可以通过定时任务进行触发,也可以通过运营后台手动触发。
默认策略装配¶
核心逻辑:cn.bugstack.domain.strategy.service.armory.StrategyArmoryDispatch#assembleLotteryStrategy
空间换时间的做法,提前计算好抽奖概率分布,存储在Redis中,最后抽奖的时候通过生成的随机值,在空间内定位即可,复杂度为O(1)。
具体逻辑:
- 根据策略id从Redis中查询这个策略配置的明细(strategy_award),查到直接返回,查不到就会从MySQL中查询,表:strategy_award,查询出来后再写入到Redis中。
- 查询出策略id下各个奖品的明细之后,会将这些奖品的库存缓存至Redis中,用于decr扣减库存使用。
- 然后会进行默认装配,所谓默认装配就是全量抽奖概率装配。
- 获取所有奖品中的 最小概率值,再根据最小概率值求得概率值范围,求概率值范围方法如下:
private double convert(double min) {
if (0 == min) return 1D;
String minStr = String.valueOf(min);
// 小数点前
String beginVale = minStr.substring(0, minStr.indexOf("."));
int beginLength = 0;
if (Double.parseDouble(beginVale) > 0) {
beginLength = minStr.substring(0, minStr.indexOf(".")).length();
}
// 小数点后
String endValue = minStr.substring(minStr.indexOf(".") + 1);
int endLength = 0;
if (Double.parseDouble(endValue) > 0) {
endLength = minStr.substring(minStr.indexOf(".") + 1).length();
}
return Math.pow(10, beginLength + endLength);
}
也就是概率值范围是10的倍数:1、10、100、1000。
- 接着生成策略奖品概率表,先生成对应概率范围大小的数组,然后按照各个奖品的中奖概率,放到数组中(奖品概率越高,占位越多),然后对数组进行乱序操作。
比如 0.1、0.02、0.003,需要找到的值是 0.003,基于1找到的最小值,0.003 就可以计算出百分比、千分比的整数值。这里就是1000,那么「概率 * 1000」分别占比100个、20个、3个,总计是123个。后续的抽奖就用123作为随机数的范围值,生成的值100个都是0.1概率的奖品、20个是概率0.02的奖品、最后是3个是0.003的奖品。
- 将策略范围值、策略奖品概率表分别存储至 Redis。(策略范围值的目的是为了生成随机数)。
- 最后生成一个策略范围值内的随机数,从Redis中的策略奖品概率表获取对应key的value,value就是获得的奖品。
权重策略装配¶
核心逻辑:cn.bugstack.domain.strategy.service.armory.StrategyArmoryDispatch#assembleLotteryStrategy
在大营销平台的抽奖子模块中,需要满足用户抽奖N积分后,可中奖范围的设定。也就是说你总共消耗了6000积分抽奖了,那么接下来的抽奖就会圈定奖品范围,不会让用户再抽到过低价值的奖品。那么这就需要我们在设计系统实现的时候,处理下不同策略规则权重的概率装配。
适用于 rule_weight 权重规则配置如:【4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109】。
具体逻辑:
- 先从缓存中查询策略id的策略配置,缓存中查不到就查MySQL,查到之后再写回缓存。
- 查询这个策略是否配置了权重,如果没有的话就直接返回,使用上面的默认策略。
- 如果配置了权重,则从策略规则表(strategy_rule)中查询出具体的权重配置。权重配置类似:4000:102,103,104,105。4000是积分值,102,103,104,105是奖品id。
- 从奖品列表中剔除不在权重配置中的奖品id,然后进行权限策略装配。装配过程和上面的默认装配是一样的,装配好后再把权重配置概率存储至Redis里,这样累计消费积分越高,抽中好奖品的概率就越大。
权重抽奖流程:根据用户id查询用户已消耗的积分,找出最小符合的值,也就是【4500 积分,能找到 4000:102,103,104,105,然后找到Redis对应的key,从中抽奖。
权重以及下面的黑名单是不会扣减库存的!权重和黑名单类似于兑换机制,告知用户满足什么条件后必会中奖范围,也就是说权重消耗积分必中奖。这类奖品,直接扣奖品库存就可以。也是运营提前设置好的预算成本。
对于权重的抽奖范围值,运营是可以拿到每天以及预计到本周,本月,可能消耗掉的库存数量。之后提前准备好奖品数量。而每日对于权重抽奖的奖品量是可以统计的。
抽象抽奖流程¶
整个规则来说,分为抽奖前、抽奖中、抽奖后,三个阶段执行。
在流程实现中,设计出抽奖的前中后置过程,并在每个阶段设计对应的操作规则。当你这样设计以后,你会发现整个抽奖功能实现变得非常灵活好扩展。
如图黄色部分「次数过滤」,这个规则的作用是为任何一个奖品配置抽奖n次后解锁的操作。这个配置就是图中数据库内的配置。
对于抽奖策略的前置规则过滤是顺序一条链的,有一个成功就可以返回。比如;黑名单抽奖、权重人群抽奖、默认抽奖,总之它只能有一种情况,所以这样的流程是适合责任链的。
然而,在我们中后期的规则过滤,不能够一条路走到底,因为他不像是前置规则是必须要全部经过的。我们对于不同的情况就需要有不同的实现,自然就会有分岔路口。所以使用树这一模型就可以了。
抽奖前-责任链模式¶
核心逻辑:cn.bugstack.domain.strategy.service.raffle.DefaultRaffleStrategy#raffleLogicChain
这块使用了责任链模式。
- 如果这个策略没有配置策略规则,则使用默认的规则,默认规则就是上面的默认策略,在概率范围内生成随机数,然后抽奖。
- 如果配置了规则,则按照配置顺序装填用户配置的责任链;rule_blacklist、rule_weight,最后再装填默认规则。目前规则有黑名单规则、权重规则、默认规则,装填好后写入本地缓存。
- 然后进行责任链执行。
- 黑名单规则类似:101:user001,user002,user003。意思就是如果用户在:user001,user002,user003 中。则直接返回奖品 101。如果黑名单规则不命中,则继续执行责任链的下一个节点。
- 权重规则会查询用户活动账户(raffle_activity_account),查出用户已消耗的积分,然后进行抽奖。
- 权重规则和黑名单规则不扣减库存!如果前面的规则都没有返回奖品,则走最后的默认规则(最后一个节点)。
- 至此,抽奖前流程结束。
抽奖中-规则树¶
如果命中了前置规则(黑名单、权重),则直接返回奖品,不会进行规则树过滤。
核心逻辑:cn.bugstack.domain.strategy.service.AbstractRaffleStrategy#raffleLogicTree
具体逻辑:
- 根据策略id和奖品id查询规则模型,根据规则模型生成规则树,先查询缓存,缓存查不到就查MySQL。涉及的表:rule_tree,rule_tree_node,rule_tree_node_line。然后进行规则树构建,构建好之后会存储至Redis中。
- 然后进行规则树的决策。目前规则树有三种节点类型:次数锁节点(用户抽奖次数大于限定值则放行),兜底奖励节点,库存扣减节点。
-
如果次数锁节点放行了,则接下来走库存处理节点。库存扣减节点会扣减库存,在缓存中扣减(之前已经加载过了),使用的是滑块锁:strategy_award_count_key_策略id_奖品id_库存剩余,比如:strategy_award_count_key_1001_1_99,strategy_award_count_key_1001_1_98,过期时间设置为活动结束时间+1天。
-
库存扣减是在Redis中扣减的,并不会实时同步到MySQL中,而是先写入延迟队列,延迟消费更新数据库记录(延迟3秒)。延迟队列使用的是 Redission 的 DelayedQueue。 逻辑:cn.bugstack.infrastructure.adapter.repository.StrategyRepository#awardStockConsumeSendQueue。
延迟队列消费逻辑在:cn.bugstack.trigger.job.UpdateAwardStockJob#exec
消费逻辑就是从队列里面消费数据,更新数据库,这样可以缓解数据库压力。
抽奖后-兜底¶
承接上面的规则树,如果库存不足,或者次数锁校验不通过,则放行走兜底节点,发放兜底奖品。兜底奖品也会写入延迟队列进行库存扣减。
第二阶段-抽奖活动¶
大营销的抽奖活动其实是抽象为商品购买模型,抽奖策略是具体的商品,商品有多少个,用户怎么消费,我们是抽象为一种下单流程。
我们可以把抽奖的行为理解为一个下单过程,用户参与抽奖,也等价于商品下单。只不过这个商品的 sku 是活动信息。
活动装配-数据预热¶
活动开始之前会进行活动数据的装配,做数据预热,把活动配置的对应的 sku 一起装配。
- 一个活动会有多个 sku(raffle_activity_sku),遍历各个sku,先把活动下的各个sku的剩余库存,加载到缓存中。然后获取 sku 下,一个人可参与多少次活动(总、日、月)(raffle_activity_count),加载到缓存中。
- 然后根据活动id查询活动(raffle_activity),把活动配置加载到缓存中。
- 根据活动id查询对应的策略id,进行策略装配(接第一阶段)。
- 至此,活动装配完成,这步就是把一些相关信息加载到缓存中,进行一些数据的预热。
创建兑换商品sku订单¶
在【打卡、签到、分享、对话、积分兑换】等行为动作下,创建出活动订单,给用户的活动账户【日、月】充值可用的抽奖次数。
对于用户可获得的抽奖次数,比如首次进来就有一次,则是依赖于运营配置的动作,在前端页面上。用户点击后,可以获得一次抽奖次数。
核心逻辑:cn.bugstack.domain.activity.service.quota.AbstractRaffleActivityAccountQuota#createOrder
创建sku订单参数:用户id,sku,outBusinessNo(幂等业务单号),orderTradeType(订单交易类型)。
outBusinessNo 是外部传进来的,orderTradeType 标识订单类型如:积分兑换、返利奖品。
具体流程:
- 首先会进行一些参数校验如:sku是否为空,用户id是否为空等。
- 如果订单类型属于积分兑换,则查询活动下单记录表(raffle_activity_order),近一个月是否有未支付订单,有则直接返回此订单,没有则继续订单创建流程。
- 根据 sku 从之前已预热的缓存中查询基础信息:活动、次数等。
- 接下来进行账户额度校验,如果额度不足以支付则抛个异常。
- 通过校验则进行活动动作责任链执行。这块使用的是责任链模式。目前有两个节点:基础节点和商品库存节点。 基础节点就是活动相关的基础信息校验。状态、日期。以及把库存规则校验下。如果库存不足,也就不需要走到第二个责任链了。这里的错误信息会直接抛业务定义的异常。让外部反馈给前端做展示即可。 商品库存节点会去扣减sku库存(在缓存中扣减,之前已经预热过了),扣减使用的是Redis滑块锁,置加锁时间为活动到期 + 延迟1天,如果sku库存没了会发送MQ消息,更新数据库库存(最终一致性)。核心逻辑:cn.bugstack.infrastructure.adapter.repository.ActivityRepository#subtractionActivitySkuStock。扣减成功则写入延迟队列,延迟消费更新库存记录。
- 构建订单聚合对象。
- 如果订单类型需要支付,则订单状态更新为待支付,创建交易订单,插入表(raffle_activity_order),订单更新完成状态逻辑在:cn.bugstack.domain.credit.service.ICreditAdjustService#createOrder; 如果订单类型不需要支付则更新订单为完成状态,插入订单,同时更新用户的总、月、日库存(raffle_activity_account、raffle_activity_account_month、raffle_activity_account_day)。
- 返回订单信息。
创建抽奖单¶
核心逻辑:cn.bugstack.domain.activity.service.IRaffleActivityPartakeService#createOrder。
参与活动会先创建抽奖单。用户参与抽奖活动,扣减活动账户库存,产生抽奖单。如存在未被使用的抽奖单则直接返回已存在的抽奖单。
- 查询活动信息,之后校验活动状态(活动时间是否结束,活动是否开启等),查询是否有未被使用的活动参与订单记录(user_raffle_order),有则直接返回。
- 查询用户总账户额度(raffle_activity_account),看用户总活动次数是不是达到上限了。查看月账户额度(raffle_activity_account_month),看用户当月参与次数是不是达到上限了。查看日账户额度(raffle_activity_account_day),看用户当日参与次数是不是达到上限了。如果不存在月账户或日账户,那月账户和日账户额度就从总账户中取。
- 接下来构建订单,会生成一个订单id,使用lang3工具类:RandomStringUtils.randomNumeric(12)。
- 然后保存订单聚合对象。更新用户总账户额度(减去1),创建或更新月账户,存在则更新,不存在则插入,再更新用户总账户中月镜像库存(镜像记录,是为了方便查询统计的)。日账户同理。最后写入用户参与订单(user_raffle_order)。这里使用了编程式事务保证了这几个操作在一个事务中(同库同连接可以保证)。 月账户、日账户,是随着用户每次参与活动自动创建的。所以根据是否有已经有账户选择更新和创建操作。如果这里出现创建的时候已经存在了数据,则会抛出一个主键冲突异常。也就是一个日账户的用户ID、活动ID、和当日,组装了一个唯一索引,来仿重。
- 返回订单信息。
写入中奖记录和任务补偿发送MQ¶
核心逻辑:cn.bugstack.domain.award.service.IAwardService#saveUserAwardRecord
- 构建 Task 任务对象,构建聚合对象。
- 新增用户中奖记录( user_award_record),新增 task 对象(task),更新抽奖单(user_raffle_order)状态为 used。
- 发生 MQ 消息,更新 task 状态为 completed,发生失败则更新为 fail。 用户发奖监听逻辑:cn.bugstack.trigger.listener.SendAwardCustomer。 task 补偿逻辑:cn.bugstack.trigger.job.SendMessageTaskJob。 发奖的时候会根据奖品配置,随机发放积分值。
活动抽奖接口¶
核心逻辑:cn.bugstack.trigger.api.IRaffleActivityService#draw
这个接口做了限流保护,自定义限流注解,以用户ID作为拦截,限制用户访问次数,超过限制,还访问的,扔到黑名单里24小时,防止刷接口。
逻辑:cn.bugstack.aop.RateLimiterAOP
具体逻辑:
- 首先进行基础参数校验。
- 检查降级开关是否打开,打开则直接降级返回。
- 创建抽奖单(见创建抽奖单流程)。
- 抽奖策略 - 执行抽奖(见抽象抽奖流程)。
- 写入中奖记录(写入中奖记录和任务补偿发送MQ流程)。
- 返回结果。
用户行为返利入账¶
用户的行为动作返利,是一种日常的活动类型。比如;你在某个平台创建了新账号,就会给你发一堆的开户优惠券,这些都是日常的返利活动。
日常的返利会根据用户所完成的行为动作来触达,这包括;打卡、签到、连签、支付、开户、交易、信贷、拉新等各类的动作。我们本节中所涉及的,主要是日常的日历签到行为,并也从功能实现上扩展出各类型的任务,以便于后续扩展。
核心逻辑:cn.bugstack.domain.rebate.service.IBehaviorRebateService。
具体逻辑:
- 创建行为动作的入账订单,简单来说就是返利是哪个行为动作引起的。会先查询返利配置(daily_behavior_rebate),一个行为可能有多个返利配置。
- 构建聚合对象,这里会生成业务id(签到则是日期字符串,支付则是外部的业务ID,查询用户行为返利流水订单表的时候会根据业务id查询),创建 task 对象。
- 写入用户行为返利流水订单表(user_behavior_rebate_order),写入的时候会有一个 order_id,使用lang3工具类:RandomStringUtils.randomNumeric(12) 生成。task 对象写入任务表,发送MQ消息,更新任务表状态。
上面发送的MQ消息,监听逻辑在:cn.bugstack.trigger.listener.RebateMessageCustomer#listener
会根据不同的入账奖励,创建不同类型的订单。 sku就创建sku增加订单:cn.bugstack.domain.activity.service.IRaffleActivityAccountQuotaService#createOrder 积分的话就创建积分入账订单:cn.bugstack.domain.credit.service.ICreditAdjustService#createOrder 创建订单的时候有一个 outBusinessNo,幂等业务单号,外部谁充值谁透传,这样来保证幂等(多次调用也能确保结果唯一,不会多次充值)。
第三阶段-用户积分¶
在积分的获取和使用上,可以看到3个行为动作来增加和使用积分。
- 抽奖中有随机积分,抽中后可以给用户增加积分。注意黑名单一般会发放一个指定范围的很小的积分值。
- 用户行为动作返利,当用户在抽奖系统中日历📅签到,以及提供出让外部对接的接口,也可以给用户增加积分。
- 最后就是积分消耗,目前这里的积分消耗主要是兑换抽奖资格,也就是和活动 sku 进行兑换。这部分会产生订单。
创建增加积分额度订单¶
-
判断交易类型(逆向交易减积分,正向交易加积分),如果是逆向交易要校验用户额度是否足够,不足则报错:用户积分账户额度不足。
-
创建账户积分实体;创建账户订单实体;构建消息任务对象;构建交易聚合对象。
-
保存积分交易订单:
-
先更新用户积分账户(user_credit_account)。
- 保存账户订单(user_credit_order)。
- 写入 task 任务表,发送 MQ 消息,更新任务表状态。
监听逻辑:cn.bugstack.trigger.listener.CreditAdjustSuccessCustomer#listener
订单出货 - 积分充值¶
监听积分账户调整成功消息,进行交易商品发货
- 查询活动订单(raffle_activity_order)。获取总、月、日账户信息。
- 更新订单状态,从待支付更新为完成。
- 更新总、月、日账户。
积分兑换商品¶
- 创建兑换商品sku订单。
- 支付兑换商品,创建增加积分额度订单