Преглед на файлове

feat: 模板模式处理抽奖流程

seamew преди 2 години
родител
ревизия
7683ea9712
променени са 17 файла, в които са добавени 377 реда и са изтрити 141 реда
  1. 74 3
      lottery-common/src/main/java/com/seamew/lottery/common/Constants.java
  2. 0 40
      lottery-common/src/main/java/com/seamew/lottery/common/enumeration/StrategyModeEnum.java
  3. 3 2
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/annotation/StrategyMode.java
  4. 22 6
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/model/res/DrawResult.java
  5. 12 0
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/repository/IStrategyRepository.java
  6. 14 0
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/repository/impl/StrategyRepository.java
  7. 10 0
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/BaseAlgorithm.java
  8. 5 7
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/impl/EntiretyRateRandomDrawAlgorithm.java
  9. 3 7
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/impl/SingleRateRandomDrawAlgorithm.java
  10. 116 0
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/AbstractDrawBase.java
  11. 0 35
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawBase.java
  12. 5 3
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawConfig.java
  13. 40 0
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawStrategySupport.java
  14. 28 37
      lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/impl/DrawExecImpl.java
  15. 5 0
      lottery-interfaces/pom.xml
  16. 2 1
      lottery-interfaces/src/test/java/com/seamew/lottery/test/ApiTest.java
  17. 38 0
      lottery-interfaces/src/test/java/com/seamew/lottery/test/DrawAlgorithmTest.java

+ 74 - 3
lottery-common/src/main/java/com/seamew/lottery/common/Constants.java

@@ -10,9 +10,9 @@ package com.seamew.lottery.common;
 public class Constants {
     public enum ResponseCode {
         SUCCESS("0000", "成功"),
-        UN_ERROR("0001","未知失败"),
-        ILLEGAL_PARAMETER("0002","非法参数"),
-        INDEX_DUP("0003","主键冲突");
+        UN_ERROR("0001", "未知失败"),
+        ILLEGAL_PARAMETER("0002", "非法参数"),
+        INDEX_DUP("0003", "主键冲突");
 
         private final String code;
         private final String info;
@@ -32,4 +32,75 @@ public class Constants {
 
     }
 
+    /**
+     * 抽奖策略模式:总体概率、单项概率
+     * 场景:两种抽奖算法描述,场景A20%、B30%、C50%
+     * 单项概率:如果A奖品抽空后,B和C保持目前中奖概率,用户抽奖扔有20%中为A,因A库存抽空则结果展示为未中奖。为了运营成本,通常这种情况的使用的比较多
+     * 总体概率:如果A奖品抽空后,B和C奖品的概率按照 3:5 均分,相当于B奖品中奖概率由 0.3 升为 0.375
+     */
+    public enum StrategyMode {
+
+        /**
+         * 单项概率:如果A奖品抽空后,B和C保持目前中奖概率,用户抽奖扔有20%中为A,因A库存抽空则结果展示为未中奖。为了运营成本,通常这种情况的使用的比较多
+         */
+        SINGLE(1, "单项概率"),
+
+        /**
+         * 总体概率:如果A奖品抽空后,B和C奖品的概率按照 3:5 均分,相当于B奖品中奖概率由 0.3 升为 0.375
+         */
+        ENTIRETY(2, "总体概率");
+
+        private final Integer code;
+        private final String info;
+
+        StrategyMode(Integer code, String info) {
+            this.code = code;
+            this.info = info;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getInfo() {
+            return info;
+        }
+    }
+
+    /**
+     * 中奖状态:0未中奖、1已中奖、2兜底奖
+     */
+    public enum DrawState {
+        /**
+         * 未中奖
+         */
+        FAIL(0, "未中奖"),
+
+        /**
+         * 已中奖
+         */
+        SUCCESS(1, "已中奖"),
+
+        /**
+         * 兜底奖
+         */
+        Cover(2, "兜底奖");
+
+        private final Integer code;
+        private final String info;
+
+        DrawState(Integer code, String info) {
+            this.code = code;
+            this.info = info;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getInfo() {
+            return info;
+        }
+
+    }
 }

+ 0 - 40
lottery-common/src/main/java/com/seamew/lottery/common/enumeration/StrategyModeEnum.java

@@ -1,40 +0,0 @@
-package com.seamew.lottery.common.enumeration;
-
-/**
- * @Author: seamew
- * @Title: StrategyModeEnum
- * @CreateTime: 2023年02月13日 21:23:00
- * @Description: 抽奖策略枚举
- * @Version: 1.0
- */
-public enum StrategyModeEnum {
-    DEFAULT_RATE_RANDOM_DRAW_ALGORITHM(1, "必中奖策略抽奖,排掉已经中奖的概率,重新计算中奖范围"),
-
-    SINGLE_RATE_RANDOM_DRAW_ALGORITHM(2, "单项随机概率抽奖,抽到一个已经排掉的奖品则未中奖"),
-    ;
-
-    StrategyModeEnum(Integer id, String description) {
-        this.id = id;
-        this.description = description;
-    }
-
-    /**
-     * 策略id
-     */
-    private final Integer id;
-
-    /**
-     * 策略描述
-     */
-    private final String description;
-
-    public Integer getId() {
-        return id;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-
-}

+ 3 - 2
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/annotation/StrategyMode.java

@@ -1,6 +1,7 @@
 package com.seamew.lottery.domain.strategy.annotation;
 
-import com.seamew.lottery.common.enumeration.StrategyModeEnum;
+
+import com.seamew.lottery.common.Constants;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -18,5 +19,5 @@ import java.lang.annotation.Target;
 @Retention(RetentionPolicy.RUNTIME)
 public @interface StrategyMode {
 
-    StrategyModeEnum strategyMode();
+    Constants.StrategyMode strategyMode();
 }

+ 22 - 6
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/model/res/DrawResult.java

@@ -1,5 +1,7 @@
 package com.seamew.lottery.domain.strategy.model.res;
 
+import com.seamew.lottery.common.Constants;
+import com.seamew.lottery.domain.strategy.model.vo.DrawAwardInfo;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -15,15 +17,29 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor
 @AllArgsConstructor
 public class DrawResult {
-    // 用户ID
+    /**
+     * 用户ID
+     */
     private String uId;
 
-    // 策略ID
+    /**
+     * 策略ID
+     */
     private Long strategyId;
 
-    // 奖品ID
-    private String rewardId;
+    /**
+     * 中奖状态:0未中奖、1已中奖、2兜底奖 Constants.DrawState
+     */
+    private Integer drawState = Constants.DrawState.FAIL.getCode();
 
-    // 奖品名称
-    private String awardName;
+    /**
+     * 中奖奖品信息
+     */
+    private DrawAwardInfo drawAwardInfo;
+
+    public DrawResult(String uId, Long strategyId, Integer drawState) {
+        this.uId = uId;
+        this.strategyId = strategyId;
+        this.drawState = drawState;
+    }
 }

+ 12 - 0
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/repository/IStrategyRepository.java

@@ -3,6 +3,8 @@ package com.seamew.lottery.domain.strategy.repository;
 import com.seamew.lottery.domain.strategy.model.aggregates.StrategyRich;
 import com.seamew.lottery.infrastructure.po.Award;
 
+import java.util.List;
+
 /**
  * @Author: seamew
  * @Title: IStrategyRepository
@@ -14,4 +16,14 @@ public interface IStrategyRepository {
     StrategyRich queryStrategyRich(Long strategyId);
 
     Award queryAwardInfo(String awardId);
+
+    List<String> queryNoStockStrategyAwardList(Long strategyId);
+
+    /**
+     * 扣减库存
+     * @param strategyId 策略ID
+     * @param awardId    奖品ID
+     * @return           扣减结果
+     */
+    boolean deductStock(Long strategyId, String awardId);
 }

+ 14 - 0
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/repository/impl/StrategyRepository.java

@@ -42,4 +42,18 @@ public class StrategyRepository implements IStrategyRepository {
     public Award queryAwardInfo(String awardId) {
         return awardDao.queryAwardInfo(awardId);
     }
+
+    @Override
+    public List<String> queryNoStockStrategyAwardList(Long strategyId) {
+        return strategyDetailDao.queryNoStockStrategyAwardList(strategyId);
+    }
+
+    @Override
+    public boolean deductStock(Long strategyId, String awardId) {
+        StrategyDetail req = new StrategyDetail();
+        req.setStrategyId(strategyId);
+        req.setAwardId(awardId);
+        int count = strategyDetailDao.deductStock(req);
+        return count == 1;
+    }
 }

+ 10 - 0
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/BaseAlgorithm.java

@@ -3,6 +3,7 @@ package com.seamew.lottery.domain.strategy.service.algorithm;
 import com.seamew.lottery.domain.strategy.model.vo.AwardRateInfo;
 
 import java.math.BigDecimal;
+import java.security.SecureRandom;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -68,4 +69,13 @@ public abstract class BaseAlgorithm implements IDrawAlgorithm {
         return hashCode & (RATE_TUPLE_LENGTH - 1);
     }
 
+    /**
+     * 生成百位随机抽奖码
+     *
+     * @return 随机值
+     */
+    protected int generateSecureRandomIntCode(int bound){
+        return new SecureRandom().nextInt(bound) + 1;
+    }
+
 }

+ 5 - 7
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/impl/DefaultRateRandomDrawAlgorithm.java → lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/impl/EntiretyRateRandomDrawAlgorithm.java

@@ -1,6 +1,6 @@
 package com.seamew.lottery.domain.strategy.service.algorithm.impl;
 
-import com.seamew.lottery.common.enumeration.StrategyModeEnum;
+import com.seamew.lottery.common.Constants;
 import com.seamew.lottery.domain.strategy.annotation.StrategyMode;
 import com.seamew.lottery.domain.strategy.model.vo.AwardRateInfo;
 import com.seamew.lottery.domain.strategy.service.algorithm.BaseAlgorithm;
@@ -8,20 +8,19 @@ import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  * @Author: seamew
- * @Title: DefaultRateRandomDrawAlgorithm
+ * @Title: EntiretyRateRandomDrawAlgorithm
  * @CreateTime: 2023年02月13日 16:00:00
  * @Description: 必中奖策略抽奖,排掉已经中奖的概率,重新计算中奖范围
  * @Version: 1.0
  */
 @Component
-@StrategyMode(strategyMode = StrategyModeEnum.DEFAULT_RATE_RANDOM_DRAW_ALGORITHM)
-public class DefaultRateRandomDrawAlgorithm extends BaseAlgorithm {
+@StrategyMode(strategyMode = Constants.StrategyMode.ENTIRETY)
+public class EntiretyRateRandomDrawAlgorithm extends BaseAlgorithm {
     @Override
     public String randomDraw(Long strategyId, List<String> excludeAwardIds) {
 
@@ -44,8 +43,7 @@ public class DefaultRateRandomDrawAlgorithm extends BaseAlgorithm {
         if (differenceAwardRateList.size() == 1) return differenceAwardRateList.get(0).getAwardId();
 
         // 获取随机概率值
-        SecureRandom secureRandom = new SecureRandom();
-        int randomVal = secureRandom.nextInt(100) + 1;
+        int randomVal = generateSecureRandomIntCode(100);
 
         // 循环获取奖品
         String awardId = "";

+ 3 - 7
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/algorithm/impl/SingleRateRandomDrawAlgorithm.java

@@ -1,14 +1,11 @@
 package com.seamew.lottery.domain.strategy.service.algorithm.impl;
 
-import com.seamew.lottery.common.enumeration.StrategyModeEnum;
+import com.seamew.lottery.common.Constants;
 import com.seamew.lottery.domain.strategy.annotation.StrategyMode;
-import com.seamew.lottery.domain.strategy.model.vo.AwardRateInfo;
 import com.seamew.lottery.domain.strategy.service.algorithm.BaseAlgorithm;
 import org.springframework.stereotype.Component;
 
-import java.math.BigDecimal;
 import java.security.SecureRandom;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -19,7 +16,7 @@ import java.util.List;
  * @Version: 1.0
  */
 @Component
-@StrategyMode(strategyMode = StrategyModeEnum.SINGLE_RATE_RANDOM_DRAW_ALGORITHM)
+@StrategyMode(strategyMode = Constants.StrategyMode.SINGLE)
 public class SingleRateRandomDrawAlgorithm extends BaseAlgorithm {
     @Override
     public String randomDraw(Long strategyId, List<String> excludeAwardIds) {
@@ -28,8 +25,7 @@ public class SingleRateRandomDrawAlgorithm extends BaseAlgorithm {
         String[] rateTuple = rateTupleMap.get(strategyId);
 
         // 随机索引
-        SecureRandom secureRandom = new SecureRandom();
-        int randomVal = secureRandom.nextInt(100) + 1;
+        int randomVal = generateSecureRandomIntCode(100);
         int index = hashIdx(randomVal);
 
         // 返回结果

+ 116 - 0
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/AbstractDrawBase.java

@@ -0,0 +1,116 @@
+package com.seamew.lottery.domain.strategy.service.draw;
+
+import com.seamew.lottery.common.Constants;
+import com.seamew.lottery.domain.strategy.model.aggregates.StrategyRich;
+import com.seamew.lottery.domain.strategy.model.req.DrawReq;
+import com.seamew.lottery.domain.strategy.model.res.DrawResult;
+import com.seamew.lottery.domain.strategy.model.vo.AwardRateInfo;
+import com.seamew.lottery.domain.strategy.model.vo.DrawAwardInfo;
+import com.seamew.lottery.domain.strategy.service.algorithm.IDrawAlgorithm;
+import com.seamew.lottery.infrastructure.po.Award;
+import com.seamew.lottery.infrastructure.po.Strategy;
+import com.seamew.lottery.infrastructure.po.StrategyDetail;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @Author: seamew
+ * @Title: AbstractDrawBase
+ * @CreateTime: 2023年02月14日 18:09:00
+ * @Description: 定义抽象抽奖过程,模板模式
+ * @Version: 1.0
+ */
+@Slf4j
+public abstract class AbstractDrawBase extends DrawStrategySupport implements IDrawExec {
+    @Override
+    public DrawResult doDrawExec(DrawReq req) {
+        // 1. 获取抽奖策略
+        StrategyRich strategyRich = queryStrategyRich(req.getStrategyId());
+        Strategy strategy = strategyRich.getStrategy();
+
+        // 2. 校验抽奖策略是否已经初始化到内存
+        checkAndInitRateData(req.getStrategyId(), strategy.getStrategyMode(), strategyRich.getStrategyDetailList());
+
+        // 3. 获取不在抽奖范围内的列表,包括:奖品库存为空、风控策略、临时调整等
+        List<String> excludeAwardIds = queryExcludeAwardIds(req.getStrategyId());
+
+        // 4. 执行抽奖算法
+        String awardId = drawAlgorithm(req.getStrategyId(), drawAlgorithmGroup.get(strategy.getStrategyMode()), excludeAwardIds);
+
+        // 5. 包装中奖结果
+        return buildDrawResult(req.getUId(), req.getStrategyId(), awardId);
+    }
+
+    /**
+     * 获取不在抽奖范围内的列表,包括:奖品库存为空、风控策略、临时调整等,这类数据是含有业务逻辑的,所以需要由具体地实现方决定
+     *
+     * @param strategyId 策略ID
+     * @return 排除的奖品ID集合
+     */
+    protected abstract List<String> queryExcludeAwardIds(Long strategyId);
+
+    /**
+     * 执行抽奖算法
+     *
+     * @param strategyId      策略ID
+     * @param drawAlgorithm   抽奖算法模型
+     * @param excludeAwardIds 排除的抽奖ID集合
+     * @return 中奖奖品ID
+     */
+    protected abstract String drawAlgorithm(Long strategyId, IDrawAlgorithm drawAlgorithm, List<String> excludeAwardIds);
+
+    /**
+     * 校验抽奖策略是否已经初始化到内存
+     *
+     * @param strategyId         抽奖策略ID
+     * @param strategyMode       抽奖策略模式
+     * @param strategyDetailList 抽奖策略详情
+     */
+    private void checkAndInitRateData(Long strategyId, Integer strategyMode, List<StrategyDetail> strategyDetailList) {
+        // 非单项概率,不必存入缓存
+        // 暂时注释,有BUG
+        // if (!Constants.StrategyMode.SINGLE.getCode().equals(strategyMode)) {
+        //     return;
+        // }
+
+        IDrawAlgorithm drawAlgorithm = drawAlgorithmGroup.get(strategyMode);
+
+        // 已初始化过的数据,不必重复初始化
+        if (drawAlgorithm.isExistRateTuple(strategyId)) {
+            return;
+        }
+
+        // 解析并初始化中奖概率数据到散列表
+        List<AwardRateInfo> awardRateInfoList = new ArrayList<>(strategyDetailList.size());
+        for (StrategyDetail strategyDetail : strategyDetailList) {
+            awardRateInfoList.add(new AwardRateInfo(strategyDetail.getAwardId(), strategyDetail.getAwardRate()));
+        }
+
+        drawAlgorithm.initRateTuple(strategyId, awardRateInfoList);
+
+    }
+
+    /**
+     * 包装抽奖结果
+     *
+     * @param uId        用户ID
+     * @param strategyId 策略ID
+     * @param awardId    奖品ID,null 情况:并发抽奖情况下,库存临界值1 -> 0,会有用户中奖结果为 null
+     * @return 中奖结果
+     */
+    private DrawResult buildDrawResult(String uId, Long strategyId, String awardId) {
+        if (null == awardId) {
+            log.info("执行策略抽奖完成【未中奖】,用户:{} 策略ID:{}", uId, strategyId);
+            return new DrawResult(uId, strategyId, Constants.DrawState.FAIL.getCode());
+        }
+
+        Award award = queryAwardInfoByAwardId(awardId);
+        DrawAwardInfo drawAwardInfo = new DrawAwardInfo(award.getAwardId(), award.getAwardName());
+        log.info("执行策略抽奖完成【已中奖】,用户:{} 策略ID:{} 奖品ID:{} 奖品名称:{}", uId, strategyId, awardId, award.getAwardName());
+
+        return new DrawResult(uId, strategyId, Constants.DrawState.SUCCESS.getCode(), drawAwardInfo);
+    }
+
+}

+ 0 - 35
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawBase.java

@@ -1,35 +0,0 @@
-package com.seamew.lottery.domain.strategy.service.draw;
-
-import com.seamew.lottery.domain.strategy.model.vo.AwardRateInfo;
-import com.seamew.lottery.domain.strategy.service.algorithm.IDrawAlgorithm;
-import com.seamew.lottery.infrastructure.po.StrategyDetail;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @Author: seamew
- * @Title: DrawBase
- * @CreateTime: 2023年02月13日 21:00:00
- * @Description:
- * @Version: 1.0
- */
-public class DrawBase extends DrawConfig {
-    public void checkAndInitRateData(Long strategyId, Integer strategyMode, List<StrategyDetail> strategyDetailList) {
-        // 单体概率才需要初始化
-        if (1 != strategyMode) return;
-        IDrawAlgorithm drawAlgorithm = drawAlgorithmMap.get(strategyMode);
-
-        // 防止重复初始化
-        boolean existRateTuple = drawAlgorithm.isExistRateTuple(strategyId);
-        if (existRateTuple) return;
-
-        List<AwardRateInfo> awardRateInfoList = new ArrayList<>(strategyDetailList.size());
-        for (StrategyDetail strategyDetail : strategyDetailList) {
-            awardRateInfoList.add(new AwardRateInfo(strategyDetail.getAwardId(), strategyDetail.getAwardRate()));
-        }
-
-        drawAlgorithm.initRateTuple(strategyId, awardRateInfoList);
-
-    }
-}

+ 5 - 3
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawConfig.java

@@ -15,12 +15,12 @@ import java.util.concurrent.ConcurrentHashMap;
  * @Author: seamew
  * @Title: DrawConfig
  * @CreateTime: 2023年02月13日 21:00:00
- * @Description:
+ * @Description: 抽奖统一配置信息类
  * @Version: 1.0
  */
 public class DrawConfig {
 
-    protected static Map<Integer, IDrawAlgorithm> drawAlgorithmMap = new ConcurrentHashMap<>();
+    protected static Map<Integer, IDrawAlgorithm> drawAlgorithmGroup = new ConcurrentHashMap<>();
 
     @Resource
     private List<IDrawAlgorithm> algorithmList = new ArrayList<>();
@@ -30,7 +30,9 @@ public class DrawConfig {
     public void init() {
         algorithmList.forEach(r -> {
             StrategyMode strategyMode = AnnotationUtils.findAnnotation(r.getClass(), StrategyMode.class);
-            drawAlgorithmMap.put(strategyMode.strategyMode().getId(), r);
+            if (strategyMode != null) {
+                drawAlgorithmGroup.put(strategyMode.strategyMode().getCode(), r);
+            }
         });
     }
 }

+ 40 - 0
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/DrawStrategySupport.java

@@ -0,0 +1,40 @@
+package com.seamew.lottery.domain.strategy.service.draw;
+
+import com.seamew.lottery.domain.strategy.model.aggregates.StrategyRich;
+import com.seamew.lottery.domain.strategy.repository.IStrategyRepository;
+import com.seamew.lottery.infrastructure.po.Award;
+
+import javax.annotation.Resource;
+
+/**
+ * @Author: seamew
+ * @Title: DrawStrategySupport
+ * @CreateTime: 2023年02月14日 18:07:00
+ * @Description: 抽奖策略数据支撑,一些通用的数据服务
+ * @Version: 1.0
+ */
+public class DrawStrategySupport extends DrawConfig {
+    @Resource
+    protected IStrategyRepository strategyRepository;
+
+
+    /**
+     * 查询策略配置信息
+     *
+     * @param strategyId 策略ID
+     * @return 策略配置信息
+     */
+    protected StrategyRich queryStrategyRich(Long strategyId){
+        return strategyRepository.queryStrategyRich(strategyId);
+    }
+
+    /**
+     * 查询奖品详情信息
+     *
+     * @param awardId 奖品ID
+     * @return 中奖详情
+     */
+    protected Award queryAwardInfoByAwardId(String awardId){
+        return strategyRepository.queryAwardInfo(awardId);
+    }
+}

+ 28 - 37
lottery-domain/src/main/java/com/seamew/lottery/domain/strategy/service/draw/impl/DrawExecImpl.java

@@ -1,58 +1,49 @@
 package com.seamew.lottery.domain.strategy.service.draw.impl;
 
-import com.seamew.lottery.domain.strategy.model.aggregates.StrategyRich;
-import com.seamew.lottery.domain.strategy.model.req.DrawReq;
-import com.seamew.lottery.domain.strategy.model.res.DrawResult;
-import com.seamew.lottery.domain.strategy.repository.IStrategyRepository;
+import com.alibaba.fastjson2.JSON;
 import com.seamew.lottery.domain.strategy.service.algorithm.IDrawAlgorithm;
-import com.seamew.lottery.domain.strategy.service.draw.DrawBase;
-import com.seamew.lottery.domain.strategy.service.draw.IDrawExec;
-import com.seamew.lottery.infrastructure.po.Award;
-import com.seamew.lottery.infrastructure.po.Strategy;
-import com.seamew.lottery.infrastructure.po.StrategyDetail;
+import com.seamew.lottery.domain.strategy.service.draw.AbstractDrawBase;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
-import javax.annotation.Resource;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
  * @Author: seamew
  * @Title: DrawExecImpl
- * @CreateTime: 2023年02月13日 21:00:00
+ * @CreateTime: 2023年02月14日 18:43:00
  * @Description: 抽奖过程方法实现
  * @Version: 1.0
  */
 @Service("drawExec")
 @Slf4j
-public class DrawExecImpl extends DrawBase implements IDrawExec {
-
-    @Resource
-    private IStrategyRepository strategyRepository;
+public class DrawExecImpl extends AbstractDrawBase {
 
     @Override
-    public DrawResult doDrawExec(DrawReq req) {
-        log.info("执行策略抽奖开始,strategyId:{}", req.getStrategyId());
-
-        // 获取抽奖策略配置数据
-        StrategyRich strategyRich = strategyRepository.queryStrategyRich(req.getStrategyId());
-        Strategy strategy = strategyRich.getStrategy();
-        List<StrategyDetail> strategyDetailList = strategyRich.getStrategyDetailList();
-
-        // 校验和初始化数据
-        checkAndInitRateData(req.getStrategyId(), strategy.getStrategyMode(), strategyDetailList);
-
-        // 根据策略方式抽奖
-        IDrawAlgorithm drawAlgorithm = drawAlgorithmMap.get(strategy.getStrategyMode());
-        String awardId = drawAlgorithm.randomDraw(req.getStrategyId(), new ArrayList<>());
-
-        // 获取奖品信息
-        Award award = strategyRepository.queryAwardInfo(awardId);
-
-        log.info("执行策略抽奖完成,中奖用户:{} 奖品ID:{} 奖品名称:{}", req.getUId(), awardId, award.getAwardName());
+    protected List<String> queryExcludeAwardIds(Long strategyId) {
+        List<String> awardList = strategyRepository.queryNoStockStrategyAwardList(strategyId);
+        log.info("执行抽奖策略 strategyId:{},无库存排除奖品列表ID集合 awardList:{}", strategyId, JSON.toJSONString(awardList));
+        return awardList;
+    }
 
-        // 封装结果
-        return new DrawResult(req.getUId(), req.getStrategyId(), awardId, award.getAwardName());
+    @Override
+    protected String drawAlgorithm(Long strategyId, IDrawAlgorithm drawAlgorithm, List<String> excludeAwardIds) {
+        // 执行抽奖
+        String awardId = drawAlgorithm.randomDraw(strategyId, excludeAwardIds);
+
+        // 判断抽奖结果
+        if (null == awardId) {
+            return null;
+        }
+
+        /*
+         * 扣减库存,暂时采用数据库行级锁的方式进行扣减库存,后续优化为 Redis 分布式锁扣减 decr/incr
+         * 注意:通常数据库直接锁行记录的方式并不能支撑较大体量的并发,但此种方式需要了解,
+         * 因为在分库分表下的正常数据流量下的个人数据记录中,是可以使用行级锁的,因为他只影响到自己的记录,不会影响到其他人
+         */
+        boolean isSuccess = strategyRepository.deductStock(strategyId, awardId);
+
+        // 返回结果,库存扣减成功返回奖品ID,否则返回NULL 「在实际的业务场景中,如果中奖奖品库存为空,则会发送兜底奖品,比如各类券」
+        return isSuccess ? awardId : null;
     }
 }

+ 5 - 0
lottery-interfaces/pom.xml

@@ -80,6 +80,11 @@
             <artifactId>lottery-rpc</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>com.seamew</groupId>
+            <artifactId>lottery-domain</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 
 

+ 2 - 1
lottery-interfaces/src/test/java/com/seamew/lottery/test/ApiTest.java

@@ -37,9 +37,10 @@ public class ApiTest {
         //
         // activityDao.insert(activity);
 
-        Activity activity = activityDao.queryActivityById(100002l);
+        Activity activity = activityDao.queryActivityById(100002L);
         System.out.println(activity);
     }
+
 }
 
 

+ 38 - 0
lottery-interfaces/src/test/java/com/seamew/lottery/test/DrawAlgorithmTest.java

@@ -0,0 +1,38 @@
+package com.seamew.lottery.test;
+
+import com.seamew.lottery.domain.strategy.model.req.DrawReq;
+import com.seamew.lottery.domain.strategy.service.draw.IDrawExec;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.annotation.Resource;
+
+/**
+ * @Author: seamew
+ * @Title: DrawAlgorithmTest
+ * @CreateTime: 2023年02月14日 18:52:00
+ * @Description:
+ * @Version: 1.0
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class DrawAlgorithmTest {
+    @Resource
+    private IDrawExec drawExec;
+
+    @Test
+    public void test_drawExec() {
+        drawExec.doDrawExec(new DrawReq("小傅哥", 10001L));
+        // drawExec.doDrawExec(new DrawReq("小佳佳", 10001L));
+        // drawExec.doDrawExec(new DrawReq("小蜗牛", 10001L));
+        // drawExec.doDrawExec(new DrawReq("八杯水", 10001L));
+        // for (int i = 0; i < 100; i++) {
+        //     new Thread(() -> {
+        //         drawExec.doDrawExec(new DrawReq("小傅哥", 10001L));
+        //     }).start();
+        // }
+    }
+
+}