大营销平台 —— 策略权重概率装配
一、前言上一期我们实现了在简单条件下的抽奖概率装配这是基于没有任何策略规则约束的情况下的简单场景虽然代码比较复杂但搞清楚逻辑后还是比较容易实现的。这一期是基于上一期的简易抽奖来进行改进的这次我们将有策略规则约束了这一起我们主要讲权重规则的装配这个权重规则其实就是保底机制消耗到指定的积分就能从其中的几个奖品中直接中奖而不会再只获得随机积分了二、接口抽离在开始项目前先将兵工厂中的装配方法和抽奖方法分离开因为我们马上要对抽奖方法进行改动了如果不把这两个方法分离开就会让接口的分工不明确了。这里我们选择将抽奖部分重新创建一个接口但是注意我们的实现类是不变的也就是多个接口的方法都在同一个armory兵工厂中进行实现因为我们只是想在调用接口的时候分工更明确并不在乎这些接口实现在哪里/** * author 印东升 * description 策略抽奖调度 * create 2026-04-09 11:18 */ public interface IStrategyDispatch { /** * 获取抽奖策略的随机结果 * param strategyId * return */ Integer getRandomAwardId(Long strategyId); Integer getRandomAwardId(Long strategyId ,String ruleWeightValue); }三、策略规则装配1.抽离简易抽奖方法这一段是昨天写的方法我们将它抽离出来便于后续我们在这个基础上继续做规则约束处理。private void assembleLotteryStrategy(String key, ListStrategyAwardEntity strategyAwardEntities) { //1.获取最小概率值-使用stream流 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集合中存放上对应的奖品占位即可占位越多等于概率越高」 ArrayListInteger strategyAwardSearchRateTables new ArrayList(rateRange.intValue()); for (StrategyAwardEntity strategyAward : strategyAwardEntities) { Integer awardId strategyAward.getAwardId(); BigDecimal awardRate strategyAward.getAwardRate(); //计算出每个概率值需要存放到查找表的数量循环填充[原版的这里有问题假设同策略id的库中的概率和不为1这里必然填不满概率查找表] for (int i 0; i awardRate.divide(minAwardRate, 0, RoundingMode.CEILING).intValue(); i) { strategyAwardSearchRateTables.add(awardId); } } //5.乱序 Collections.shuffle(strategyAwardSearchRateTables); //6.成出Map集合key值对应的就是后续的概率值。通过概率来获得对应的奖品ID HashMapInteger, Integer shuffleStrategyAwardSearchRateTables new HashMap(); for (int i 0; i strategyAwardSearchRateTables.size(); i) { shuffleStrategyAwardSearchRateTables.put(i, strategyAwardSearchRateTables.get(i)); } //7.存储到Redis repository.storeStrategyAwardRateTables(key, rateRange, shuffleStrategyAwardSearchRateTables); }2.实现权重装配这里的第二行代码就是将我们刚刚抽离的方法进行使用也就是从第三行开始就是策略权重装配了。首先我们要在这里明确一下整体的流程不然后面会越写越乱1.根据当前策略查询策略表看看当前策略的所有策略规则2.判断其中是否有权重规则3.如果有权重规则就获取权重配置比如4000102,103,104就是一组权重配置我们用map存储key是4000value是[102,103,104]数组4.将key抽出生成一个集合5.遍历集合剔除所有不符合当前权重配置的奖品6.最后再次调用先前封装的概率装配方法只是这次只会将剔除后剩下奖品存入redis了整体代码如下其实理清楚逻辑还是比较容易理解的Override //装配抽奖策概率 public boolean assembleLotteryStrategy(Long strategyId) { //1.查询策略配置 ListStrategyAwardEntity 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()); } MapString, ListInteger ruleWeightValueMap strategyRuleEntity.getRuleWeightValues(); SetString keys ruleWeightValueMap.keySet(); for (String key : keys) { ListInteger ruleWeightValues ruleWeightValueMap.get(key); ArrayListStrategyAwardEntity strategyAwardEntitiesClone new ArrayList(strategyAwardEntities); strategyAwardEntitiesClone.removeIf(entity-!ruleWeightValues.contains(entity.getAwardId())); assembleLotteryStrategy(String.valueOf(strategyId).concat(_).concat(key),strategyAwardEntitiesClone); } return true; }3.充血模型这里是第一次遇到充血模型所以首先给出DDD中充血模型的定义充血模型是领域驱动设计DDD中的一种领域模型设计模式其核心特征是将数据和行为封装在同一个领域对象中领域对象不仅包含属性数据还包含业务逻辑行为。其实说简单一点就是把一些处理成员变量的方法放在了对象内部集中管理。这样就便于维护同时service中就可以少写一些处理成员变量的代码了直接调用对象内部的方法可以让整体逻辑更加清晰。那哪些方法可以放在充血模型中呢核心原则保护业务不变性凡是涉及业务规则、状态一致性、计算逻辑的成员变量处理都应该放到充血模型中。我们这里使用了两个充血模型一个是StrategyEntity一个是StrategyRuleEntity。Data Builder AllArgsConstructor NoArgsConstructor public class StrategyEntity { /** * 抽奖策略id */ private Long strategyId; /** * 抽奖策略描述 */ private String strategyDesc; /** * 抽奖规则模型 */ 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; } }Data Builder AllArgsConstructor NoArgsConstructor public class StrategyRuleEntity { /** * 策略id */ private Long strategyId; /** * 抽奖奖品id */ private Integer awardId; /** * 抽奖规则类型 */ private Integer ruleType; /** * 抽象规则类型 */ private String ruleModel; /** * 抽奖规则比值 */ private String ruleValue; /** * 抽奖规则描述 */ private String ruleDesc; /** * 按照表中的格式来获取权重配置map * 数据案例4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109 */ public MapString, ListInteger getRuleWeightValues() { if (!rule_weight.equals(ruleModel)) return null; String[] ruleValueGroups ruleValue.split(Constants.SPACE); MapString, ListInteger 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(com.bigmarket.types.common.Constants.SPLIT); ListInteger values new ArrayList(); for (String valueString : valueStrings) { values.add(Integer.parseInt(valueString)); } // 将键和值放入Map中 resultMap.put(ruleValueGroup, values); } return resultMap; } }现在来看看为啥这俩要做成充血模型StrategyEntity其中的ruleModels成员变量是用逗号分隔的伪数组如果想要单独获取每个ruleModel就需要分隔这个行为是和成员变量紧密联系的属于计算逻辑所以直接选择聚合在内部处理。StrategyRuleEntity:其中的ruleValue成员变量是类似于map的伪键值对我们现在需要把这个伪键值对放到一个map中去resultMap这个行为也是属于计算逻辑所以也直接聚合到充血模型内部。4.仓储优化这两个方法都是会用到的上面的代码中其实已经用到了所以仓储中需要获取两个充血模型当然需要调用Dao查实体类表了最后将实体类封装成充血模型返回即可。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 strategyRule strategyRuleDao.queryStrategyRule(strategyRuleReq); return StrategyRuleEntity.builder() .strategyId(strategyRule.getStrategyId()) .awardId(strategyRule.getAwardId()) .ruleType(strategyRule.getRuleType()) .ruleModel(strategyRule.getRuleModel()) .ruleValue(strategyRule.getRuleValue()) .ruleDesc(strategyRule.getRuleDesc()) .build(); }同时下面两个方法也需要优化因为现在我们需要从过滤后的奖品中查询所以也就不能通过strategyId直接查询了应该查询map中对应的key//获取概率范围 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); }5.测试最后测试先执行策略装配后打印不同权重配置下的随机奖品。Before public void test_strategyArmory(){ strategyArmory.assembleLotteryStrategy(100001L); } Test public void test_getRandomAwardId_ruleWeightValue(){ log.info(测试结果{}-4000 策略配置,strategyDispatch.getRandomAwardId(100001L,4000:102,103,104,105)); log.info(测试结果{}-5000 策略配置,strategyDispatch.getRandomAwardId(100001L,5000:102,103,104,105,106,107)); log.info(测试结果{}-6000 策略配置,strategyDispatch.getRandomAwardId(100001L,6000:102,103,104,105,106,107,108,109)); }测试结果如下