《大营销平台系统设计实现》 - 营销服务 第9节:模板模式串联抽奖规则

一、本章诉求

1. 这一节整体在解决什么问题

第八节的时候,规则树虽然已经有了:

  • 树模型
  • 节点接口
  • 节点实现
  • 决策引擎

但它还只是"能单独跑一个测试"的状态,并没有真正接入抽奖主流程。

同时,第八节的抽奖主流程里,前置规则和规则树之间还没有形成完整串联:

  • 前置责任链拿到一个初步奖品
  • 后面的树结构只是设计好了
  • 中间还有很多旧的过滤器结构残留

第九节干的事,就是把这些碎片拼完整:

  • 前置规则继续用责任链
  • 责任链返回的不再只是 awardId,而是"带来源信息的结果对象"
  • 后续规则不再走旧的过滤器模式,而是正式交给规则树
  • 规则树配置不再手工在测试里构造,而是从数据库读取
  • 主流程通过模板方法把这两段规则逻辑正式串起来

所以这一节不是"又加了一个模式",而是把前面几节铺的结构真正落成一条完整业务链。

二、什么是模板方法

模板方法是一种设计模式,你可以把它理解成:

先在父类里把一整套业务流程的执行顺序固定下来,再把其中某些具体步骤留给子类去实现。

也就是说,它解决的是:

流程骨架不变,但某些环节允许扩展

最简单的理解

比如抽奖流程,不管怎么变化,大体步骤都差不多:

  • 校验参数
  • 做前置规则处理
  • 计算抽奖结果
  • 做中奖后规则过滤
  • 返回最终结果

这个"先做什么,后做什么"的顺序,其实是稳定的。

那就可以把这条固定顺序写在父类里。

但具体怎么做前置规则、怎么做中奖后过滤,不同实现可能不一样。

这些变化的部分,就交给子类去实现,这就是模板方法。

三、功能实现

1. 库表设计

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

2. 工程结构

  1. rule 规则部分,保留;责任链、规则树,去掉之前的 filter 过滤器。
  2. 在 AbstractRaffleStrategy 抽象类,串联调用流程。先是责任链,后是规则树。责任链处理的是不同的抽奖【黑名单、权重、默认】,处理完的抽奖结果,如果是默认抽奖则需要进行库存、次数等校验,并给出最终发奖结果。
  3. 本节还包括了根据上一节实现的规则树模型,设计的库表结构。并实现出仓储数据查询的操作。

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
  • 再继续做中置规则过滤
  • 如果拦截就走兜底描述
  • 否则返回奖品

到了第九节,主流程完全重组了。

现在的流程是:

  1. 参数校验
  2. 调用 raffleLogicChain(userId, strategyId) 做责任链抽奖
  3. 如果责任链结果不是默认抽奖,而是黑名单或权重规则直接接管,那就直接返回结果
  4. 如果责任链走的是默认抽奖,再继续调用 raffleLogicTree(userId, strategyId, awardId) 做规则树过滤
  5. 最终返回规则树产出的奖品信息

这个变化很重要,因为它把主流程清晰拆成了两段:

  • 责任链负责前置规则与初步抽奖
  • 规则树负责中奖后的进一步规则过滤和结果修正

另外,这个类还新增了一个依赖:

  • 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。

组装过程是:

  1. 先查 rule_tree 拿树基础信息
  2. 查 rule_tree_node 拿所有节点
  3. 查 rule_tree_node_line 拿所有连线
  4. 先把连线按 ruleNodeFrom 分组,转成 Map<String, List<RuleTreeNodeLineVO>>
  5. 再把节点和对应的连线拼起来,组装成 Map<String, RuleTreeNodeVO>
  6. 最后构建完整 RuleTreeVO
  7. 再缓存到 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;
    }

}
相关推荐
流年随风9 小时前
在LINUX服务器 CentOS 7中同步网络时间
linux·服务器·centos
阿正的梦工坊9 小时前
【Typescript】10-条件类型与-infer
前端·javascript·typescript
Harm灬小海10 小时前
【云计算学习之路】学习Centos7系统:服务搭建(VSFTP)
linux·运维·服务器·学习·云计算
GuWenyue10 小时前
我被 React 性能问题逼疯了,直到学会这 4 个优化技巧
前端
窗边的anini10 小时前
那个因为 vibecoding 差点搞砸约会的女孩,被 TRAE SOLO 救了
前端·人工智能·程序员
用户7138742290010 小时前
OAuth 2.0 client_id深度解析:从规范到安全实践
前端
李燚10 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
javascript·人工智能·react.js·golang·aigc·agent
vortex510 小时前
virsh 使用指南:KVM 虚拟化管理的命令行艺术
linux·运维·服务器
ZC跨境爬虫10 小时前
跟着 MDN 学CSS day_8:(盒模型完全解)
前端·javascript·css·ui·交互