抽奖系统设计

抽奖系统设计

1.背景介绍

image-20240826103849215

2.库表设计

2.1奖品表

image-20240826104148627

2.2活动表

image-20240826104256316

2.3策略表

image-20240826104404849

2.4策略奖品表

image-20240826104704108

2.5策略规则表

image-20240826104530273

2.6规则树表

image-20240826104811883

2.7规则树节点表

image-20240826105038501

2.8规则树连接表

image-20240826105105685

3.接口设计

image-20240826105137847

4.功能设计

4.1抽奖策略装配接口

image-20240826105249868

image-20240826110220542

image-20240826110248496

4.2抽奖核心接口

image-20240826111315071

4.2.1模板设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public abstract class AbstractRaffleStrategy implements IRaffleStrategy {

// 策略仓储服务 -> domain层像一个大厨,仓储层提供米面粮油
protected IStrategyRepository repository;
// 策略调度服务 -> 只负责抽奖处理,通过新增接口的方式,隔离职责,不需要使用方关心或者调用抽奖的初始化
protected IStrategyDispatch strategyDispatch;
// 抽奖的责任链 -> 从抽奖的规则中,解耦出前置规则为责任链处理
protected final DefaultChainFactory defaultChainFactory;
// 抽奖的决策树 -> 负责抽奖中到抽奖后的规则过滤,如抽奖到A奖品ID,之后要做次数的判断和库存的扣减等。
protected final DefaultTreeFactory defaultTreeFactory;

public AbstractRaffleStrategy(IStrategyRepository repository, IStrategyDispatch strategyDispatch, DefaultChainFactory defaultChainFactory, DefaultTreeFactory defaultTreeFactory) {
this.repository = repository;
this.strategyDispatch = strategyDispatch;
this.defaultChainFactory = defaultChainFactory;
this.defaultTreeFactory = defaultTreeFactory;
}

@Override
public RaffleAwardEntity performRaffle(RaffleFactorEntity raffleFactorEntity) {
// 1. 参数校验
String userId = raffleFactorEntity.getUserId();
Long strategyId = raffleFactorEntity.getStrategyId();
if (null == strategyId || StringUtils.isBlank(userId)) {
throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo());
}

// 2. 责任链抽奖计算【这步拿到的是初步的抽奖ID,之后需要根据ID处理抽奖】注意;黑名单、权重等非默认抽奖的直接返回抽奖结果
DefaultChainFactory.StrategyAwardVO chainStrategyAwardVO = raffleLogicChain(userId, strategyId);
log.info("抽奖策略计算-责任链 {} {} {} {}", userId, strategyId, chainStrategyAwardVO.getAwardId(), chainStrategyAwardVO.getLogicModel());
if (!DefaultChainFactory.LogicModel.RULE_DEFAULT.getCode().equals(chainStrategyAwardVO.getLogicModel())) {
return buildRaffleAwardEntity(strategyId, chainStrategyAwardVO.getAwardId(), null);
}

// 3. 规则树抽奖过滤【奖品ID,会根据抽奖次数判断、库存判断、兜底兜里返回最终的可获得奖品信息】
DefaultTreeFactory.StrategyAwardVO treeStrategyAwardVO = raffleLogicTree(userId, strategyId, chainStrategyAwardVO.getAwardId());
log.info("抽奖策略计算-规则树 {} {} {} {}", userId, strategyId, treeStrategyAwardVO.getAwardId(), treeStrategyAwardVO.getAwardRuleValue());

// 4. 返回抽奖结果
return buildRaffleAwardEntity(strategyId, treeStrategyAwardVO.getAwardId(), treeStrategyAwardVO.getAwardRuleValue());
}

private RaffleAwardEntity buildRaffleAwardEntity(Long strategyId, Integer awardId, String awardConfig) {
StrategyAwardEntity strategyAward = repository.queryStrategyAwardEntity(strategyId, awardId);
return RaffleAwardEntity.builder()
.awardId(awardId)
.awardConfig(awardConfig)
.sort(strategyAward.getSort())
.build();
}

/**
* 抽奖计算,责任链抽象方法
*
* @param userId 用户ID
* @param strategyId 策略ID
* @return 奖品ID
*/
public abstract DefaultChainFactory.StrategyAwardVO raffleLogicChain(String userId, Long strategyId);

/**
* 抽奖结果过滤,决策树抽象方法
*
* @param userId 用户ID
* @param strategyId 策略ID
* @param awardId 奖品ID
* @return 过滤结果【奖品ID,会根据抽奖次数判断、库存判断、兜底兜里返回最终的可获得奖品信息】
*/
public abstract DefaultTreeFactory.StrategyAwardVO raffleLogicTree(String userId, Long strategyId, Integer awardId);

}

4.2.2责任链设计模式

image-20240826112002663

image-20240826112125658

image-20240826112159034

4.2.3工厂设计模式

image-20240826112401213

image-20240826112527533

image-20240826113312339

image-20240826113401054

4.2.4组合设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DecisionTreeEngine implements IDecisionTreeEngine {

private final Map<String, ILogicTreeNode> logicTreeNodeGroup;//Spring注入

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();

// 获取起始节点「根节点记录了第一个要执行的规则」
RuleTreeNodeVO ruleTreeNode = treeNodeMap.get(nextNode);
while (null != nextNode) {
// 获取决策节点
ILogicTreeNode logicTreeNode = logicTreeNodeGroup.get(ruleTreeNode.getRuleKey());
String ruleValue = ruleTreeNode.getRuleValue();

// 决策节点计算
DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategyId, awardId, ruleValue);
RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
strategyAwardData = logicEntity.getStrategyAwardVO();
log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());

// 获取下个节点
nextNode = nextNode(ruleLogicCheckTypeVO.getCode(), ruleTreeNode.getTreeNodeLineVOList());
ruleTreeNode = treeNodeMap.get(nextNode);
}

// 返回最终结果
return strategyAwardData;
}

public String nextNode(String matterValue, List<RuleTreeNodeLineVO> treeNodeLineVOList) {
if (null == treeNodeLineVOList || treeNodeLineVOList.isEmpty()) return null;
for (RuleTreeNodeLineVO nodeLine : treeNodeLineVOList) {
if (decisionLogic(matterValue, nodeLine)) {
return nodeLine.getRuleNodeTo();
}
}
return null;
}

public boolean decisionLogic(String matterValue, RuleTreeNodeLineVO nodeLine) {
switch (nodeLine.getRuleLimitType()) {
case EQUAL:
return matterValue.equals(nodeLine.getRuleLimitValue().getCode());
// 以下规则暂时不需要实现
case GT:
case LT:
case GE:
case LE:
default:
return false;
}
}

}

image-20240826115047431

image-20240826115147781

image-20240826115317251

4.2.5库存扣减逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description 更新奖品库存任务;为了不让更新库存的压力打到数据库中,这里采用了redis更新缓存库存,异步队列更新数据库,数据库表最终一致即可。
* @create 2024-02-09 12:13
*/
@Slf4j
@Component()
public class UpdateAwardStockJob {

@Resource
private IRaffleStock raffleStock;

@Scheduled(cron = "0/5 * * * * ?")
public void exec() {
try {
log.info("定时任务,更新奖品消耗库存【延迟队列获取,降低对数据库的更新频次,不要产生竞争】");
StrategyAwardStockKeyVO strategyAwardStockKeyVO = raffleStock.takeQueueValue();
if (null == strategyAwardStockKeyVO) return;
log.info("定时任务,更新奖品消耗库存 strategyId:{} awardId:{}", strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
raffleStock.updateStrategyAwardStock(strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
} catch (Exception e) {
log.error("定时任务,更新奖品消耗库存失败", e);
}
}

}

image-20240826115848362