文章目录
开篇介绍:将按照渐进的顺序逐步编写一个完整的抽奖项目。
规则树模式
对于上一步已经经过责任链抽奖后的结果,有以下几种情况:
- 被黑名单拦截,那么无需操作返回一个配置好的兜底奖品数据;
- 被权重规则拦截,也就是当前用户的积分满足了权重规则中的配置,且经过权重规则获得了一个奖品id;
- 没被两面两种规则拦截,走默认抽奖流程返回一个奖品id
既然这里抽奖都结束了那为什么还需要抽奖中过滤呢?
首先每个奖品虽然是抽到了上一步返回给你的id值
但是奖品也可以配置对应的规则
例如,
- 当前奖品id对应的规则为必须抽经6次之后才能解锁
- 当前奖品抽到之后但是奖品库存不足
- 一个兜底奖品
因此需要基于上一步获得奖品id的基础上进一步进行过滤判断。
规则树过滤
这里为什么又采用了树模型进行过滤呢?首先根据不同的策略走向的分支不同,
例如 我先去过滤判断当前用户是否满足抽奖次数如果满足 那么后续就进行扣减库存的动作;如果不满足就走兜底。
同时扣减库存也可以分为如果满足则返回奖品,不满足走兜底的状态,因此可以用树的结构来进行抽奖中过滤。
库表结构
奖品中的规则
规则树 rule_tree
rule_tree_node 规则树节点
配置了当前规则树中的校验状态有几种
rule_tree_node_line 规则树具体行为分支
这张表中就具体配置了树的形状
例如如果是rule_lock 满足的话 走 库存扣减的逻辑。
rule_lock如果不满足则走默认兜底的逻辑。
类结构
ILogicTreeNode 接口
类似于责任链中的接口,但是只包含一个logic判断的逻辑,因为树的规律已经定义在数据库了。
public interface ILogicTreeNode {
DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId,String ruleValue);
}
RuleLockLogicTreeNode 次数锁
用来判断经过责任链之后获得的奖品id是否满足这个奖品的次数锁设置,例如设置为5,就代表需要满足抽奖5次才能得到当前奖品,如果不满足5次根据树的规则需要走兜底
private Long userRaffleCount = 10L;
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId,String ruleValue) {
log.info("规则树-次数锁 userId:{} strategyId:{} ",userId,strategyId);
long raffleCount = 0L;
try {
raffleCount = Long.parseLong(ruleValue);
}catch (Exception e){
throw new RuntimeException("规则过滤-次数锁异常 ruleValue: " + ruleValue + "配置不正确");
}
// 放行
if(userRaffleCount >= raffleCount){
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
.build();
}
// 被次数锁拦截了走兜底奖励.
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.build();
}
RuleStockLogicTreeNode 扣减库存
如果库存满足则放行,如果库存没有剩余扣减失败则拦截执行兜底奖励
@Slf4j
@Component("rule_stock")
public class RuleStockLogicTreeNode implements ILogicTreeNode {
@Resource
private IStrategyDispatch strategyDispatch;
@Resource
private IStrategyRepository repository;
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId,String ruleValue) {
log.info("规则过滤-库存扣减 userId: {} strategyId: {} awardId:{}",userId,strategyId,awardId);
// 进行库存扣减.
Boolean status = strategyDispatch.subtractionAwardStock(strategyId, awardId);
if(status){
// 发送异步队列消息. 扣减数据库库存
repository.awardStockConsumeSendQueue(
StrategyAwardStockKeyVO.builder()
.strategyId(strategyId)
.awardId(awardId)
.build());
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
.strategyAwardVO(DefaultTreeFactory.StrategyAwardVO.builder()
.awardId(awardId)
.awardRuleValue("")
.build()
)
.build();
}
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.build();
}
}
RuleLuckAwardLogicTreeNode 兜底幸运奖
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId,String ruleValue) {
String[] split = ruleValue.split(Constants.COLON);
if(split.length == 0){
throw new RuntimeException("兜底奖品未配置 " + ruleValue);
}
// 兜底奖励配置
Integer luckAwardId = Integer.valueOf(split[0]);
String awardRuleValue = split.length > 1 ? split[1] : "";
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.strategyAwardVO(DefaultTreeFactory.StrategyAwardVO.builder()
.awardId(luckAwardId)
.awardRuleValue(awardRuleValue)
.build())
.build();
}
DefaultTreeFactory同样通过工厂来管理
private final Map<String, ILogicTreeNode> logicTreeNodeMap;
public DefaultTreeFactory(Map<String, ILogicTreeNode> logicTreeNodeMap) {
this.logicTreeNodeMap = logicTreeNodeMap;
}
区别于责任链因为树的配置信息是在数据库中的因此我们使用模板引擎的方式配置工厂的引擎
// 这里的ruleTreeVO就是从数据库中读取的树模型
// 将当前树对应的规则引擎创建提供出去..
public IDecisionTreeEngine openLogicTree(RuleTreeVO ruleTreeVO){
return new DecisionTreeEngine(logicTreeNodeMap,ruleTreeVO);
}
DecisionTreeEngine 规则树引擎
规则树引擎拿到树的结构之后进行逐个判断处理
private final Map<String, ILogicTreeNode> logicTreeNodeGroup;
private final RuleTreeVO ruleTreeVO;
public DecisionTreeEngine(Map<String, ILogicTreeNode> logicTreeNodeGroup, RuleTreeVO ruleTreeVO) {
this.logicTreeNodeGroup = logicTreeNodeGroup;
this.ruleTreeVO = ruleTreeVO;
}
// 执行过滤流程
@Override
public DefaultTreeFactory.StrategyAwardVO process(String userId, Long strategyId, Integer awardId) {
DefaultTreeFactory.StrategyAwardVO strategyAwardData = null;
// 获取基础信息
String nextNode = ruleTreeVO.getTreeRootRuleNode();
Map<String, RuleTreeNodeVO> treeNodeMap = ruleTreeVO.getTreeNodeMap();
// 拿到当前树节点对应的node
RuleTreeNodeVO ruleTreeNode = treeNodeMap.get(nextNode);
// 当规则树走完了 就停止
while(null != nextNode) {
// 拿到当前节点对应的规则树节点
ILogicTreeNode iLogicTreeNode = logicTreeNodeGroup.get(ruleTreeNode.getRuleKey());
// 这里就限定了当前节点规则的次数
String ruleValue = ruleTreeNode.getRuleValue();
DefaultTreeFactory.TreeActionEntity logicEntity = iLogicTreeNode.logic(userId, strategyId, awardId,ruleValue);
// 判断经过当前节点之后是放行还是拦截 ALLOW CHECK_OUT
RuleLogicCheckTypeVO checkType = logicEntity.getRuleLogicCheckType();
strategyAwardData = logicEntity.getStrategyAwardVO();
// 找到当前节点对应的下一个节点
nextNode = nextNode(checkType.getCode(),ruleTreeNode.getTreeNodeLineVOList());
ruleTreeNode = treeNodeMap.get(nextNode);
}
return strategyAwardData;
}
至此规则树过滤也已完成。
使用
@Override
public DefaultTreeFactory.StrategyAwardVO raffleLogicTree(String userId, Long strategyId, Integer awardId) {
// 树规则引擎执行判断
StrategyAwardRuleModelVO strategyAwardRuleModelVO = repository.queryStrategyAwardRuleModel(strategyId, awardId);
// 为空证明当前奖品没有配置对应的规则
log.info("当前奖品对应的规则模型: {}",strategyAwardRuleModelVO.getRuleModels());
if(StringUtils.isBlank(strategyAwardRuleModelVO.getRuleModels())){
return DefaultTreeFactory.StrategyAwardVO.builder()
.awardId(awardId)
.build();
}
// 不为空查询出当前奖品对应的规则.
RuleTreeVO ruleTreeVO = repository.queryRuleTreeVOByTreeId(strategyAwardRuleModelVO.getRuleModels());
if(null == ruleTreeVO){
throw new RuntimeException("存在抽奖规则模型key,但是数据库中未见rule_tree配置");
}
// 获得这棵树对应的执行引擎
IDecisionTreeEngine engine = treeFactory.openLogicTree(ruleTreeVO);
// 执行规则树的判断
return engine.process(userId,strategyId,awardId);
}
根据数据库信息构造树的过程
@Override
public RuleTreeVO queryRuleTreeVOByTreeId(String treeId) {
String cacheKey = Constants.RedisKey.RULE_TREE_VO_KEY + treeId;
RuleTree ruleTree = ruleTreeDao.queryRuleTreeByTreeId(treeId);
// 全部的节点
List<RuleTreeNode> ruleTreeNodes = ruleTreeNodeDao.queryRuleTreeNodeListByTreeId(treeId);
List<RuleTreeNodeLine> ruleTreeNodeLines = ruleTreeNodeLineDao.queryRuleTreeNodeLineListByTreeId(treeId);
// 找到每个节点后续的line集合
Map<String, List<RuleTreeNodeLineVO>> ruleTreeNodeLineMap = new HashMap<>();
for (RuleTreeNodeLine ruleTreeNodeLine : ruleTreeNodeLines) {
// 当前对应的TreeNodeLine
RuleTreeNodeLineVO ruleTreeNodeLineVO = RuleTreeNodeLineVO.builder()
.treeId(ruleTreeNodeLine.getTreeId())
.ruleNodeFrom(ruleTreeNodeLine.getRuleNodeFrom())
.ruleNodeTo(ruleTreeNodeLine.getRuleNodeTo())
.ruleLimitType(RuleLimitTypeVO.valueOf(ruleTreeNodeLine.getRuleLimitType()))
.ruleLimitValue(RuleLogicCheckTypeVO.valueOf(ruleTreeNodeLine.getRuleLimitValue()))
.build();
List<RuleTreeNodeLineVO> ruleTreeNodeLineVOList = ruleTreeNodeLineMap.computeIfAbsent(ruleTreeNodeLine.getRuleNodeFrom(), k -> new ArrayList<>());
ruleTreeNodeLineVOList.add(ruleTreeNodeLineVO);
}
// 构造出一个Map : key:节点的key,value是RuleTreeNodeVO
Map<String, RuleTreeNodeVO> treeNodeMap = new HashMap<>();
for (RuleTreeNode ruleTreeNode : ruleTreeNodes) {
RuleTreeNodeVO ruleTreeNodeVO = RuleTreeNodeVO.builder()
.treeId(ruleTreeNode.getTreeId())
.ruleKey(ruleTreeNode.getRuleKey())
.ruleDesc(ruleTreeNode.getRuleDesc())
.ruleValue(ruleTreeNode.getRuleValue())
.treeNodeLineVOList(ruleTreeNodeLineMap.get(ruleTreeNode.getRuleKey()))
.build();
treeNodeMap.put(ruleTreeNode.getRuleKey(), ruleTreeNodeVO);
}
RuleTreeVO ruleTreeVODB = RuleTreeVO.builder()
.treeId(ruleTree.getTreeId())
.treeName(ruleTree.getTreeName())
.treeDesc(ruleTree.getTreeDesc())
.treeRootRuleNode(ruleTree.getTreeRootRuleKey())
.treeNodeMap(treeNodeMap)
.build();
redisService.setValue(cacheKey, ruleTreeVODB);
return ruleTreeVODB;
}
扣减库存之后不超卖问题
UpdateAwardStockJob 类 扣减库存
用于处理在规则树模块向redis的topic交换机发送数据之后执行扣减库存的处理
@Resource
private IRaffleStock raffleStock;
// 执行定时任务定期去刷队列中的数据到mysql中 意思是扣减就发MQ消息,然后等一段事件MQ异步改数据库.
@Scheduled(cron = "0/5 * * * * ?")
public void exec(){
try {
StrategyAwardStockKeyVO stockKeyVO = raffleStock.takeQueueValue();
if(null == stockKeyVO) return;
log.info("定时任务,更新奖品消耗库存: {}",stockKeyVO.getAwardId());
raffleStock.updateStrategyAwardStock(stockKeyVO.getStrategyId(), stockKeyVO.getAwardId());
}catch (Exception e){
log.error("定时任务 更新奖品消耗库存失败",e);
}
}
这里并没有使用监听等其他方式,而是通过一个定时任务轮询去扫描队列中的消息,如果存在则取出去执行扣减库存的逻辑。