一、本章诉求
电商平台的抽奖营销系统业务逻辑复杂度较高,以拼多多类平台为例,抽奖玩法场景丰富。项目运营中后期,产品常会围绕积分消耗、提升用户抽奖参与频次优化玩法,典型策略为阶梯式解锁奖品池:用户初始抽奖仅可抽取 1-6 档奖品,累计抽奖 3 次解锁 1-7 档,累计 6 次进一步开放 1-9 档,利用逐级扩容奖品范围的机制,驱动用户主动消耗积分、反复参与抽奖。
基于这类多变的业务需求,抽奖系统设计需遵循松耦合原则,参考 Spring 拆解 Bean 的分层思想,将完整抽奖流程拆分多个独立执行阶段。各阶段可灵活插拔新增业务逻辑,大幅降低后续版本迭代成本,避免架构耦合度过高,引发每次需求改动都大面积改动代码的问题。
二、流程设计
在流程实现中,设计出抽奖的前中后置过程,并在每个阶段设计对应的操作规则。当你这样设计以后,你会发现整个抽奖功能实现变得非常灵活好扩展。

在上一章节中,我们已经搭建好了抽奖规则的核心实现框架,本章节将基于这套成熟的规则模型结构,继续扩展新的抽奖规则能力。
在开发过程中,我们需要通过DefaultLogicFactory和LogicModel为每一条规则标记执行时机(前 / 中 / 后),让规则的加载、执行与调度更加清晰可控,为后续灵活扩展打下基础。
本章节重点实现图中黄色标注的 「抽奖次数过滤」规则扩展: 这条规则的核心作用,是支持为任意奖品配置「抽奖 N 次后解锁」 的能力,解锁门槛直接通过数据库配置管理,实现产品需求中的阶梯式解锁奖品玩法。我们会先完成次数过滤规则的开发,后续章节再继续实现抽奖流程结束后的规则逻辑处理。
三、功能实现
1. 工程结构

- 【绿色新增】RuleLockLogicFilter 是本节新增的规则,用于抽奖中进行过滤
- 【蓝色修改】把本节新增加的规则在抽奖过程中进行使用。本节判断拦截后,则先返回固定的访问。后续将进行兜底奖品的发放。
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.仓储层变更


