《大营销平台系统设计实现》 - 营销服务 第9节:模板模式串联抽奖规则 一、本章诉求1. 这一节整体在解决什么问题第八节的时候规则树虽然已经有了树模型节点接口节点实现决策引擎但它还只是“能单独跑一个测试”的状态并没有真正接入抽奖主流程。同时第八节的抽奖主流程里前置规则和规则树之间还没有形成完整串联前置责任链拿到一个初步奖品后面的树结构只是设计好了中间还有很多旧的过滤器结构残留第九节干的事就是把这些碎片拼完整前置规则继续用责任链责任链返回的不再只是 awardId而是“带来源信息的结果对象”后续规则不再走旧的过滤器模式而是正式交给规则树规则树配置不再手工在测试里构造而是从数据库读取主流程通过模板方法把这两段规则逻辑正式串起来所以这一节不是“又加了一个模式”而是把前面几节铺的结构真正落成一条完整业务链。二、什么是模板方法模板方法是一种设计模式你可以把它理解成先在父类里把一整套业务流程的执行顺序固定下来再把其中某些具体步骤留给子类去实现。也就是说它解决的是流程骨架不变但某些环节允许扩展最简单的理解比如抽奖流程不管怎么变化大体步骤都差不多校验参数做前置规则处理计算抽奖结果做中奖后规则过滤返回最终结果这个“先做什么后做什么”的顺序其实是稳定的。那就可以把这条固定顺序写在父类里。但具体怎么做前置规则、怎么做中奖后过滤不同实现可能不一样。这些变化的部分就交给子类去实现这就是模板方法。三、功能实现1. 库表设计设计3个表 树根 - rule_tree、节点 - rule_tree_node、连线 - rule_tree_node_line 通过这3个表构建出一颗规则树。2. 工程结构rule 规则部分保留责任链、规则树去掉之前的 filter 过滤器。在 AbstractRaffleStrategy 抽象类串联调用流程。先是责任链后是规则树。责任链处理的是不同的抽奖【黑名单、权重、默认】处理完的抽奖结果如果是默认抽奖则需要进行库存、次数等校验并给出最终发奖结果。本节还包括了根据上一节实现的规则树模型设计的库表结构。并实现出仓储数据查询的操作。3.抽奖模板主流程正式升级/** * 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 升级成结果对象/** * 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 里多了两个字段awardIdlogicModel这意味着责任链不只告诉主流程“抽到了哪个奖”还会告诉主流程这个奖是通过哪种规则路径拿到的。为什么这一步重要因为主流程现在要判断如果是黑名单接管如果是权重接管这些都属于“规则已经替你决定完了”不需要再进入规则树只有当责任链返回的是 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_DEFAULTRULE_BLACKLISTRULE_WEIGHT8.规则树正式落库不再只靠测试手工构造这是这一节和上一节最大的实质性差别之一。新增了三张规则树相关表及其 DAO / Mapper / POrule_treerule_tree_noderule_tree_node_line对应文件包括这一步的意义是第八节的规则树只是“内存里构造一棵树来跑一下”第九节开始规则树变成真正的配置化能力可以从数据库查出来组装成领域对象。9.仓储层新增规则树查询与组装新增接口RuleTreeVO queryRuleTreeVOByTreeId(String treeId)这个方法非常关键因为它把数据库里的三张表组装成了领域层真正可用的 RuleTreeVO。组装过程是先查 rule_tree 拿树基础信息查 rule_tree_node 拿所有节点查 rule_tree_node_line 拿所有连线先把连线按 ruleNodeFrom 分组转成 MapString, ListRuleTreeNodeLineVO再把节点和对应的连线拼起来组装成 MapString, RuleTreeNodeVO最后构建完整 RuleTreeVO再缓存到 Redispackage 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 ListStrategyAwardEntity queryStrategyAwardList(Long strategyId) { // 优先从缓存获取 String cacheKey Constants.RedisKey.STRATEGY_AWARD_KEY strategyId; ListStrategyAwardEntity strategyAwardEntities redisService.getValue(cacheKey); if (null ! strategyAwardEntities !strategyAwardEntities.isEmpty()) return strategyAwardEntities; // 从库中获取数据 ListStrategyAward 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, MapInteger, Integer strategyAwardSearchRateTable) { // 1. 存储抽奖策略范围值如10000用于生成1000以内的随机数 redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY key, rateRange); // 2. 存储概率查找表 MapInteger, 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); ListRuleTreeNode ruleTreeNodes ruleTreeNodeDao.queryRuleTreeNodeListByTreeId(treeId); ListRuleTreeNodeLine ruleTreeNodeLines ruleTreeNodeLineDao.queryRuleTreeNodeLineListByTreeId(treeId); // 1. tree node line 转换Map结构 MapString, ListRuleTreeNodeLineVO 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(); ListRuleTreeNodeLineVO ruleTreeNodeLineVOList ruleTreeNodeLineMap.computeIfAbsent(ruleTreeNodeLine.getRuleNodeFrom(), k - new ArrayList()); ruleTreeNodeLineVOList.add(ruleTreeNodeLineVO); } // 2. tree node 转换为Map结构 MapString, 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; } }