大营销平台 —— 抽奖规则决策树

一、前言

上面一节我们尝试使用了责任链模式使前置规则过滤解耦,通过调用责任链来进行顺序过滤。但是这样的过滤依旧不够完美,因为责任链的设定就是 "一条道走到黑" ,无论中间的过滤结果怎么样,都会按照顺序走下去,直到最后兜底尾节点进行默认处理。这样的顺序过滤对于前置规则过滤来说是合理的,但是在中置规则过滤就不太适配了,因为中置过滤规则中会涉及到库存扣减,存在多种情况,自然也就有多种不同的走向了,用单一的链表很难处理:

所以这里我们要引入组合模式来解决这个问题,组合模式本身就是一个树形结构,具体的概念后面阐述,总之,这里只需要知道我们需要通过树结构来对中置规则过滤进行处理,和责任链一样,需要把这些处理类当作一个节点。

二、抽奖规则决策树

老样子,我们还是 "找核心",显然的,和责任链一样,这里我们的核心类一定是规定树结构的那个类。

java 复制代码
/**
 * @author 印东升
 * @description 决策树引擎
 * @create 2026-04-15 11:38
 */
@Slf4j
public class DecisionTreeEngine implements IDecisionTreeEngine {

    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.StrategyAwardData process(String userId, Long strategy, Integer awardId) {
        DefaultTreeFactory.StrategyAwardData 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());

            DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategy, awardId);
            RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
            strategyAwardData = logicEntity.getStrategyAwardData();
            log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());

            nextNode = nextNode(ruleLogicCheckTypeVO.getCode(), ruleTreeNode.getTreeNodeLineVOList());
            ruleTreeNode = treeNodeMap.get(nextNode);
        }

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

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

        throw new RuntimeException("决策树引擎,nextNode 计算失败,未找到可执行节点");
    }


    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;
        }
    }
}

DecisionTreeEngine 这个规则树引擎就是核心类了,接下来我们一步一步分析它的流程:

1.注入叶子节点

和责任链一样,我们首先要注入这三个处理节点的Bean(次数锁、库存处理、兜底奖励),还是通过Map注入,Spring会自动装配:

java 复制代码
 private final Map<String, ILogicTreeNode> logicTreeNodeGroup;

2.核心流程方法

然后就是整体的流程方法**process()**了,在里面我们要去定义节点的处理顺序链:

java 复制代码
    @Override
    public DefaultTreeFactory.StrategyAwardData process(String userId, Long strategy, Integer awardId) {
        DefaultTreeFactory.StrategyAwardData 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());

            DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategy, awardId);
            RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
            strategyAwardData = logicEntity.getStrategyAwardData();
            log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());

            nextNode = nextNode(ruleLogicCheckTypeVO.getCode(), ruleTreeNode.getTreeNodeLineVOList());
            ruleTreeNode = treeNodeMap.get(nextNode);
        }

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

我们一步一步分析:

(1)值对象

在正式开始前先介绍一下和规则树相关的节点属性类:

树的值对象:这里面包含了整个树的属性

java 复制代码
//规则树对象【注意;不具有唯一ID,不需要改变数据库结果的对象,可以被定义为值对象】
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RuleTreeVO {
    /** 规则树ID */
    private Integer treeId;
    /** 规则树名称 */
    private String treeName;
    /** 规则树描述 */
    private String treeDesc;
    /** 规则根节点 */
    private String treeRootRuleNode;

    /** 规则节点 */
    private Map<String, RuleTreeNodeVO> treeNodeMap;
}

这是每个节点对象

java 复制代码
//规则树节点对象
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RuleTreeNodeVO {

    /** 规则树ID */
    private Integer treeId;
    /** 规则Key */
    private String ruleKey;
    /** 规则描述 */
    private String ruleDesc;
    /** 规则比值 */
    private String ruleValue;

    /** 规则连线 */
    private List<RuleTreeNodeLineVO> treeNodeLineVOList;

}

这是连接对象

java 复制代码
//规则树节点指向线对象。用于衔接 from->to 节点链路关系
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RuleTreeNodeLineVO {

    /** 规则树ID */
    private Integer treeId;
    /** 规则Key节点 From */
    private String ruleNodeFrom;
    /** 规则Key节点 To */
    private String ruleNodeTo;
    /** 限定类型;1:=;2:>;3:<;4:>=;5<=;6:enum[枚举范围] */
    private RuleLimitTypeVO ruleLimitType;
    /** 限定值(到下个节点) */
    private RuleLogicCheckTypeVO ruleLimitValue;

}

(2)获取基础信息

java 复制代码
        //获取根节点名
        String nextNode = ruleTreeVO.getTreeRootRuleNode();
        //从接口map中拿出所有的节点
        Map<String, RuleTreeNodeVO> treeNodeMap = ruleTreeVO.getTreeNodeMap();
        //根据根节点名获取根节点属性
        RuleTreeNodeVO ruleTreeNode = treeNodeMap.get(nextNode);

(3)树型处理(核心方法)

我已经把每一步都写上注释了,应该很容易就能看懂了:

java 复制代码
        while (null != nextNode) {
            //1.根据节点的规则名找到map中注入的规则处理节点
            ILogicTreeNode logicTreeNode = logicTreeNodeGroup.get(ruleTreeNode.getRuleKey());
            //2.规则处理,得到一个处理结果------封装到TreeActionEntity中
            DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategy, awardId);
            //3.查看是放行还是接管(用于选择后续的路由)
            RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
            //4.更新处理结果
            strategyAwardData = logicEntity.getStrategyAwardData();
            log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());
            //5.节点迭代
            nextNode = nextNode(ruleLogicCheckTypeVO.getCode(), ruleTreeNode.getTreeNodeLineVOList());
            ruleTreeNode = treeNodeMap.get(nextNode);
        }

(4)返回结果

java 复制代码
        //返回最终结果
        return strategyAwardData;

(5)关于节点迭代

nextNode是节点与节点之间的连线,只有满足特定条件才会走这一条,这里注意,在抽奖开始前,这一整棵规则树一定是创建好了的,包括里面各个节点、连线的顺序都是全部确定好了的,所有这个选择路由其实是固定指向了一个固定节点的。

java 复制代码
    //寻找下一个节点(选择路由)
    private String nextNode(String matterValue, List<RuleTreeNodeLineVO> ruleTreeNodeLineVOList) {
        //1.判断是否为叶子节点,如果是叶子节点,那么就没有连线了
        if (null == ruleTreeNodeLineVOList || ruleTreeNodeLineVOList.isEmpty()) return null;
        //2.遍历所有连线,找到第一根条件匹配的连线,走匹配的连线
        for (RuleTreeNodeLineVO nodeLine : ruleTreeNodeLineVOList) {
            if (decisionLogic(matterValue, nodeLine)) {
                return nodeLine.getRuleNodeTo();
            }
        }

        throw new RuntimeException("决策树引擎,nextNode 计算失败,未找到可执行节点");
    }

    //条件选择器,规定走当前连线的条件,对比条件后返回一个布尔值来表示 走or不走
    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;
        }
    }

3.节点处理实现

树中的三种节点实现:

次数锁:

java 复制代码
//次数锁节点
@Slf4j
@Component("rule_lock")
public class RuleLockLogicTreeNode implements ILogicTreeNode {

    @Override
    public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
        return DefaultTreeFactory.TreeActionEntity.builder()
                .ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
                .build();
    }
}

兜底奖励:

java 复制代码
//兜底奖励节点
@Slf4j
@Component("rule_luck_award")
public class RuleLuckAwardLogicTreeNode implements ILogicTreeNode {

    @Override
    public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
        return DefaultTreeFactory.TreeActionEntity.builder()
                .ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
                .strategyAwardData(DefaultTreeFactory.StrategyAwardData.builder()
                        .awardId(101)
                        .awardRuleValue("1,100")
                        .build())
                .build();
    }
}

库存:

java 复制代码
//库存节点
@Slf4j
@Component("rule_stock")
public class RuleStockLogicTreeNode implements ILogicTreeNode {
    @Override
    public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
        return DefaultTreeFactory.TreeActionEntity.builder()
                .ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
                .build();
    }
}

4.规则树工厂

刚刚我们主要是在讲核心规则树引擎类,而这个规则树工厂是用来生产规则树引擎的,同时提供了规则树引擎需要使用的数据类型。

为什么要把这些数据类型放在这个规则工厂?

因为这样可以告诉其他类:"这些数据类型是专门为这个工厂及其产出的引擎服务的,不要在其他地方乱用。"

同时当我们需要使用这些静态内部类时,就必须写DefaultTreeFactory.TreeActionEntity,使这些静态类的从属关系一目了然。

java 复制代码
/**
 * @author 印东升
 * @description 规则树工厂
 * @create 2026-04-15 11:34
 */
@Service
public class DefaultTreeFactory {

    private final Map<String, ILogicTreeNode> logicTreeNodeMap;

    public DefaultTreeFactory(Map<String, ILogicTreeNode> logicTreeNodeMap) {
        this.logicTreeNodeMap = logicTreeNodeMap;
    }

    public IDecisionTreeEngine openLogicTree(RuleTreeVO ruleTreeVO) {
        return new DecisionTreeEngine(logicTreeNodeMap, ruleTreeVO);
    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class TreeActionEntity {
        private RuleLogicCheckTypeVO ruleLogicCheckType;
        private StrategyAwardData strategyAwardData;

    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class StrategyAwardData {
        /**
         * 抽奖奖品ID - 内部流转使用
         */
        private Integer awardId;
        /**
         * 抽奖奖品规则
         */
        private String awardRuleValue;
    }
}

三、测试

先构建树的流程,然后构建树的属性,最后测试:

java 复制代码
/**
 * @author 印东升
 * @description 规则树测试
 * @create 2026-04-15 13:00
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogicTreeTest {

    @Resource
    private DefaultTreeFactory defaultTreeFactory;

    /**
     * rule_lock --左--> rule_luck_award
     * --右--> rule_stock --右--> rule_luck_award
     */
    @Test
    public void test_tree_rule() {
        // 构建规则树整体流程
        RuleTreeNodeVO rule_lock = RuleTreeNodeVO.builder()
                .treeId(100000001)
                .ruleKey("rule_lock")
                .ruleDesc("限定用户已完成N次抽奖后解锁")
                .ruleValue("1")
                .treeNodeLineVOList(new ArrayList<RuleTreeNodeLineVO>() {{
                    add(RuleTreeNodeLineVO.builder()
                            .treeId(100000001)
                            .ruleNodeFrom("rule_lock")
                            .ruleNodeTo("rule_luck_award")
                            .ruleLimitType(RuleLimitTypeVO.EQUAL)
                            .ruleLimitValue(RuleLogicCheckTypeVO.TAKE_OVER)
                            .build());

                    add(RuleTreeNodeLineVO.builder()
                            .treeId(100000001)
                            .ruleNodeFrom("rule_lock")
                            .ruleNodeTo("rule_stock")
                            .ruleLimitType(RuleLimitTypeVO.EQUAL)
                            .ruleLimitValue(RuleLogicCheckTypeVO.ALLOW)
                            .build());
                }})
                .build();

        RuleTreeNodeVO rule_luck_award = RuleTreeNodeVO.builder()
                .treeId(100000001)
                .ruleKey("rule_luck_award")
                .ruleDesc("限定用户已完成N次抽奖后解锁")
                .ruleValue("1")
                .treeNodeLineVOList(null)
                .build();

        RuleTreeNodeVO rule_stock = RuleTreeNodeVO.builder()
                .treeId(100000001)
                .ruleKey("rule_stock")
                .ruleDesc("库存处理规则")
                .ruleValue(null)
                .treeNodeLineVOList(new ArrayList<RuleTreeNodeLineVO>() {{
                    add(RuleTreeNodeLineVO.builder()
                            .treeId(100000001)
                            .ruleNodeFrom("rule_lock")
                            .ruleNodeTo("rule_luck_award")
                            .ruleLimitType(RuleLimitTypeVO.EQUAL)
                            .ruleLimitValue(RuleLogicCheckTypeVO.TAKE_OVER)
                            .build());
                }})
                .build();
        //构建规则树属性
        RuleTreeVO ruleTreeVO = new RuleTreeVO();
        ruleTreeVO.setTreeId(100000001);
        ruleTreeVO.setTreeName("决策树规则;增加dall-e-3画图模型");
        ruleTreeVO.setTreeDesc("决策树规则;增加dall-e-3画图模型");
        ruleTreeVO.setTreeRootRuleNode("rule_lock");

        ruleTreeVO.setTreeNodeMap(new HashMap<String, RuleTreeNodeVO>() {{
            put("rule_lock", rule_lock);
            put("rule_stock", rule_stock);
            put("rule_luck_award", rule_luck_award);
        }});

        IDecisionTreeEngine treeEngine = defaultTreeFactory.openLogicTree(ruleTreeVO);

        DefaultTreeFactory.StrategyAwardData data = treeEngine.process("yds", 100001L, 100);
        log.info("测试结果:{}", JSON.toJSONString(data));

    }
}
相关推荐
一 乐20 小时前
电影院|基于springboot + vue电影院购票管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·电影院购票管理管理系统
恼书:-(空寄20 小时前
JVM GC 日志分析 + 常见 GC 场景 + 实战参数调优
java·jvm
消失的旧时光-194320 小时前
Spring Boot 实战(五):接口工程化升级(统一返回 + 异常处理 + 错误码体系 + 异常流转机制)
java·spring boot·后端·解耦
杨凯凡21 小时前
【012】图与最短路径:了解即可
java·数据结构
比特森林探险记21 小时前
【无标题】
java·前端
椰猫子1 天前
Javaweb(Filter、Listener、AJAX、JSON)
java·开发语言
朝新_1 天前
【Spring AI 】核心知识体系梳理:从入门到实战
java·人工智能·spring
一 乐1 天前
旅游|基于springboot + vue旅游信息推荐系统(源码+数据库+文档)
java·vue.js·spring boot·论文·旅游·毕设·旅游信息推荐系统
我命由我123451 天前
Android 开发中,关于 Gradle 的 distributionUrl 的一些问题
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
橙露1 天前
SpringBoot 全局异常处理:优雅封装统一返回格式
java·spring boot·后端