《大营销平台系统设计实现》 - 营销服务 第5节:抽奖前置规则过滤

一、本章诉求

在我们的流程设计中,用户执行抽奖时会判断是否已经超过N积分,如果超过N积分则可以在限定范围内进行抽奖。同时如果用户是黑名单范围的羊毛党用户,则只返回固定的奖品ID。

二、流程设计

  1. 整个规则来说,分为抽奖前、抽奖中、抽奖后,三个阶段执行。本节我们先来处理抽奖前的规则。
  2. 在工程分包上,需要添加 rule 来处理抽奖规则,在添加 raffle 处理抽奖过程。

三、功能实现

1. 工程结构

  1. 首先,我们需要在 strategy 层下,添加2个包;rule 规则、raffle 抽奖。
  2. rule 下面是实现的整个规则部分的处理,后续可以更好的扩展添加其他规则。
  3. raffle 是抽奖功能的实现,抽象类是模板模式,定义出标准的抽奖流程。

2.抽奖入口层(后续还会讲)

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 抽奖策略接口
 * @create 2024-01-06 09:19
 */
public interface IRaffleStrategy {

    /**
     * 执行抽奖;用抽奖因子入参,执行抽奖计算,返回奖品信息
     *
     * @param raffleFactorEntity 抽奖因子实体对象,根据入参信息计算抽奖结果
     * @return 抽奖的奖品
     */
    RaffleAwardEntity performRaffle(RaffleFactorEntity raffleFactorEntity);

}

新增了统一抽奖接口 performRaffle,它的意义是把"抽奖"从以前零散的装配/调度调用里抽象成一个明确的领域服务入口

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;

    public AbstractRaffleStrategy(IStrategyRepository repository, IStrategyDispatch strategyDispatch) {
        this.repository = repository;
        this.strategyDispatch = strategyDispatch;
    }

    @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. 策略查询
        StrategyEntity strategy = repository.queryStrategyEntityByStrategyId(strategyId);

        // 3. 抽奖前 - 规则过滤
        RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> ruleActionEntity = this.doCheckRaffleBeforeLogic(RaffleFactorEntity.builder().userId(userId).strategyId(strategyId).build(), strategy.ruleModels());

        if (RuleLogicCheckTypeVO.TAKE_OVER.getCode().equals(ruleActionEntity.getCode())) {
            if (DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode().equals(ruleActionEntity.getRuleModel())) {
                // 黑名单返回固定的奖品ID
                return RaffleAwardEntity.builder()
                        .awardId(ruleActionEntity.getData().getAwardId())
                        .build();
            } else if (DefaultLogicFactory.LogicModel.RULE_WIGHT.getCode().equals(ruleActionEntity.getRuleModel())) {
                // 权重根据返回的信息进行抽奖
                RuleActionEntity.RaffleBeforeEntity raffleBeforeEntity = ruleActionEntity.getData();
                String ruleWeightValueKey = raffleBeforeEntity.getRuleWeightValueKey();
                Integer awardId = strategyDispatch.getRandomAwardId(strategyId, ruleWeightValueKey);
                return RaffleAwardEntity.builder()
                        .awardId(awardId)
                        .build();
            }
        }

        // 4. 默认抽奖流程
        Integer awardId = strategyDispatch.getRandomAwardId(strategyId);

        return RaffleAwardEntity.builder()
                .awardId(awardId)
                .build();
    }

    protected abstract RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> doCheckRaffleBeforeLogic(RaffleFactorEntity raffleFactorEntity, String... logics);

}

AbstractRaffleStrategy.java 是这次最重要的类。它定义了标准流程:

  • 先校验 userId 和 strategyId
  • 通过 repository.queryStrategyEntityByStrategyId(strategyId) 查策略
  • 调 doCheckRaffleBeforeLogic(...) 做前置规则过滤
  • 如果返回的是 TAKE_OVER,说明规则已经接管
  • 黑名单规则就直接返回固定 awardId
  • 权重规则就拿到 ruleWeightValueKey,再按限定范围抽奖
  • 如果没有规则接管,就走 strategyDispatch.getRandomAwardId(strategyId) 的默认抽奖

这里你可以把它理解成"模板方法模式":

  • 父类定流程
  • 子类补"前置规则怎么检查"
java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 默认的抽奖策略实现
 * @create 2024-01-06 11:46
 */
@Slf4j
@Service
public class DefaultRaffleStrategy extends AbstractRaffleStrategy {

    @Resource
    private DefaultLogicFactory logicFactory;

    public DefaultRaffleStrategy(IStrategyRepository repository, IStrategyDispatch strategyDispatch) {
        super(repository, strategyDispatch);
    }

    @Override
    protected RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> doCheckRaffleBeforeLogic(RaffleFactorEntity raffleFactorEntity, String... logics) {
        Map<String, ILogicFilter<RuleActionEntity.RaffleBeforeEntity>> logicFilterGroup = logicFactory.openLogicFilter();

        // 黑名单规则优先过滤
        String ruleBackList = Arrays.stream(logics)
                .filter(str -> str.contains(DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode()))
                .findFirst()
                .orElse(null);

        if (StringUtils.isNotBlank(ruleBackList)) {
            ILogicFilter<RuleActionEntity.RaffleBeforeEntity> logicFilter = logicFilterGroup.get(DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode());
            RuleMatterEntity ruleMatterEntity = new RuleMatterEntity();
            ruleMatterEntity.setUserId(raffleFactorEntity.getUserId());
            ruleMatterEntity.setAwardId(ruleMatterEntity.getAwardId());
            ruleMatterEntity.setStrategyId(raffleFactorEntity.getStrategyId());
            ruleMatterEntity.setRuleModel(DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode());
            RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> ruleActionEntity = logicFilter.filter(ruleMatterEntity);
            if (!RuleLogicCheckTypeVO.ALLOW.getCode().equals(ruleActionEntity.getCode())) {
                return ruleActionEntity;
            }
        }

        // 顺序过滤剩余规则
        List<String> ruleList = Arrays.stream(logics)
                .filter(s -> !s.equals(DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode()))
                .collect(Collectors.toList());

        RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> ruleActionEntity = null;
        for (String ruleModel : ruleList) {
            ILogicFilter<RuleActionEntity.RaffleBeforeEntity> logicFilter = logicFilterGroup.get(ruleModel);
            RuleMatterEntity ruleMatterEntity = new RuleMatterEntity();
            ruleMatterEntity.setUserId(raffleFactorEntity.getUserId());
            ruleMatterEntity.setAwardId(ruleMatterEntity.getAwardId());
            ruleMatterEntity.setStrategyId(raffleFactorEntity.getStrategyId());
            ruleMatterEntity.setRuleModel(ruleModel);
            ruleActionEntity = logicFilter.filter(ruleMatterEntity);
            // 非放行结果则顺序过滤
            log.info("抽奖前规则过滤 userId: {} ruleModel: {} code: {} info: {}", raffleFactorEntity.getUserId(), ruleModel, ruleActionEntity.getCode(), ruleActionEntity.getInfo());
            if (!RuleLogicCheckTypeVO.ALLOW.getCode().equals(ruleActionEntity.getCode())) return ruleActionEntity;
        }

        return ruleActionEntity;
    }

}

DefaultRaffleStrategy.java 就是这个模板的默认实现。它主要负责两件事:

  • 优先处理黑名单规则
  • 再顺序处理其他规则

之所以把黑名单放前面,是因为它是强拦截型规则,一旦命中,就没必要继续算别的规则了。


这里你可能觉得好乱,听不懂,那你就先了解一个大概流程就行:

IRaffleStrategy新增了统一抽奖接口 performRaffle,它的意义是把"抽奖"从以前零散的装配/调度调用里抽象成一个明确的领域服务入口**,AbstractRaffleStrategy** 是抽奖策略抽象类,定义抽奖的标准流程(实现了IRaffleStrategy),DefaultRaffleStrategy****继承AbstractRaffleStrategy,实现前置规则过滤

3.新增的领域对象(entity)

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 抽奖因子实体
 * @create 2024-01-06 09:20
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RaffleFactorEntity {

    /** 用户ID */
    private String userId;
    /** 策略ID */
    private Long strategyId;

}

抽奖入参,目前只放了 userId 和 strategyId。名字叫 "Factor",意思是"影响抽奖的因子",后面要扩展用户积分等级参与次数时可以继续往里放。

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 抽奖奖品实体
 * @create 2024-01-06 09:20
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RaffleAwardEntity {

    /** 策略ID */
    private Long strategyId;
    /** 奖品ID */
    private Integer awardId;
    /** 奖品对接标识 - 每一个都是一个对应的发奖策略 */
    private String awardKey;
    /** 奖品配置信息 */
    private String awardConfig;
    /** 奖品内容描述 */
    private String awardDesc;

}

**抽奖结果对象,**当前真正用到的核心字段是 awardId,其他像 awardKey、awardConfig、awardDesc 是为后续奖品发放和展示预留的。

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 规则物料实体对象,用于过滤规则的必要参数信息。
 * @create 2024-01-06 09:56
 */
@Data
public class RuleMatterEntity {

    /** 用户ID */
    private String userId;
    /** 策略ID */
    private Long strategyId;
    /** 抽奖奖品ID【规则类型为策略,则不需要奖品ID】 */
    private Integer awardId;
    /** 抽奖规则类型【rule_random - 随机值计算、rule_lock - 抽奖几次后解锁、rule_luck_award - 幸运奖(兜底奖品)】 */
    private String ruleModel;

}

可以理解为"规则执行时喂给过滤器的材料"。它和 RaffleFactorEntity 不完全一样,因为规则不一定只关心用户和策略,也可能关心奖品、规则类型,所以专门拆了一个对象。

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 规则动作实体
 * @create 2024-01-06 09:47
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RuleActionEntity<T extends RuleActionEntity.RaffleEntity> {

    private String code = RuleLogicCheckTypeVO.ALLOW.getCode();
    private String info = RuleLogicCheckTypeVO.ALLOW.getInfo();
    private String ruleModel;
    private T data;

    static public class RaffleEntity {

    }

    // 抽奖之前
    @EqualsAndHashCode(callSuper = true)
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    static public class RaffleBeforeEntity extends RaffleEntity {
        /**
         * 策略ID
         */
        private Long strategyId;

        /**
         * 权重值Key;用于抽奖时可以选择权重抽奖。
         */
        private String ruleWeightValueKey;

        /**
         * 奖品ID;
         */
        private Integer awardId;
    }

    // 抽奖之中
    static public class RaffleCenterEntity extends RaffleEntity {

    }

    // 抽奖之后
    static public class RaffleAfterEntity extends RaffleEntity {

    }

}

RuleActionEntity.java 是"规则执行结果"。

这层设计很关键,它不直接返回 true/false,而是返回:

  • code:放行还是接管
  • ruleModel:是哪条规则生效
  • data:规则附带的数据

规定上界,传入的泛型必须是RuleActionEntity.RaffleEntity的子类,防止乱传进来影响最终输出

其中 RaffleBeforeEntity 放的是"抽奖前规则"的返回数据,比如:

  • 黑名单场景返回 awardId
  • 权重场景返回 ruleWeightValueKey

不只定义了 RaffleBeforeEntity

它还预留了:

  • RaffleCenterEntity
  • RaffleAfterEntity

这里继承RuleActionEntity.RaffleEntity,可以当做传入的泛型

这样,这个实体就有了code,info,规则类型,策略id,以及对应规则的值

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 规则过滤校验类型值对象
 * @create 2024-01-06 11:10
 */
@Getter
@AllArgsConstructor
public enum RuleLogicCheckTypeVO {

    ALLOW("0000", "放行;执行后续的流程,不受规则引擎影响"),
    TAKE_OVER("0001","接管;后续的流程,受规则引擎执行结果影响"),
    ;

    private final String code;
    private final String info;

}

定义了两个状态:

  • ALLOW:规则放行,继续后面流程
  • TAKE_OVER:规则接管,后续结果受规则影响

4.规则扩展机制

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 抽奖规则过滤接口
 * @create 2024-01-06 09:55
 */
public interface ILogicFilter<T extends RuleActionEntity.RaffleEntity> {

    RuleActionEntity<T> filter(RuleMatterEntity ruleMatterEntity);

}

规则过滤器接口。所有前置规则都实现它,这样系统可以统一调度不同规则

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 策略自定义枚举
 * @create 2023-12-31 11:29
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogicStrategy {
    //这是注解里的一个"属性定义"
    DefaultLogicFactory.LogicModel logicMode();

}

一个标记注解。它的作用是给每个过滤器贴上"我是哪种规则"的标签。

  • @LogicStrategy 这个注解,必须带一个参数
  • 这个参数名字叫 logicMode
  • 参数类型是 DefaultLogicFactory.LogicModel 这个枚举

5.两个具体规则

java 复制代码
@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicModel.RULE_BLACKLIST)
public class RuleBackListLogicFilter implements ILogicFilter<RuleActionEntity.RaffleBeforeEntity> {

    @Resource
    private IStrategyRepository repository;
    //100:user001,user002,user003
    @Override
    public RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> filter(RuleMatterEntity ruleMatterEntity) {
        log.info("规则过滤-黑名单 userId:{} strategyId:{} ruleModel:{}", ruleMatterEntity.getUserId(), ruleMatterEntity.getStrategyId(), ruleMatterEntity.getRuleModel());

        String userId = ruleMatterEntity.getUserId();

        // 查询规则值配置
        String ruleValue = repository.queryStrategyRuleValue(ruleMatterEntity.getStrategyId(), ruleMatterEntity.getAwardId(), ruleMatterEntity.getRuleModel());
        String[] splitRuleValue = ruleValue.split(Constants.COLON);
        Integer awardId = Integer.parseInt(splitRuleValue[0]);

        // 过滤其他规则
        String[] userBlackIds = splitRuleValue[1].split(Constants.SPLIT);
        for (String userBlackId : userBlackIds) {
            if (userId.equals(userBlackId)) {
                return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                        .ruleModel(DefaultLogicFactory.LogicModel.RULE_BLACKLIST.getCode())
                        .data(RuleActionEntity.RaffleBeforeEntity.builder()
                                .strategyId(ruleMatterEntity.getStrategyId())
                                .awardId(awardId)
                                .build())
                        .code(RuleLogicCheckTypeVO.TAKE_OVER.getCode())
                        .info(RuleLogicCheckTypeVO.TAKE_OVER.getInfo())
                        .build();
            }
        }

        return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                .build();
    }

}

整体流程

  1. 这个类是一个"抽奖前规则过滤器"。
  2. 它专门处理 rule_blacklist 这类规则。
  3. 系统在抽奖前会把用户信息和策略信息传进来。
  4. 这个过滤器先去数据库查黑名单规则配置。
  5. 如果当前用户在黑名单里,就直接接管抽奖结果。
  6. 如果不在黑名单里,就放行给后面的正常抽奖流程。
java 复制代码
@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicModel.RULE_WIGHT)
public class RuleWeightLogicFilter implements ILogicFilter<RuleActionEntity.RaffleBeforeEntity> {

    @Resource
    private IStrategyRepository repository;

    public Long userScore = 4500L;

    /**
     * 权重规则过滤;
     * 1. 权重规则格式;4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109
     * 2. 解析数据格式;判断哪个范围符合用户的特定抽奖范围
     *
     * @param ruleMatterEntity 规则物料实体对象
     * @return 规则过滤结果
     */
    @Override
    public RuleActionEntity<RuleActionEntity.RaffleBeforeEntity> filter(RuleMatterEntity ruleMatterEntity) {
        log.info("规则过滤-权重范围 userId:{} strategyId:{} ruleModel:{}", ruleMatterEntity.getUserId(), ruleMatterEntity.getStrategyId(), ruleMatterEntity.getRuleModel());

        String userId = ruleMatterEntity.getUserId();
        Long strategyId = ruleMatterEntity.getStrategyId();
        String ruleValue = repository.queryStrategyRuleValue(ruleMatterEntity.getStrategyId(), ruleMatterEntity.getAwardId(), ruleMatterEntity.getRuleModel());

        // 1. 根据用户ID查询用户抽奖消耗的积分值,本章节我们先写死为固定的值。后续需要从数据库中查询。
        Map<Long, String> analyticalValueGroup = getAnalyticalValue(ruleValue);
        if (null == analyticalValueGroup || analyticalValueGroup.isEmpty()) {
            return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                    .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                    .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                    .build();
        }

        // 2. 转换Keys值,并默认排序
        List<Long> analyticalSortedKeys = new ArrayList<>(analyticalValueGroup.keySet());
        Collections.sort(analyticalSortedKeys);

        // 3. 找出最小符合的值,也就是【4500 积分,能找到 4000:102,103,104,105】、【5000 积分,能找到 5000:102,103,104,105,106,107】
        Long nextValue = analyticalSortedKeys.stream()
                .filter(key -> userScore >= key)
                .findFirst()
                .orElse(null);

        if (null != nextValue) {
            return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                    .data(RuleActionEntity.RaffleBeforeEntity.builder()
                            .strategyId(strategyId)
                            .ruleWeightValueKey(analyticalValueGroup.get(nextValue))
                            .build())
                    .ruleModel(DefaultLogicFactory.LogicModel.RULE_WIGHT.getCode())
                    .code(RuleLogicCheckTypeVO.TAKE_OVER.getCode())
                    .info(RuleLogicCheckTypeVO.TAKE_OVER.getInfo())
                    .build();
        }

        return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                .build();
    }

    private Map<Long, String> getAnalyticalValue(String ruleValue) {
        String[] ruleValueGroups = ruleValue.split(Constants.SPACE);
        Map<Long, String> ruleValueMap = new HashMap<>();
        for (String ruleValueKey : ruleValueGroups) {
            // 检查输入是否为空
            if (ruleValueKey == null || ruleValueKey.isEmpty()) {
                return ruleValueMap;
            }
            // 分割字符串以获取键和值
            String[] parts = ruleValueKey.split(Constants.COLON);
            if (parts.length != 2) {
                throw new IllegalArgumentException("rule_weight rule_rule invalid input format" + ruleValueKey);
            }
            ruleValueMap.put(Long.parseLong(parts[0]), ruleValueKey);
        }
        return ruleValueMap;
    }

}

整体流程

  1. 这是一个"抽奖前规则过滤器"。
  2. 它专门处理 rule_weight 规则。
  3. 系统先根据策略 ID 查出权重规则配置。
  4. 再根据用户积分判断他应该落到哪个权重区间。
  5. 如果命中了某个区间,就返回这个区间对应的抽奖范围。
  6. 后面的抽奖流程只会在这个范围里随机,不再用默认全量奖池。

6.仓储和 SQL

java 复制代码
public interface IStrategyRepository {

    List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId);

    void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map<Integer, Integer> strategyAwardSearchRateTable);

    Integer getStrategyAwardAssemble(String key, Integer rateKey);

    int getRateRange(Long strategyId);

    int getRateRange(String key);

    StrategyEntity queryStrategyEntityByStrategyId(Long strategyId);

    StrategyRuleEntity queryStrategyRule(Long strategyId, String ruleModel);

    String queryStrategyRuleValue(Long strategyId, Integer awardId, String ruleModel);

}

新增了 queryStrategyRuleValue,因为规则过滤器只关心规则值本身,不一定需要整条规则对象

7.规则工厂

java 复制代码
@Service
public class DefaultLogicFactory {

    public Map<String, ILogicFilter<?>> logicFilterMap = new ConcurrentHashMap<>();

    public DefaultLogicFactory(List<ILogicFilter<?>> logicFilters) {
        logicFilters.forEach(logic -> {
            LogicStrategy strategy = AnnotationUtils.findAnnotation(logic.getClass(), LogicStrategy.class);
            if (null != strategy) {
                logicFilterMap.put(strategy.logicMode().getCode(), logic);
            }
        });
    }

    public <T extends RuleActionEntity.RaffleEntity> Map<String, ILogicFilter<T>> openLogicFilter() {
        return (Map<String, ILogicFilter<T>>) (Map<?, ?>) logicFilterMap;
    }

    @Getter
    @AllArgsConstructor
    public enum LogicModel {

        RULE_WIGHT("rule_weight","【抽奖前规则】根据抽奖权重返回可抽奖范围KEY"),
        RULE_BLACKLIST("rule_blacklist","【抽奖前规则】黑名单规则过滤,命中黑名单则直接返回"),

        ;

        private final String code;
        private final String info;

    }

}

整体作用

在这套代码里,后面会有很多规则类,比如:

  • RuleBackListLogicFilter
  • RuleWeightLogicFilter

如果没有这个工厂,主流程就只能手写很多判断:

java 复制代码
if (ruleModel.equals("rule_blacklist")) { ... }
if (ruleModel.equals("rule_weight")) { ... }

这样规则一多,代码就会越来越乱。

所以这里专门做了一个工厂,统一管理"规则编码 -> 规则实现类"的映射关系。

这个 Map 是干什么的

java 复制代码
public Map<String, ILogicFilter<?>> logicFilterMap = new ConcurrentHashMap<>();

它是这个工厂最核心的数据结构。

含义是:

  • key:规则编码,比如 rule_blacklist
  • value:对应的规则过滤器实现,比如 RuleBackListLogicFilter

注册完之后,大概会变成这样:

java 复制代码
{
  "rule_blacklist" -> RuleBackListLogicFilter,
  "rule_weight" -> RuleWeightLogicFilter
}

构造函数为什么能拿到 List<ILogicFilter<?>>

这是 Spring 的自动注入能力

意思是:

  • Spring 会扫描容器里所有实现了 ILogicFilter 的 Bean
  • 然后把它们全部装进一个 List
  • 再传给这个构造函数

也就是说,只要你写了一个类:

java 复制代码
@Component
public class XxxLogicFilter implements ILogicFilter<...>

Spring 就会把它自动收集进来。

在你这个项目里,目前会被收进来的就是:

  • RuleBackListLogicFilter
  • RuleWeightLogicFilter

这一步非常关键,它让"新增规则"变得很轻量:

  • 新写一个过滤器类
  • 实现接口
  • 加注解
  • 工厂就能自动发现

forEach 这段在做什么

java 复制代码
logicFilters.forEach(logic -> {
    LogicStrategy strategy = AnnotationUtils.findAnnotation(logic.getClass(), LogicStrategy.class);
    if (null != strategy) {
        logicFilterMap.put(strategy.logicMode().getCode(), logic);
    }
});

这段就是"自动注册"的核心

它分 3 步:

  1. 遍历所有 ILogicFilter

    Spring 已经把所有规则过滤器类都传进来了,所以这里逐个处理。

  2. 读取类上的 @LogicStrategy

    比如:

    • RuleBackListLogicFilter 上有
      @LogicStrategy(logicMode = RULE_BLACKLIST)
    • RuleWeightLogicFilter 上有
      @LogicStrategy(logicMode = RULE_WIGHT)

AnnotationUtils.findAnnotation(...) 就是在反射读取这个标签。

  1. 把规则编码和实现类塞进 Map
    比如:
    • rule_blacklist -> RuleBackListLogicFilter
    • rule_weight -> RuleWeightLogicFilter

这样后面主流程只需要传一个规则编码,就能找到对应实现。

openLogicFilter() 是什么作用

java 复制代码
   public <T extends RuleActionEntity.RaffleEntity> Map<String, ILogicFilter<T>> openLogicFilter() {
        return (Map<String, ILogicFilter<T>>) (Map<?, ?>) logicFilterMap;
    }

这个方法就是把内部注册表暴露出去。

调用方拿到后,就可以这样按规则名取:

java 复制代码
Map<String, ILogicFilter<RuleActionEntity.RaffleBeforeEntity>> logicFilterGroup = logicFactory.openLogicFilter();
ILogicFilter<RuleActionEntity.RaffleBeforeEntity> logicFilter = logicFilterGroup.get("rule_blacklist");

这样就能动态找到某个规则过滤器。

这里的泛型 <T extends RuleActionEntity.RaffleEntity> 是为了让不同阶段的规则都能复用这套工厂。

因为 RuleActionEntity 里预留了:

  • RaffleBeforeEntity
  • RaffleCenterEntity
  • RaffleAfterEntity

也就是说,这个工厂不只服务"抽奖前规则",理论上后面"抽奖中规则""抽奖后规则"也能继续用。

整体总结

1. 先理解旧流程在干什么

旧版本的核心能力只有两块:

  • IStrategyArmory:装配抽奖策略,把概率表算出来放 Redis
  • IStrategyDispatch:根据策略从 Redis 的概率表里随机拿一个奖品

也就是说,之前更像:

  1. 先准备好概率表
  2. 抽奖时直接随机取结果

这时候系统还没有"统一抽奖入口",也没有"抽奖前规则判断"。

所以这次改动的出发点就是:

  • 以前只有"怎么抽"
  • 现在要补上"抽之前先判断能不能这么抽"

2. 再看新加的抽奖入口层

这一步是最大的结构变化。

新增了:

  • IRaffleStrategy
  • AbstractRaffleStrategy
  • DefaultRaffleStrategy

它们的职责是:

  • IRaffleStrategy:定义统一抽奖能力,对外暴露 performRaffle
  • AbstractRaffleStrategy:定义抽奖标准流程
  • DefaultRaffleStrategy:实现"抽奖前规则过滤"这一步

所以现在抽奖不再是外面自己去调 getRandomAwardId(...),而是统一走:

java 复制代码
raffleStrategy.performRaffle(raffleFactorEntity)

这个入口把整个流程串起来了。


3. 把主流程背下来,这是整章主线

主流程在 AbstractRaffleStrategy.performRaffle(),可以直接记成 4 步:

  1. 参数校验
    检查 userId、strategyId

  2. 查询策略

    根据 strategyId 查当前策略配置

  3. 抽奖前规则过滤

    调用 doCheckRaffleBeforeLogic(...)【子类进行具体实现】

  4. 根据规则结果决定怎么走

    • 如果规则 TAKE_OVER:规则接管
    • 如果规则 ALLOW:走默认随机抽奖

这里是全流程的核心分叉点。

你可以把它理解成:

  • ALLOW = 放行,继续正常抽
  • TAKE_OVER = 接管,不按默认方式抽

4. 新增领域对象是为了把"输入、规则、输出"拆开

这次还补了一组领域对象,不是为了凑类,而是为了让流程清楚。

  • RaffleFactorEntity
    作用:抽奖入参
    当前只有:
  • userId
  • strategyId

它表示"触发一次抽奖时,外部传进来的因子"。

  • RaffleAwardEntity

    作用:抽奖结果

    核心是 awardId

  • RuleMatterEntity

    作用:规则执行时的入参

    给规则过滤器看的"规则物料"

  • RuleActionEntity

    作用:规则执行后的返回值

    里面会告诉主流程:

  • 是 ALLOW 还是 TAKE_OVER

  • 如果接管,带什么数据回来

这一组类的意义是:

  • 抽奖入参和规则入参分开
  • 规则返回结果和最终抽奖结果分开

这样后面规则扩展就不会把参数搞乱

5. 规则扩展机制是为了以后能继续加规则

这一层是本次设计里最像"框架"的地方。

涉及:

  • ILogicFilter
  • LogicStrategy
  • DefaultLogicFactory

先说目标:

系统以后不可能只有黑名单和权重规则,肯定还会继续加。

所以不能把所有规则都写死在主流程里。

于是这里做了一个扩展机制:

  • ILogicFilter

    定义统一规则接口,所有规则都实现它

  • @LogicStrategy

    给规则类打标签,说明"我处理哪种规则"

  • DefaultLogicFactory
    启动时扫描所有规则过滤器,把它们注册进 Map

这样以后要新增规则时,只要:

  1. 写一个新过滤器类
  2. 实现 ILogicFilter
  3. 加上 @LogicStrategy
    就能接入

6. 规则工厂在整条链路里扮演什么角色

你可以把 DefaultLogicFactory 单独当成"规则注册中心"。

它启动时做的事是:

  1. Spring 把所有实现了 ILogicFilter 的 Bean 收集起来
  2. 工厂逐个读取它们类上的 @LogicStrategy
  3. 拿到规则编码,比如:
    • rule_blacklist
    • rule_weight
  4. 注册成:
    • rule_blacklist -> RuleBackListLogicFilter
    • rule_weight -> RuleWeightLogicFilter

后面 DefaultRaffleStrategy 在做规则过滤时,就不用写死 if else 找类了,而是直接按规则名去工厂拿过滤器。

7. 两个具体规则分别干什么

这一步是把规则扩展机制真正落地到业务。

黑名单规则 RuleBackListLogicFilter

作用:

  • 先查黑名单规则配置
  • 判断当前用户是不是黑名单
  • 如果命中,直接返回固定奖品
  • 主流程收到 TAKE_OVER 后,不再走默认抽奖

它处理的规则值格式是:

100:user001,user002,user003

含义:

  • 100:命中后直接返回奖品 100
  • 后面是黑名单用户列表

这是"强接管型规则"。

权重规则 RuleWeightLogicFilter

作用:

  • 查权重规则配置
  • 根据用户积分命中某个积分门槛
  • 返回一个可抽奖范围 key
  • 主流程收到 TAKE_OVER 后,不是直接给奖品,而是在指定范围里再抽一次

规则值格式是:

4000:102,103,104,105 5000:102,103,104,105,106,107

含义:

  • 不同积分对应不同奖品池范围

这是"限制抽奖范围型规则"。

现在回头去想想我们之前遗留的抽奖入口层,是不是就可以明白为什么这样设计了,这样设计的功能流程是什么,每一步都是在干什么

相关推荐
倔强的石头_8 小时前
《Kingbase护城河》——猎捕慢查询:执行计划的微观解析与索引调优实战
数据库
SelectDB10 小时前
Apache Doris Python UDF:让 SQL 直接调用 Python 生态,支撑 Agent 时代复杂业务逻辑
大数据·数据库·python
Flittly12 小时前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
小兔崽子去哪了12 小时前
Java 生成二维码解决方案
java·后端
YuePeng13 小时前
写了五年注解的低代码框架,2.0 决定让你连注解都不用写了
github·产品
小白ai14 小时前
从"能 ping 通吗"到"为什么上不了网"——我写了一个网络故障诊断引擎
github
徐小夕16 小时前
jitword 协同文档3.2发布:打造浏览器中最强word编辑器
前端·架构·github
人活一口气16 小时前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
齐翊17 小时前
分享一个在 Claude Code 里 [同时] 用多个 ApiKey 的方法
程序员·github·agent
A_Lonely_Cat18 小时前
记一次 GitHub 幽灵协作者大清洗:强制重写 Git 历史与穿透 CDN 缓存实践
git·github