news 2026/5/20 18:58:57

《大营销平台系统设计实现》 - 营销服务 第4节:策略权重概率装配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《大营销平台系统设计实现》 - 营销服务 第4节:策略权重概率装配

一、本章诉求

在大营销平台的抽奖子模块中,需要满足用户抽奖N积分后,可中奖范围的设定。也就是说你总共消耗了6000积分抽奖了,那么接下来的抽奖就会有圈定到固定的奖品范围,不会让用户再抽到过低价值的奖品。那么这就需要我们在设计系统实现的时候,处理下不同策略规则权重的概率装配。

二、流程设计

在流程实现中,我们需要结合上一节中的整体概率装配,按照接口单一原则进行拆分出装配和使用。之后在装配接口中重构装配操作,满足对权重策略的装配处理。—— 这里会在实体对象中填充充血方法。

  1. 集合着梳理的系统设计流程,将后续需要用到的权重抽奖规则,进行提前装配处理。
  2. 所有装配的数据都会存放到 Redis Map 数据结构下。对于权重的策略装配为策略ID+权重值组合。
  3. 最终用户在从装配的工厂中执行抽奖的时候,则可以通过策略ID抽奖和策略ID+权重值组合的方式抽奖。

三、功能实现

1. 工程结构

  1. 首先,拆分装配接口,一个是装配接口,一个是调度接口。这样可以保持接口的但一直职责,避免使用方调用装配操作。装配是活动在创建或者审核的时候初始化的装配动作
  2. 本节会在实体对象中添加属于实体本身的功能,这样会让代码更加干净,也会体现出充血的作用

2.充血实体

/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略实体 * @create 2023-12-31 15:24 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class StrategyEntity { /** 抽奖策略ID */ private Long strategyId; /** 抽奖策略描述 */ private String strategyDesc; /** 抽奖规则模型 rule_weight,rule_blacklist */ private String ruleModels; //获取全部规则 public String[] ruleModels() { if (StringUtils.isBlank(ruleModels)) return null; return ruleModels.split(Constants.SPLIT); } //获得规则权重那个规则 public String getRuleWeight() { String[] ruleModels = this.ruleModels(); for (String ruleModel : ruleModels) { if ("rule_weight".equals(ruleModel)) return ruleModel; } return null; } }
package cn.bugstack.domain.strategy.model.entity; import cn.bugstack.types.common.Constants; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略规则实体 * @create 2023-12-31 15:32 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class StrategyRuleEntity { /** 抽奖策略ID */ private Long strategyId; /** 抽奖奖品ID【规则类型为策略,则不需要奖品ID】 */ private Integer awardId; /** 抽象规则类型;1-策略规则、2-奖品规则 */ private Integer ruleType; /** 抽奖规则类型【rule_random - 随机值计算、rule_lock - 抽奖几次后解锁、rule_luck_award - 幸运奖(兜底奖品)】 */ private String ruleModel; /** 抽奖规则比值 */ private String ruleValue; /** 抽奖规则描述 */ private String ruleDesc; /** * 获取权重值 * 数据案例;4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109 */ public Map<String, List<Integer>> getRuleWeightValues() { if (!"rule_weight".equals(ruleModel)) return null; String[] ruleValueGroups = ruleValue.split(Constants.SPACE); Map<String, List<Integer>> resultMap = new HashMap<>(); for (String ruleValueGroup : ruleValueGroups) { // 检查输入是否为空 if (ruleValueGroup == null || ruleValueGroup.isEmpty()) { return resultMap; } // 分割字符串以获取键和值 String[] parts = ruleValueGroup.split(Constants.COLON); if (parts.length != 2) { throw new IllegalArgumentException("rule_weight rule_rule invalid input format" + ruleValueGroup); } // 解析值 String[] valueStrings = parts[1].split(Constants.SPLIT); List<Integer> values = new ArrayList<>(); for (String valueString : valueStrings) { values.add(Integer.parseInt(valueString)); } // 将键和值放入Map中 resultMap.put(ruleValueGroup, values); } return resultMap; } }

获得权重值map(

ruleValue(抽奖规则比值) 对应: 4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109
ruleModel 对应:rule_weight"

3.接口拆分

将原来的一个接口拆分为俩个接口(一个是装配接口,一个是调度接口)实现单一职责

/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略抽奖调度 * @create 2023-12-31 15:15 */ public interface IStrategyDispatch { /** * 获取抽奖策略装配的随机结果 * * @param strategyId 策略ID * @return 抽奖结果 */ Integer getRandomAwardId(Long strategyId); Integer getRandomAwardId(Long strategyId, String ruleWeightValue); }

增加了按照权重的抽奖策略

/** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略装配库(兵工厂),负责初始化策略计算 * @create 2023-12-23 09:44 */ public interface IStrategyArmory { /** * 装配抽奖策略配置「触发的时机可以为活动审核通过后进行调用」 * * @param strategyId 策略ID * @return 装配结果 */ boolean assembleLotteryStrategy(Long strategyId); }

然后这里实现俩个接口

之前是这样的,职责没有划分,不好管理

4.仓储功能新增

package cn.bugstack.infrastructure.persistent.repository; import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity; import cn.bugstack.domain.strategy.model.entity.StrategyEntity; import cn.bugstack.domain.strategy.model.entity.StrategyRuleEntity; import cn.bugstack.domain.strategy.repository.IStrategyRepository; import cn.bugstack.infrastructure.persistent.dao.IStrategyAwardDao; import cn.bugstack.infrastructure.persistent.dao.IStrategyDao; import cn.bugstack.infrastructure.persistent.dao.IStrategyRuleDao; import cn.bugstack.infrastructure.persistent.po.Strategy; import cn.bugstack.infrastructure.persistent.po.StrategyAward; import cn.bugstack.infrastructure.persistent.po.StrategyRule; import cn.bugstack.infrastructure.persistent.redis.IRedisService; import cn.bugstack.types.common.Constants; import org.springframework.stereotype.Repository; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略服务仓储实现 * @create 2023-12-23 10:33 */ @Repository public class StrategyRepository implements IStrategyRepository { @Resource private IStrategyDao strategyDao; @Resource private IStrategyRuleDao strategyRuleDao; @Resource private IStrategyAwardDao strategyAwardDao; @Resource private IRedisService redisService; @Override public List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId) { // 优先从缓存获取 String cacheKey = Constants.RedisKey.STRATEGY_AWARD_KEY + strategyId; List<StrategyAwardEntity> strategyAwardEntities = redisService.getValue(cacheKey); if (null != strategyAwardEntities && !strategyAwardEntities.isEmpty()) return strategyAwardEntities; // 从库中获取数据 List<StrategyAward> strategyAwards = strategyAwardDao.queryStrategyAwardListByStrategyId(strategyId); strategyAwardEntities = new ArrayList<>(strategyAwards.size()); for (StrategyAward strategyAward : strategyAwards) { StrategyAwardEntity strategyAwardEntity = StrategyAwardEntity.builder() .strategyId(strategyAward.getStrategyId()) .awardId(strategyAward.getAwardId()) .awardCount(strategyAward.getAwardCount()) .awardCountSurplus(strategyAward.getAwardCountSurplus()) .awardRate(strategyAward.getAwardRate()) .build(); strategyAwardEntities.add(strategyAwardEntity); } redisService.setValue(cacheKey, strategyAwardEntities); return strategyAwardEntities; } @Override public void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map<Integer, Integer> strategyAwardSearchRateTable) { // 1. 存储抽奖策略范围值,如10000,用于生成1000以内的随机数 redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key, rateRange); // 2. 存储概率查找表 Map<Integer, Integer> cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key); cacheRateTable.putAll(strategyAwardSearchRateTable); } @Override public Integer getStrategyAwardAssemble(String key, Integer rateKey) { return redisService.getFromMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key, rateKey); } @Override public int getRateRange(Long strategyId) { return getRateRange(String.valueOf(strategyId)); } @Override public int getRateRange(String key) { return redisService.getValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key); } @Override public StrategyEntity queryStrategyEntityByStrategyId(Long strategyId) { // 优先从缓存获取 String cacheKey = Constants.RedisKey.STRATEGY_KEY + strategyId; StrategyEntity strategyEntity = redisService.getValue(cacheKey); if (null != strategyEntity) return strategyEntity; Strategy strategy = strategyDao.queryStrategyByStrategyId(strategyId); strategyEntity = StrategyEntity.builder() .strategyId(strategy.getStrategyId()) .strategyDesc(strategy.getStrategyDesc()) .ruleModels(strategy.getRuleModels()) .build(); redisService.setValue(cacheKey, strategyEntity); return strategyEntity; } @Override public StrategyRuleEntity queryStrategyRule(Long strategyId, String ruleModel) { StrategyRule strategyRuleReq = new StrategyRule(); strategyRuleReq.setStrategyId(strategyId); strategyRuleReq.setRuleModel(ruleModel); StrategyRule strategyRuleRes = strategyRuleDao.queryStrategyRule(strategyRuleReq); return StrategyRuleEntity.builder() .strategyId(strategyRuleRes.getStrategyId()) .awardId(strategyRuleRes.getAwardId()) .ruleType(strategyRuleRes.getRuleType()) .ruleModel(strategyRuleRes.getRuleModel()) .ruleValue(strategyRuleRes.getRuleValue()) .ruleDesc(strategyRuleRes.getRuleDesc()) .build(); } }

1. int getRateRange(String key);
作用:按字符串 key 读取某张概率表的范围值
之前只有这种用法:

getRateRange(Long strategyId)

只能拿默认策略表的范围,比如:

100001

但加了权重规则后,Redis 里不止一张表了,还会有这种 key:

100001_4000:102,103,104,105 100001_5000:102,103,104,105,106,107

所以就需要支持按字符串 key 取范围,而不是只按 strategyId。
它服务的是这种场景:

  • 默认抽奖:查默认概率表范围
  • 权重抽奖:查某个权重子表范围

2. StrategyEntity queryStrategyEntityByStrategyId(Long strategyId);
作用:查询策略主配置
这个方法是为了拿到策略本身配置了哪些规则,比如:

rule_weight,rule_blacklist

StrategyArmoryDispatch会先查策略:

StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId); String ruleWeight = strategyEntity.getRuleWeight();

也就是说,它的用途是:

  • 先判断这个策略有没有启用 rule_weight
  • 如果没有,就只装默认概率表
  • 如果有,再继续查规则明细做权重装配

3. StrategyRuleEntity queryStrategyRule(Long strategyId, String ruleModel);
作用:查询某个策略下某种规则的详细配置
比如:

  • strategyId = 100001
  • ruleModel = "rule_weight"

查出来的可能就是:

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

然后交给 StrategyRuleEntity 去解析成:

  • 哪些权重档
  • 每档包含哪些奖品 ID

这个方法的意义就是:
拿到权重规则的原始配置明细,没有它,就不知道每个权重档对应哪些奖品。

5.主要装配和抽奖逻辑功能

assembleLotteryStrategy 逻辑变了

旧版流程很简单:

  1. 查策略奖品列表
  2. 算概率表
  3. 存 Redis
  4. 结束

新版流程变成:

  1. 先查奖品列表
  2. 先装配一份默认概率表
  3. 再查策略主配置 StrategyEntity
  4. 判断有没有 rule_weight
  5. 如果有,再查 StrategyRuleEntity
  6. 解析出每个权重档位
  7. 每个档位筛出自己的奖品集合
  8. 为每个档位再单独装配一张概率表

对应装配和抽奖代码实现

package cn.bugstack.domain.strategy.service.armory; import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity; import cn.bugstack.domain.strategy.model.entity.StrategyEntity; import cn.bugstack.domain.strategy.model.entity.StrategyRuleEntity; import cn.bugstack.domain.strategy.repository.IStrategyRepository; import cn.bugstack.types.enums.ResponseCode; import cn.bugstack.types.exception.AppException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.security.SecureRandom; import java.util.*; /** * @author Fuzhengwei bugstack.cn @小傅哥 * @description 策略装配库(兵工厂),负责初始化策略计算 * @create 2023-12-23 10:02 */ @Slf4j @Service public class StrategyArmoryDispatch implements IStrategyArmory, IStrategyDispatch { @Resource private IStrategyRepository repository; @Override public boolean assembleLotteryStrategy(Long strategyId) { // 1. 查询策略配置 List<StrategyAwardEntity> strategyAwardEntities = repository.queryStrategyAwardList(strategyId); assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities); // 2. 权重策略配置 - 适用于 rule_weight 权重规则配置 StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId); String ruleWeight = strategyEntity.getRuleWeight(); if (null == ruleWeight) return true; StrategyRuleEntity strategyRuleEntity = repository.queryStrategyRule(strategyId, ruleWeight); if (null == strategyRuleEntity) { throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo()); } Map<String, List<Integer>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues(); Set<String> keys = ruleWeightValueMap.keySet(); for (String key : keys) { List<Integer> ruleWeightValues = ruleWeightValueMap.get(key); ArrayList<StrategyAwardEntity> strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities); strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId())); assembleLotteryStrategy(String.valueOf(strategyId).concat("_").concat(key), strategyAwardEntitiesClone); } return true; } private void assembleLotteryStrategy(String key, List<StrategyAwardEntity> strategyAwardEntities) { // 1. 获取最小概率值 BigDecimal minAwardRate = strategyAwardEntities.stream() .map(StrategyAwardEntity::getAwardRate) .min(BigDecimal::compareTo) .orElse(BigDecimal.ZERO); // 2. 获取概率值总和 BigDecimal totalAwardRate = strategyAwardEntities.stream() .map(StrategyAwardEntity::getAwardRate) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. 用 1 % 0.0001 获得概率范围,百分位、千分位、万分位 BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING); // 4. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」 List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue()); for (StrategyAwardEntity strategyAward : strategyAwardEntities) { Integer awardId = strategyAward.getAwardId(); BigDecimal awardRate = strategyAward.getAwardRate(); // 计算出每个概率值需要存放到查找表的数量,循环填充 for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) { strategyAwardSearchRateTables.add(awardId); } } // 5. 对存储的奖品进行乱序操作 Collections.shuffle(strategyAwardSearchRateTables); // 6. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>(); for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) { shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i)); } // 7. 存放到 Redis repository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable); } @Override public Integer getRandomAwardId(Long strategyId) { // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 int rateRange = repository.getRateRange(strategyId); // 通过生成的随机值,获取概率值奖品查找表的结果 return repository.getStrategyAwardAssemble(String.valueOf(strategyId), new SecureRandom().nextInt(rateRange)); } @Override public Integer getRandomAwardId(Long strategyId, String ruleWeightValue) { String key = String.valueOf(strategyId).concat("_").concat(ruleWeightValue); // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 int rateRange = repository.getRateRange(key); // 通过生成的随机值,获取概率值奖品查找表的结果 return repository.getStrategyAwardAssemble(key, new SecureRandom().nextInt(rateRange)); } }

抽奖逻辑也分为权重抽奖和普通抽奖

对应测试方法

抽奖测试

装配测试

所以这里@Before本质是在做:测试初始化 / 测试前准备数据

注意,不是整个类只跑一次,而是每个测试方法前都跑一次

@RunWith(SpringRunner.class)的意思是:
让这个测试类不要用默认 JUnit 方式跑,而是交给 Spring 提供的测试运行器来跑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/20 18:55:06

5分钟掌握终极音频格式转换:Unlock Music完整指南

5分钟掌握终极音频格式转换&#xff1a;Unlock Music完整指南 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库&#xff1a; 1. https://github.com/unlock-music/unlock-music &#xff1b;2. https://git.unlock-music.dev/um/web 项目地址: https://g…

作者头像 李华
网站建设 2026/5/20 18:55:01

CANN Ascend C Atan接口文档

Atan 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言&#xff0c;原生支持C和C标准规范&#xff0c;主要由类库和语言扩展层构成&#xff0c;提供多层级API&#xff0c;满足多维场景算子开发诉求。 项目地址: https://gitcode.com/cann/…

作者头像 李华
网站建设 2026/5/20 18:54:54

【Maxwell 边界条件速查表】一篇分清气球 / 对称 / 奇 / 偶边界

一、核心边界条件对比表边界条件核心作用适用场景关键特点Balloon Boundary&#xff08;气球边界&#xff09;模拟无穷远边界&#xff0c;大幅减小求解域尺寸漏磁计算、磁饱和模型、远处磁场分析磁场既不垂直也不平行于边界&#xff1b;无额外参数&#xff0c;仅需命名Symmetry…

作者头像 李华
网站建设 2026/5/20 18:54:05

BiliTools:重新定义B站内容消费的技术解决方案

BiliTools&#xff1a;重新定义B站内容消费的技术解决方案 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱&#xff0c;支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools 你是否曾…

作者头像 李华