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

一、本章诉求

电商平台的抽奖营销系统业务逻辑复杂度较高,以拼多多类平台为例,抽奖玩法场景丰富。项目运营中后期,产品常会围绕积分消耗、提升用户抽奖参与频次优化玩法,典型策略为阶梯式解锁奖品池:用户初始抽奖仅可抽取 1-6 档奖品,累计抽奖 3 次解锁 1-7 档,累计 6 次进一步开放 1-9 档,利用逐级扩容奖品范围的机制,驱动用户主动消耗积分、反复参与抽奖。

基于这类多变的业务需求,抽奖系统设计需遵循松耦合原则,参考 Spring 拆解 Bean 的分层思想,将完整抽奖流程拆分多个独立执行阶段。各阶段可灵活插拔新增业务逻辑,大幅降低后续版本迭代成本,避免架构耦合度过高,引发每次需求改动都大面积改动代码的问题。

二、流程设计

在流程实现中,设计出抽奖的前中后置过程,并在每个阶段设计对应的操作规则。当你这样设计以后,你会发现整个抽奖功能实现变得非常灵活好扩展。

在上一章节中,我们已经搭建好了抽奖规则的核心实现框架,本章节将基于这套成熟的规则模型结构,继续扩展新的抽奖规则能力。

在开发过程中,我们需要通过DefaultLogicFactory和LogicModel为每一条规则标记执行时机(前 / 中 / 后),让规则的加载、执行与调度更加清晰可控,为后续灵活扩展打下基础。

本章节重点实现图中黄色标注的 「抽奖次数过滤」规则扩展: 这条规则的核心作用,是支持为任意奖品配置「抽奖 N 次后解锁」 的能力,解锁门槛直接通过数据库配置管理,实现产品需求中的阶梯式解锁奖品玩法。我们会先完成次数过滤规则的开发,后续章节再继续实现抽奖流程结束后的规则逻辑处理。

三、功能实现

1. 工程结构

  1. 【绿色新增】RuleLockLogicFilter 是本节新增的规则,用于抽奖中进行过滤
  2. 【蓝色修改】把本节新增加的规则在抽奖过程中进行使用。本节判断拦截后,则先返回固定的访问。后续将进行兜底奖品的发放。

2.核心流程

在上一章中,performRaffle 做完前置规则后,直接 getRandomAwardId(strategyId) 返回结果

抽出 awardId 后,又多了两步:

  • 第 74 行:查这个奖品挂了哪些规则 queryStrategyAwardRuleModelVO(strategyId, awardId)
  • 第 77 行:执行中置规则 doCheckRaffleCenterLogic(...)
  • 如果中置规则返回 TAKE_OVER,第 83 行开始就不再把这个奖品直接返回,而是先做拦截处理。现在这个分支里还是临时实现,只返回一段 awardDesc 文案,表示"中奖中规则拦截,后续应走兜底奖励"。
java 复制代码
package cn.bugstack.domain.strategy.service.raffle;

import cn.bugstack.domain.strategy.model.entity.*;
import cn.bugstack.domain.strategy.model.valobj.RuleLogicCheckTypeVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardRuleModelVO;
import cn.bugstack.domain.strategy.repository.IStrategyRepository;
import cn.bugstack.domain.strategy.service.IRaffleStrategy;
import cn.bugstack.domain.strategy.service.armory.IStrategyDispatch;
import cn.bugstack.domain.strategy.service.rule.factory.DefaultLogicFactory;
import cn.bugstack.types.enums.ResponseCode;
import cn.bugstack.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/**
 * @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> ruleActionBeforeEntity = this.doCheckRaffleBeforeLogic(RaffleFactorEntity.builder()
                .userId(userId)
                .strategyId(strategyId)
                .build(), strategy.ruleModels());

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

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

        // 5. 查询奖品规则「抽奖中(拿到奖品ID时,过滤规则)、抽奖后(扣减完奖品库存后过滤,抽奖中拦截和无库存则走兜底)」
        StrategyAwardRuleModelVO strategyAwardRuleModelVO = repository.queryStrategyAwardRuleModelVO(strategyId, awardId);

        // 6. 抽奖中 - 规则过滤
        RuleActionEntity<RuleActionEntity.RaffleCenterEntity> ruleActionCenterEntity = this.doCheckRaffleCenterLogic(RaffleFactorEntity.builder()
                .userId(userId)
                .strategyId(strategyId)
                .awardId(awardId)
                .build(), strategyAwardRuleModelVO.raffleCenterRuleModelList());

        if (RuleLogicCheckTypeVO.TAKE_OVER.getCode().equals(ruleActionCenterEntity.getCode())){
            log.info("【临时日志】中奖中规则拦截,通过抽奖后规则 rule_luck_award 走兜底奖励。");
            return RaffleAwardEntity.builder()
                    .awardDesc("中奖中规则拦截,通过抽奖后规则 rule_luck_award 走兜底奖励。")
                    .build();
        }

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

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

    protected abstract RuleActionEntity<RuleActionEntity.RaffleCenterEntity> doCheckRaffleCenterLogic(RaffleFactorEntity raffleFactorEntity, String... logics);

}

3.新增规则

java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 用户抽奖n次后,对应奖品可解锁抽奖
 * @create 2024-01-13 08:35
 */
@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicModel.RULE_LOCK)
public class RuleLockLogicFilter implements ILogicFilter<RuleActionEntity.RaffleCenterEntity> {

    @Resource
    private IStrategyRepository repository;

    // 用户抽奖次数,后续完成这部分流程开发的时候,从数据库/Redis中读取
    private Long userRaffleCount = 0L;

    @Override
    public RuleActionEntity<RuleActionEntity.RaffleCenterEntity> filter(RuleMatterEntity ruleMatterEntity) {
        log.info("规则过滤-次数锁 userId:{} strategyId:{} ruleModel:{}", ruleMatterEntity.getUserId(), ruleMatterEntity.getStrategyId(), ruleMatterEntity.getRuleModel());

        // 查询规则值配置;当前奖品ID,抽奖中规则对应的校验值。如;1、2、6
        String ruleValue = repository.queryStrategyRuleValue(ruleMatterEntity.getStrategyId(), ruleMatterEntity.getAwardId(), ruleMatterEntity.getRuleModel());
        long raffleCount = Long.parseLong(ruleValue);

        // 用户抽奖次数大于规则限定值,规则放行
        if (userRaffleCount>= raffleCount) {
            return RuleActionEntity.<RuleActionEntity.RaffleCenterEntity>builder()
                    .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                    .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                    .build();
        }

        // 用户抽奖次数小于规则限定值,规则拦截
        return RuleActionEntity.<RuleActionEntity.RaffleCenterEntity>builder()
                .code(RuleLogicCheckTypeVO.TAKE_OVER.getCode())
                .info(RuleLogicCheckTypeVO.TAKE_OVER.getInfo())
                .build();
    }

}

正如我们上一章所做的,新增的规则只要实现了对应的接口以及添加对应的注解,就可以自动进行配置过滤规则到对应的mapper中注册

  • 含义是:用户抽奖达到 N 次后,某个奖品才允许真正中奖。
  • 第 36 行会查当前奖品对应的 rule_lock 配置值,比如 1、2、6。
  • 第 40 行判断 userRaffleCount >= raffleCount
  • 满足就放行,返回 ALLOW。
  • 不满足就拦截,返回 TAKE_OVER

4.实体变更

java 复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RaffleFactorEntity {

    /** 用户ID */
    private String userId;
    /** 策略ID */
    private Long strategyId;
    /** 奖品ID */
    private Integer awardId;

}

新增了 awardId 字段。上一章只有 userId + strategyId,够做前置规则;要判断"某个奖品"的规则,所以必须把 awardId 带上。

java 复制代码
@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 {

    }

}

原先就预留了 RaffleCenterEntity,这章正式开始用这层泛型类型,表示"抽奖中"的规则动作结果。

java 复制代码
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StrategyAwardRuleModelVO {

    private String ruleModels;

    /**
     * 获取抽奖中规则;或者使用 lambda 表达式
     * <p>
     * List<String> ruleModelList = Arrays.stream(ruleModels.split(Constants.SPLIT))
     * .filter(DefaultLogicFactory.LogicModel::isCenter)
     * .collect(Collectors.toList());
     * return ruleModelList;
     * <p>
     * List<String> collect = Arrays.stream(ruleModelValues).filter(DefaultLogicFactory.LogicModel::isCenter).collect(Collectors.toList());
     */
    public String[] raffleCenterRuleModelList() {
        List<String> ruleModelList = new ArrayList<>();
        String[] ruleModelValues = ruleModels.split(Constants.SPLIT);
        for (String ruleModelValue : ruleModelValues) {
            if (DefaultLogicFactory.LogicModel.isCenter(ruleModelValue)) {
                ruleModelList.add(ruleModelValue);
            }
        }
        return ruleModelList.toArray(new String[0]);
    }

    public String[] raffleAfterRuleModelList() {
        List<String> ruleModelList = new ArrayList<>();
        String[] ruleModelValues = ruleModels.split(Constants.SPLIT);
        for (String ruleModelValue : ruleModelValues) {
            if (DefaultLogicFactory.LogicModel.isAfter(ruleModelValue)) {
                ruleModelList.add(ruleModelValue);
            }
        }
        return ruleModelList.toArray(new String[0]);
    }

}

专门解析奖品表里的 rule_models

  • 第 38 行的 raffleCenterRuleModelList() 会筛出中置规则
  • 第 49 行的 raffleAfterRuleModelList() 会筛出后置规则
java 复制代码
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 规则工厂
 * @create 2023-12-31 11:23
 */
@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", "before"),
        RULE_BLACKLIST("rule_blacklist", "【抽奖前规则】黑名单规则过滤,命中黑名单则直接返回", "before"),
        RULE_LOCK("rule_lock", "【抽奖中规则】抽奖n次后,对应奖品可解锁抽奖", "center"),
        RULE_LUCK_AWARD("rule_luck_award", "【抽奖后规则】抽奖n次后,对应奖品可解锁抽奖", "after"),
        ;

        private final String code;
        private final String info;
        private final String type;

        public static boolean isCenter(String code){
            return "center".equals(LogicModel.valueOf(code.toUpperCase()).type);
        }

        public static boolean isAfter(String code){
            return "after".equals(LogicModel.valueOf(code.toUpperCase()).type);
        }

    }

}

LogicModel 也升级了:

  • 上一章只有 RULE_WIGHT、RULE_BLACKLIST
  • 这一章新增 RULE_LOCK、RULE_LUCK_AWARD
  • 并且多了 type 字段,把规则分成 before / center / after
  • 提供了 isCenter、isAfter,方便按阶段筛规则

5.默认抽奖策略变更

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) {
        if (logics == null || 0 == logics.length) return RuleActionEntity.<RuleActionEntity.RaffleBeforeEntity>builder()
                .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                .build();

        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(raffleFactorEntity.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;
    }

    @Override
    protected RuleActionEntity<RuleActionEntity.RaffleCenterEntity> doCheckRaffleCenterLogic(RaffleFactorEntity raffleFactorEntity, String... logics) {
        if (logics == null || 0 == logics.length) return RuleActionEntity.<RuleActionEntity.RaffleCenterEntity>builder()
                .code(RuleLogicCheckTypeVO.ALLOW.getCode())
                .info(RuleLogicCheckTypeVO.ALLOW.getInfo())
                .build();

        Map<String, ILogicFilter<RuleActionEntity.RaffleCenterEntity>> logicFilterGroup = logicFactory.openLogicFilter();

        RuleActionEntity<RuleActionEntity.RaffleCenterEntity> ruleActionEntity = null;
        for (String ruleModel : logics) {
            ILogicFilter<RuleActionEntity.RaffleCenterEntity> logicFilter = logicFilterGroup.get(ruleModel);
            RuleMatterEntity ruleMatterEntity = new RuleMatterEntity();
            ruleMatterEntity.setUserId(raffleFactorEntity.getUserId());
            ruleMatterEntity.setAwardId(raffleFactorEntity.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;
    }

}

在 doCheckRaffleBeforeLogic(...) 和 doCheckRaffleCenterLogic(...) 的开头,都补充了空规则保护逻辑。也就是说,当当前策略没有配置前置规则或中置规则时,方法会直接返回 ALLOW,这样既能保持流程可继续执行,也避免了后续遍历规则集合时出现空指针问题。

在抽奖前规则的顺序过滤部分,还修正了 awardId 的赋值方式。原来的写法是从 ruleMatterEntity 自身读取 awardId,但这个对象在创建时并没有提前完成赋值,因此这里实际上拿不到有效值。修改后改为从外部传入的 raffleFactorEntity 中获取 awardId,这样规则物料对象就能够正确携带奖品 ID,为后续规则判断提供准确入参。

另外,这个版本新增了 doCheckRaffleCenterLogic(...) 方法,用来处理"抽奖中规则过滤"。它整体沿用了抽奖前规则的实现方式:先通过 DefaultLogicFactory 获取规则过滤器集合,再按顺序遍历当前奖品绑定的规则模型,把用户 ID、策略 ID、奖品 ID、规则模型统一封装到 RuleMatterEntity 中,然后交给对应的 ILogicFilter 实现类处理。一旦某条规则返回的结果不是 ALLOW,就立即中断后续判断并返回当前结果。

换句话说,这个分支并没有重新设计一套新的规则处理机制,而是完整复用了前一版本已经建立好的"工厂 + 过滤器 + RuleActionEntity"结构,只是把这套规则处理模式从"抽奖前"扩展到了"抽奖中"

6.仓储层变更

相关推荐
工业机器人销售服务2 小时前
不锈钢制品美容焊手:法奥机器人施焊成型焊缝色泽均匀,防腐性能与母材保持一致
大数据·人工智能
code 小楊2 小时前
2026两大新王对决:Qwen3\.7\-Max vs Gemini 3\.5 Flash 全维度深度测评(能力、对比、选型、优劣)
大数据·人工智能
数学建模导师3 小时前
2026电工杯A题电—氢—氨”耦合系统完整版解答含论文!
大数据·人工智能·数学建模
ai_xiaogui3 小时前
一人公司AI项目真实性如何验证?
大数据·aistarter·panelai·一人公司·ai项目验证·可落地的ai项目·本地ai部署工具
Sharewinfo_BJ3 小时前
从手工报表到实时BI:一个零售数据平台的踩坑与重构实战
大数据·人工智能·科技·数据分析·微软·powerbi
Elastic 中国社区官方博客3 小时前
在 Elasticsearch 中,存储向量查询速度最高提升 3 倍
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索
想ai抽3 小时前
StarRocks 存储引擎设计深度调研笔记
大数据·starrocks·olap
云登指纹浏览器4 小时前
2026静态IP和动态IP怎么选?跨境电商场景应用完整指南
大数据·网络协议·tcp/ip
vx153027823624 小时前
CDGA|企业数据治理中,AI权限该如何拿捏分寸
大数据·人工智能·cdga·数据治理