一、本章诉求
1. 这一节整体在解决什么问题
第八节的时候,规则树虽然已经有了:
- 树模型
- 节点接口
- 节点实现
- 决策引擎
但它还只是"能单独跑一个测试"的状态,并没有真正接入抽奖主流程。
同时,第八节的抽奖主流程里,前置规则和规则树之间还没有形成完整串联:
- 前置责任链拿到一个初步奖品
- 后面的树结构只是设计好了
- 中间还有很多旧的过滤器结构残留
第九节干的事,就是把这些碎片拼完整:
- 前置规则继续用责任链
- 责任链返回的不再只是 awardId,而是"带来源信息的结果对象"
- 后续规则不再走旧的过滤器模式,而是正式交给规则树
- 规则树配置不再手工在测试里构造,而是从数据库读取
- 主流程通过模板方法把这两段规则逻辑正式串起来
所以这一节不是"又加了一个模式",而是把前面几节铺的结构真正落成一条完整业务链。
二、什么是模板方法
模板方法是一种设计模式,你可以把它理解成:
先在父类里把一整套业务流程的执行顺序固定下来,再把其中某些具体步骤留给子类去实现。
也就是说,它解决的是:
流程骨架不变,但某些环节允许扩展
最简单的理解
比如抽奖流程,不管怎么变化,大体步骤都差不多:
- 校验参数
- 做前置规则处理
- 计算抽奖结果
- 做中奖后规则过滤
- 返回最终结果
这个"先做什么,后做什么"的顺序,其实是稳定的。
那就可以把这条固定顺序写在父类里。
但具体怎么做前置规则、怎么做中奖后过滤,不同实现可能不一样。
这些变化的部分,就交给子类去实现,这就是模板方法。
三、功能实现
1. 库表设计

- 设计3个表; 树根 - rule_tree、节点 - rule_tree_node、连线 - rule_tree_node_line 通过这3个表构建出一颗规则树。
2. 工程结构

- rule 规则部分,保留;责任链、规则树,去掉之前的 filter 过滤器。
- 在 AbstractRaffleStrategy 抽象类,串联调用流程。先是责任链,后是规则树。责任链处理的是不同的抽奖【黑名单、权重、默认】,处理完的抽奖结果,如果是默认抽奖则需要进行库存、次数等校验,并给出最终发奖结果。
- 本节还包括了根据上一节实现的规则树模型,设计的库表结构。并实现出仓储数据查询的操作。
3.抽奖模板主流程正式升级
java
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description 抽奖策略抽象类,定义抽奖的标准流程
* @create 2024-01-06 09:26
*/
@Slf4j
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 RaffleAwardEntity.builder()
.awardId(chainStrategyAwardVO.getAwardId())
.build();
}
// 3. 规则树抽奖过滤【奖品ID,会根据抽奖次数判断、库存判断、兜底兜里返回最终的可获得奖品信息】
DefaultTreeFactory.StrategyAwardVO treeStrategyAwardVO = raffleLogicTree(userId, strategyId, chainStrategyAwardVO.getAwardId());
log.info("抽奖策略计算-规则树 {} {} {} {}", userId, strategyId, treeStrategyAwardVO.getAwardId(), treeStrategyAwardVO.getAwardRuleValue());
// 4. 返回抽奖结果
return RaffleAwardEntity.builder()
.awardId(treeStrategyAwardVO.getAwardId())
.awardConfig(treeStrategyAwardVO.getAwardRuleValue())
.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);
}
第八节时,它的流程大致是:
- 参数校验
- 走责任链拿 awardId
- 再继续做中置规则过滤
- 如果拦截就走兜底描述
- 否则返回奖品
到了第九节,主流程完全重组了。
现在的流程是:
- 参数校验
- 调用 raffleLogicChain(userId, strategyId) 做责任链抽奖
- 如果责任链结果不是默认抽奖,而是黑名单或权重规则直接接管,那就直接返回结果
- 如果责任链走的是默认抽奖,再继续调用 raffleLogicTree(userId, strategyId, awardId) 做规则树过滤
- 最终返回规则树产出的奖品信息
这个变化很重要,因为它把主流程清晰拆成了两段:
- 责任链负责前置规则与初步抽奖
- 规则树负责中奖后的进一步规则过滤和结果修正
另外,这个类还新增了一个依赖:
- DefaultTreeFactory
并新增两个抽象方法:
- raffleLogicChain(...)
- raffleLogicTree(...)
4.默认抽奖策略正式实现"两段式规则流"
这个类在这一节里从"过滤器实现类"进一步演进成了"流程编排实现类"。
它现在的职责很明确:
- raffleLogicChain(...):
- 从责任链工厂拿责任链
- 调用 logic(...)
- 返回责任链结果对象
- raffleLogicTree(...):
- 根据 strategyId + awardId 查询当前奖品绑定的规则模型
- 如果没有规则模型,直接返回当前奖品
- 如果有规则模型,就根据模型值查询整棵规则树
- 再通过规则树工厂创建决策引擎
- 最终执行规则树并返回结果
5. 责任链返回值从 awardId 升级成结果对象
java
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description 抽奖策略规则责任链接口
* @create 2024-01-20 09:40
*/
public interface ILogicChain extends ILogicChainArmory{
/**
* 责任链接口
*
* @param userId 用户ID
* @param strategyId 策略ID
* @return 奖品对象
*/
DefaultChainFactory.StrategyAwardVO logic(String userId, Long strategyId);
}
之前责任链接口是:
- 输入:userId, strategyId
- 输出:Integer awardId
现在改成:
- 输出:DefaultChainFactory.StrategyAwardVO
这个 StrategyAwardVO 里多了两个字段:
- awardId
- logicModel
这意味着责任链不只告诉主流程"抽到了哪个奖",还会告诉主流程:
这个奖是通过哪种规则路径拿到的。
为什么这一步重要?
因为主流程现在要判断:
- 如果是黑名单接管
- 如果是权重接管
- 这些都属于"规则已经替你决定完了"
- 不需要再进入规则树
只有当责任链返回的是 RULE_DEFAULT 时,才说明这只是一次普通随机抽奖,还需要继续进入规则树阶段。
所以 logicModel 的加入,本质上是为主流程提供了"阶段分流依据"。
6.三个责任链节点同步适配

这三个责任链节点都做了同一类改造:
- 以前直接返回 awardId
- 现在返回 StrategyAwardVO
并且都带上自己的 logicModel
比如:
- 黑名单链返回 logicModel = rule_blacklist
- 权重链返回 logicModel = rule_weight
- 默认链返回 logicModel = rule_default
同时默认链的 Bean 名也从 "default" 改成了 "rule_default",和新的 LogicModel 枚举保持统一。
这一步做完后,责任链就不只是"帮你算一个奖品",而是变成了"帮你算出一个带来源标记的前置阶段结果"。
7.责任链工厂进一步规范化

除了新增 StrategyAwardVO,这个类还补了一个 LogicModel 枚举:
- RULE_DEFAULT
- RULE_BLACKLIST
- RULE_WEIGHT

8.规则树正式落库,不再只靠测试手工构造
这是这一节和上一节最大的实质性差别之一。
新增了三张规则树相关表及其 DAO / Mapper / PO:
- rule_tree
- rule_tree_node
- rule_tree_node_line
对应文件包括:

这一步的意义是:
第八节的规则树只是"内存里构造一棵树来跑一下";
第九节开始,规则树变成真正的配置化能力,可以从数据库查出来组装成领域对象。
9.仓储层新增规则树查询与组装
新增接口:
java
RuleTreeVO queryRuleTreeVOByTreeId(String treeId)
这个方法非常关键,因为它把数据库里的三张表组装成了领域层真正可用的 RuleTreeVO。
组装过程是:
- 先查 rule_tree 拿树基础信息
- 查 rule_tree_node 拿所有节点
- 查 rule_tree_node_line 拿所有连线
- 先把连线按 ruleNodeFrom 分组,转成 Map<String, List<RuleTreeNodeLineVO>>
- 再把节点和对应的连线拼起来,组装成 Map<String, RuleTreeNodeVO>
- 最后构建完整 RuleTreeVO
- 再缓存到 Redis
java
package cn.bugstack.infrastructure.persistent.repository;
import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity;
import cn.bugstack.domain.strategy.model.entity.StrategyEntity;
import cn.bugstack.domain.strategy.model.entity.StrategyRuleEntity;
import cn.bugstack.domain.strategy.model.valobj.*;
import cn.bugstack.domain.strategy.repository.IStrategyRepository;
import cn.bugstack.infrastructure.persistent.dao.*;
import cn.bugstack.infrastructure.persistent.po.*;
import cn.bugstack.infrastructure.persistent.redis.IRedisService;
import cn.bugstack.types.common.Constants;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description 策略服务仓储实现
* @create 2023-12-23 10:33
*/
@Repository
public class StrategyRepository implements IStrategyRepository {
@Resource
private IStrategyDao strategyDao;
@Resource
private IStrategyRuleDao strategyRuleDao;
@Resource
private IStrategyAwardDao strategyAwardDao;
@Resource
private IRedisService redisService;
@Resource
private IRuleTreeDao ruleTreeDao;
@Resource
private IRuleTreeNodeDao ruleTreeNodeDao;
@Resource
private IRuleTreeNodeLineDao ruleTreeNodeLineDao;
@Override
public List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_KEY + strategyId;
List<StrategyAwardEntity> strategyAwardEntities = redisService.getValue(cacheKey);
if (null != strategyAwardEntities && !strategyAwardEntities.isEmpty()) return strategyAwardEntities;
// 从库中获取数据
List<StrategyAward> strategyAwards = strategyAwardDao.queryStrategyAwardListByStrategyId(strategyId);
strategyAwardEntities = new ArrayList<>(strategyAwards.size());
for (StrategyAward strategyAward : strategyAwards) {
StrategyAwardEntity strategyAwardEntity = StrategyAwardEntity.builder()
.strategyId(strategyAward.getStrategyId())
.awardId(strategyAward.getAwardId())
.awardCount(strategyAward.getAwardCount())
.awardCountSurplus(strategyAward.getAwardCountSurplus())
.awardRate(strategyAward.getAwardRate())
.build();
strategyAwardEntities.add(strategyAwardEntity);
}
redisService.setValue(cacheKey, strategyAwardEntities);
return strategyAwardEntities;
}
@Override
public void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map<Integer, Integer> strategyAwardSearchRateTable) {
// 1. 存储抽奖策略范围值,如10000,用于生成1000以内的随机数
redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key, rateRange);
// 2. 存储概率查找表
Map<Integer, Integer> cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key);
cacheRateTable.putAll(strategyAwardSearchRateTable);
}
@Override
public Integer getStrategyAwardAssemble(String key, Integer rateKey) {
return redisService.getFromMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key, rateKey);
}
@Override
public int getRateRange(Long strategyId) {
return getRateRange(String.valueOf(strategyId));
}
@Override
public int getRateRange(String key) {
return redisService.getValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key);
}
@Override
public StrategyEntity queryStrategyEntityByStrategyId(Long strategyId) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.STRATEGY_KEY + strategyId;
StrategyEntity strategyEntity = redisService.getValue(cacheKey);
if (null != strategyEntity) return strategyEntity;
Strategy strategy = strategyDao.queryStrategyByStrategyId(strategyId);
if (null == strategy) return StrategyEntity.builder().build();
strategyEntity = StrategyEntity.builder()
.strategyId(strategy.getStrategyId())
.strategyDesc(strategy.getStrategyDesc())
.ruleModels(strategy.getRuleModels())
.build();
redisService.setValue(cacheKey, strategyEntity);
return strategyEntity;
}
@Override
public StrategyRuleEntity queryStrategyRule(Long strategyId, String ruleModel) {
StrategyRule strategyRuleReq = new StrategyRule();
strategyRuleReq.setStrategyId(strategyId);
strategyRuleReq.setRuleModel(ruleModel);
StrategyRule strategyRuleRes = strategyRuleDao.queryStrategyRule(strategyRuleReq);
if (null == strategyRuleRes) return null;
return StrategyRuleEntity.builder()
.strategyId(strategyRuleRes.getStrategyId())
.awardId(strategyRuleRes.getAwardId())
.ruleType(strategyRuleRes.getRuleType())
.ruleModel(strategyRuleRes.getRuleModel())
.ruleValue(strategyRuleRes.getRuleValue())
.ruleDesc(strategyRuleRes.getRuleDesc())
.build();
}
@Override
public String queryStrategyRuleValue(Long strategyId, String ruleModel) {
return queryStrategyRuleValue(strategyId, null, ruleModel);
}
@Override
public String queryStrategyRuleValue(Long strategyId, Integer awardId, String ruleModel) {
StrategyRule strategyRule = new StrategyRule();
strategyRule.setStrategyId(strategyId);
strategyRule.setAwardId(awardId);
strategyRule.setRuleModel(ruleModel);
return strategyRuleDao.queryStrategyRuleValue(strategyRule);
}
@Override
public StrategyAwardRuleModelVO queryStrategyAwardRuleModelVO(Long strategyId, Integer awardId) {
StrategyAward strategyAward = new StrategyAward();
strategyAward.setStrategyId(strategyId);
strategyAward.setAwardId(awardId);
String ruleModels = strategyAwardDao.queryStrategyAwardRuleModels(strategyAward);
if (null == ruleModels) return null;
return StrategyAwardRuleModelVO.builder().ruleModels(ruleModels).build();
}
@Override
public RuleTreeVO queryRuleTreeVOByTreeId(String treeId) {
// 优先从缓存获取
String cacheKey = Constants.RedisKey.RULE_TREE_VO_KEY + treeId;
RuleTreeVO ruleTreeVOCache = redisService.getValue(cacheKey);
if (null != ruleTreeVOCache) return ruleTreeVOCache;
// 从数据库获取
RuleTree ruleTree = ruleTreeDao.queryRuleTreeByTreeId(treeId);
List<RuleTreeNode> ruleTreeNodes = ruleTreeNodeDao.queryRuleTreeNodeListByTreeId(treeId);
List<RuleTreeNodeLine> ruleTreeNodeLines = ruleTreeNodeLineDao.queryRuleTreeNodeLineListByTreeId(treeId);
// 1. tree node line 转换Map结构
Map<String, List<RuleTreeNodeLineVO>> ruleTreeNodeLineMap = new HashMap<>();
for (RuleTreeNodeLine ruleTreeNodeLine : ruleTreeNodeLines) {
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);
}
// 2. tree node 转换为Map结构
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);
}
// 3. 构建 Rule Tree
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;
}
}
