《大营销平台系统设计实现》 - 营销服务 第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

含义:

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

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

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

相关推荐
斯特凡今天也很帅1 小时前
新建数据源报错No bean named ‘SqlSessionFactorykf‘ available
java·数据库·spring boot·mybatis
带刺的坐椅1 小时前
用 Solon AI 从零构建 MCP 工具服务:让 AI Agent 拥有真实世界的能力
java·ai·solon·mcp·solon-ai
Trouvaille ~1 小时前
【Redis篇】为什么需要 Redis:从单机到分布式的架构演进之路
数据库·redis·分布式·缓存·中间件·架构·后端开发
ID_180079054731 小时前
Taobao & 1688 Product API Technical Overview and JSON Response Reference
数据库
TheRouter1 小时前
PromptCaching 工程实践:把LLM 调用成本砍掉80%
java·后端·spring·ai
寻道码路1 小时前
LangChain4j Java AI 应用开发实战(二):大模型参数调优实战:Temperature、TopP、MaxTokens 深度解析
java·开发语言·人工智能·aigc
June`1 小时前
多线程redis项目之aof
数据库·redis·缓存
SimonKing1 小时前
IP定位库的完美替代品:ip2region,开源、免费!
java·后端·程序员
Peter-OK1 小时前
Redis从3.x到8.4的核心新特性深度解析与实战学习指南
数据库·redis·缓存