Explorar o código

Merge remote-tracking branch 'origin/master'

jiandong.liu hai 1 semana
pai
achega
2fa85f8692

+ 9 - 7
ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/ControllerAspect.java

@@ -6,14 +6,13 @@ import com.tzld.piaoquan.ad.engine.commons.util.JSONUtils;
 import com.tzld.piaoquan.ad.engine.commons.util.TraceUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.ProceedingJoinPoint;
-import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
-import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -34,6 +33,8 @@ public class ControllerAspect {
 
     }
 
+    @Value("${aspect_log_switch:false}")
+    private Boolean aspectLogSwitch;
 
     @Around("logPointcut() && !excludePointcut()")
     public Object around(ProceedingJoinPoint pjp) throws Throwable {
@@ -44,12 +45,13 @@ public class ControllerAspect {
 
         String param = JSONUtils.toJson(pjp.getArgs(), Sets.newHashSet("statisticsLog"));
 
-        log.info("request className=[{}], method=[{}], param=[{}]", className, signature.getName(), param);
+        if (Boolean.TRUE.equals(aspectLogSwitch)) {
+            log.info("request className=[{}], method=[{}], param=[{}]", className, signature.getName(), param);
+        }
         Object result = pjp.proceed();
-        if (result != null && result instanceof String) {
-            log.info("request method=[{}]  param=[{}] result=[{}] cost=[{}]", signature.getName(), param, result, stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
-        } else {
-            log.info("request method=[{}]  param=[{}] result=[{}] cost=[{}]", signature.getName(), param, JSONUtils.toJson(result), stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
+        if (Boolean.TRUE.equals(aspectLogSwitch)) {
+            String resultStr = (result instanceof String) ? (String) result : JSONUtils.toJson(result);
+            log.info("request method=[{}] param=[{}] result=[{}] cost=[{}]", signature.getName(), param, resultStr, stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
         }
         TraceUtils.removeMDC();
         return result;

+ 5 - 0
ad-engine-server/src/main/resources/application.yml

@@ -129,6 +129,11 @@ grpc:
   client:
     recommend-feature:
       negotiationType: PLAINTEXT
+      # KeepAlive 配置(防止空闲连接被网关关闭)
+      enable-keep-alive: true
+      keep-alive-time: 30s
+      keep-alive-timeout: 5s
+      keep-alive-without-calls: true
     recommend-server:
       negotiationType: PLAINTEXT
     GLOBAL:

+ 4 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/feature/FeatureService.java

@@ -115,6 +115,10 @@ public class FeatureService {
     public Feature invokeFeatureService(List<FeatureKeyProto> protos) {
 
         Map<String, String> featureMap = remoteService.getFeature(protos);
+        if (featureMap == null) {
+            log.warn("invokeFeatureService: featureMap is null, return empty feature");
+            return new Feature();
+        }
         featureMap = this.featureStrCover(featureMap);
 
         Feature feature = new Feature();

+ 10 - 16
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/log/impl/LogHubServiceImpl.java

@@ -78,21 +78,6 @@ public class LogHubServiceImpl implements LogHubService {
                     json.put("score", rankItem.getScore());
                     rankItem.getScoreMap().put("score", rankItem.getScore());
                     json.put("scoremap", rankItem.getScoreMap());
-                    JSONObject featureJson = new JSONObject();
-                    for (Map.Entry<String, String> entry : rankItem.getFeatureMap().entrySet()) {
-                        if (FEATURE_FIELD_SET.contains(entry.getKey())) {
-                            featureJson.put(entry.getKey(), entry.getValue());
-                        }
-                    }
-                    for (Map.Entry<String, Map<String, String>> entry : rankItem.getMetaFeatureMap().entrySet()) {
-                        if (FEATURE_FIELD_SET.contains(entry.getKey())) {
-                            featureJson.put(entry.getKey(), entry.getValue());
-                        }
-                    }
-
-                    if (MapUtils.isNotEmpty(featureJson)) {
-                        json.put("allfeature", featureJson);
-                    }
                     scoreResult.add(json);
                 }
 
@@ -111,9 +96,18 @@ public class LogHubServiceImpl implements LogHubService {
 
                 top1.getScoreMap().put("score", top1.getScore());
                 logMap.put("scoremap", JSON.toJSONString(top1.getScoreMap()));
-                logMap.put("allfeature", JSON.toJSONString(top1.getFeatureMap()));
+                // logMap.put("allfeature", JSON.toJSONString(top1.getFeatureMap()));
                 logMap.put("metafeature", JSON.toJSONString(top1.getMetaFeatureMap()));
 
+                long featureTableSize = 0;
+                if (MapUtils.isNotEmpty(top1.getMetaFeatureMap())) {
+                    featureTableSize = top1.getMetaFeatureMap().entrySet()
+                            .stream()
+                            .filter(e -> MapUtils.isNotEmpty(e.getValue()))
+                            .count();
+                }
+                logMap.put("featuretablesize", featureTableSize);
+
                 aliyunLogManager.sendLog(project, scoreStatisticsLogStore, "", logMap);
             }
         });

+ 50 - 44
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/impl/PredictModelServiceImpl.java

@@ -80,6 +80,8 @@ public class PredictModelServiceImpl implements PredictModelService {
     @Autowired
     private PredictStrategyByRor predictStrategyByRor;
     @Autowired
+    private PredictStrategyByRorMorning predictStrategyByRorMorning;
+    @Autowired
     private PredictStrategyByFissionRate predictStrategyByFissionRate;
     @Autowired
     private PredictStrategyByRorCopy predictStrategyByRorCopy;
@@ -169,6 +171,7 @@ public class PredictModelServiceImpl implements PredictModelService {
                 expCodes.add(expCode);
             }
             boolean execute817 = false;
+            boolean in817time = false;
             // 不出广告时间判定
             int hourOfDay = DateUtils.getCurrentHour();
             if ( 0 <= hourOfDay && hourOfDay < 8 && !isAdvanceShowAd() ) {
@@ -183,8 +186,9 @@ public class PredictModelServiceImpl implements PredictModelService {
                     log.error("experiment817WithAdHour配置异常", e);
                 }
                 // 在 817实验时间范围 && 817实验开启
-                if (isIn817Time && expCodes.contains("817")) {
-                    execute817 = true;
+                if (isIn817Time) {
+                    in817time = true;
+                    execute817 = expCodes.contains("817");
                 } else {
                     result.put("ad_predict", 1);
                     result.put("no_ad_strategy", "no_ad_time_with_fixed_time");
@@ -201,6 +205,16 @@ public class PredictModelServiceImpl implements PredictModelService {
 
 
             PredictContext predictContext = ConvertUtil.predictParam2Context(requestParam);
+
+            if(in817time){
+                // 早间ror熔断
+                Map<String, Object> userRorPredictMorning = predictStrategyByRorMorning.predict(predictContext);
+                if (MapUtils.isNotEmpty(userRorPredictMorning)) {
+                    return userRorPredictMorning;
+                }
+            }
+
+
             // 817熔断
             if (execute817) {
                 return predictStrategyBy817.predict(predictContext);
@@ -208,9 +222,9 @@ public class PredictModelServiceImpl implements PredictModelService {
 
             // 819只做参数填充
             Map<String, Object> predictExtInfo = null;
-            if (expCodes.contains("819")) {
-                predictExtInfo = predictStrategyBy819.predict(predictContext);
-            }
+            // if (expCodes.contains("819")) {
+            //     predictExtInfo = predictStrategyBy819.predict(predictContext);
+            // }
 
             Map<String, Object> userLayerPredict = userLayerRootSessionIdPredict.predict(predictContext);
             if (MapUtils.isNotEmpty(userLayerPredict)) {
@@ -222,16 +236,16 @@ public class PredictModelServiceImpl implements PredictModelService {
             }
 
 
-            if (expCodes.contains("820")) {
-                Map<String, Object> userLayerPredict820 = predictStrategyBy820.predict(predictContext);
-                if (MapUtils.isNotEmpty(userLayerPredict820)) {
-                    // 填充 819 参数
-                    if (MapUtils.isNotEmpty(predictExtInfo)) {
-                        userLayerPredict820.putAll(predictExtInfo);
-                    }
-                    return userLayerPredict820;
-                }
-            }
+            // if (expCodes.contains("820")) {
+            //     Map<String, Object> userLayerPredict820 = predictStrategyBy820.predict(predictContext);
+            //     if (MapUtils.isNotEmpty(userLayerPredict820)) {
+            //         // 填充 819 参数
+            //         if (MapUtils.isNotEmpty(predictExtInfo)) {
+            //             userLayerPredict820.putAll(predictExtInfo);
+            //         }
+            //         return userLayerPredict820;
+            //     }
+            // }
 
 
 
@@ -246,12 +260,6 @@ public class PredictModelServiceImpl implements PredictModelService {
                 }
             }
 
-            // fission_rate策略数据填充
-            Map<String, Object> userFissionRatePredict = predictStrategyByFissionRate.predict(predictContext);
-
-            // fission_rate_copy策略数据填充
-            Map<String, Object> userFissionRatePredictCopy = predictStrategyByFissionRateCopy.predict(predictContext);
-
             // ror行为策略
             Map<String, Object> userRorPredict = predictStrategyByRor.predict(predictContext);
             if (MapUtils.isNotEmpty(userRorPredict)) {
@@ -259,14 +267,6 @@ public class PredictModelServiceImpl implements PredictModelService {
                 if (MapUtils.isNotEmpty(predictExtInfo)) {
                     userRorPredict.putAll(predictExtInfo);
                 }
-                // fission_rate策略数据填充
-                if (MapUtils.isNotEmpty(userFissionRatePredict)) {
-                    userRorPredict.putAll(userFissionRatePredict);
-                }
-                // fission_rate_copy策略数据填充
-                if (MapUtils.isNotEmpty(userFissionRatePredictCopy)) {
-                    userRorPredict.putAll(userFissionRatePredictCopy);
-                }
                 return userRorPredict;
             }
 
@@ -277,17 +277,31 @@ public class PredictModelServiceImpl implements PredictModelService {
                 if (MapUtils.isNotEmpty(predictExtInfo)) {
                     userRorPredictCopy.putAll(predictExtInfo);
                 }
-                // fission_rate策略数据填充
-                if (MapUtils.isNotEmpty(userFissionRatePredict)) {
-                    userRorPredictCopy.putAll(userFissionRatePredict);
+                return userRorPredictCopy;
+            }
+
+
+            // fission_rate行为策略
+            Map<String, Object> userFissionRatePredict = predictStrategyByFissionRate.predict(predictContext);
+            if (MapUtils.isNotEmpty(userFissionRatePredict)) {
+                // 填充 819 参数
+                if (MapUtils.isNotEmpty(predictExtInfo)) {
+                    userFissionRatePredict.putAll(predictExtInfo);
                 }
-                // fission_rate_copy策略数据填充
-                if (MapUtils.isNotEmpty(userFissionRatePredictCopy)) {
-                    userRorPredict.putAll(userFissionRatePredictCopy);
+                return userFissionRatePredict;
+            }
+
+            // fission_rate_copy行为策略
+            Map<String, Object> userFissionRatePredictCopy = predictStrategyByFissionRateCopy.predict(predictContext);
+            if (MapUtils.isNotEmpty(userFissionRatePredictCopy)) {
+                // 填充 819 参数
+                if (MapUtils.isNotEmpty(predictExtInfo)) {
+                    userFissionRatePredictCopy.putAll(predictExtInfo);
                 }
-                return userRorPredictCopy;
+                return userFissionRatePredictCopy;
             }
 
+
             Map<String, Object> predictResult;
             if (expCodes.contains("599")){
                 predictResult = predictStrategyBy599.predict(predictContext);
@@ -300,14 +314,6 @@ public class PredictModelServiceImpl implements PredictModelService {
             if (MapUtils.isNotEmpty(predictResult) && MapUtils.isNotEmpty(predictExtInfo)) {
                 predictResult.putAll(predictExtInfo);
             }
-            // fission_rate策略数据填充
-            if (MapUtils.isNotEmpty(predictResult) && MapUtils.isNotEmpty(userFissionRatePredict)) {
-                predictResult.putAll(userFissionRatePredict);
-            }
-            // fission_rate_copy策略数据填充
-            if (MapUtils.isNotEmpty(userFissionRatePredictCopy)) {
-                predictResult.putAll(userFissionRatePredictCopy);
-            }
             return predictResult;
             
         } catch (Exception e) {

+ 2 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByFissionRate.java

@@ -132,6 +132,8 @@ public class PredictStrategyByFissionRate extends BasicPredict {
             }
 
             // 记录决策相关的特征和参数,用于日志分析和效果追踪
+            rtnMap.putAll(rtnAdPredict(ctx));
+            rtnMap.put("model", this.name());
             rtnMap.put("launchs", launchs);
             rtnMap.put("ror", ror);
             rtnMap.put("ad_level", adLevel);

+ 2 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByFissionRateCopy.java

@@ -132,6 +132,8 @@ public class PredictStrategyByFissionRateCopy extends BasicPredict {
             }
 
             // 记录决策相关的特征和参数,用于日志分析和效果追踪
+            rtnMap.putAll(rtnAdPredict(ctx));
+            rtnMap.put("model", this.name());
             rtnMap.put("launchs", launchs);
             rtnMap.put("ror", ror);
             rtnMap.put("ad_level", adLevel);

+ 19 - 20
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRorCopy.java

@@ -94,20 +94,20 @@ public class PredictStrategyByRorCopy extends BasicPredict {
             String appType = ctx.getAppType();
 
             // 获取默认概率阈值(基于 rootSessionId 尾号和 appType 匹配配置)
-            Boolean matchResult = getDefaultProbability(rootSessionId, appType);
+            Double defaultProbability = getDefaultProbability(rootSessionId, appType);
 
             // 用户不在实验分桶内,跳过该策略
-            if (!matchResult) {
+            if (defaultProbability == null) {
                 return null;
             }
 
             Map<String, Object> rtnMap = new HashMap<>();
 
             // 用户行为特征变量(来自离线特征表 alg_mid_history_behavior_1month)
-            String launchs = null;   // 启动次数分桶(如 "0-5", "5-10" 等)
-            String ror = null;       // 留存率分桶
-            String adLevel = null;   // 广告等级(用户对广告的敏感度分层)
-            String return30day = null;   // 用户回流率分桶
+            String launchs = "-999";   // 启动次数分桶(如 "0-5", "5-10" 等)
+            String ror = "-999";       // 留存率分桶
+            String adLevel = "无转化";   // 广告等级(用户对广告的敏感度分层)
+            String return30day = "r_0_8";   // 用户回流率分桶
 
             // 根据 mid 获取用户近一个月的历史行为特征
             Feature feature = featureService.getMidBehaviorFeature(TABLE_NAME, ctx.getMid());
@@ -117,15 +117,15 @@ public class PredictStrategyByRorCopy extends BasicPredict {
             // 安全地提取特征值(多层 null 检查)
             if (feature != null && feature.getUserFeature() != null && feature.getUserFeature().get(TABLE_NAME) != null) {
                 Map<String, String> algMidHistoryBehavior1month = feature.getUserFeature().get(TABLE_NAME);
-                launchs = algMidHistoryBehavior1month.get("launchs");
-                ror = algMidHistoryBehavior1month.get("ror");
-                adLevel = algMidHistoryBehavior1month.get("ad_level");
-                return30day = algMidHistoryBehavior1month.get("return_30day");
+                launchs = StringUtils.isBlank(algMidHistoryBehavior1month.get("launchs")) ? launchs : algMidHistoryBehavior1month.get("launchs");
+                ror = StringUtils.isBlank(algMidHistoryBehavior1month.get("ror")) ? ror : algMidHistoryBehavior1month.get("ror");
+                adLevel = StringUtils.isBlank(algMidHistoryBehavior1month.get("ad_level")) ? adLevel : algMidHistoryBehavior1month.get("ad_level");
+                return30day = StringUtils.isBlank(algMidHistoryBehavior1month.get("return_30day")) ? return30day : algMidHistoryBehavior1month.get("return_30day");
             }
 
             // 计算最终的广告展示概率阈值
             // 优先使用 Redis 中基于用户特征的精细化阈值,否则使用默认阈值
-            Double showAdProbability = getShowAdProbability(launchs, ror, adLevel,return30day);
+            Double showAdProbability = getShowAdProbability(launchs, ror, adLevel,return30day,defaultProbability);
 
             if (showAdProbability == null) {
                 return null;
@@ -172,10 +172,10 @@ public class PredictStrategyByRorCopy extends BasicPredict {
      *
      * @return 广告展示概率阈值 [0, 1]
      */
-    private Double getShowAdProbability(String launchs, String ror, String ad_level,String return30day) {
+    private Double getShowAdProbability(String launchs, String ror, String ad_level,String return30day, Double defaultProbability) {
         // 任一特征为空,使用默认概率
         if (StringUtils.isAnyBlank(launchs, ror, ad_level,return30day)) {
-            return null;
+            return defaultProbability;
         }
         try {
             // 构建 Redis key:格式为 "ad_level:launchs:ror",例如 "有转化:10:000"
@@ -186,11 +186,11 @@ public class PredictStrategyByRorCopy extends BasicPredict {
             String probability = adRedisHelper.get(key);
 
             // 解析概率值,如果为 null 则使用默认值
-            return StringUtils.isBlank(probability) ? null : Double.parseDouble(probability);
+            return StringUtils.isBlank(probability) ? defaultProbability : Double.parseDouble(probability);
         } catch (Exception e) {
             // 解析失败(如非数字字符串)或 Redis 异常,记录错误并使用默认值
             log.error("getShowAdProbability error, launchs: {}, ror: {}, ad_level: {}, e = ", launchs, ror, ad_level, e);
-            return null;
+            return defaultProbability;
         }
     }
 
@@ -203,10 +203,10 @@ public class PredictStrategyByRorCopy extends BasicPredict {
      * @param appType       应用类型
      * @return 默认概率,如果不匹配任何配置则返回 null
      */
-    private Boolean getDefaultProbability(String rootSessionId, String appType) {
+    private Double getDefaultProbability(String rootSessionId, String appType) {
         // 前置校验
         if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId) || appType == null) {
-            return false;
+            return null;
         }
 
         // 提取 rootSessionId 的最后一个字符作为尾号,用于流量分桶
@@ -216,15 +216,14 @@ public class PredictStrategyByRorCopy extends BasicPredict {
         for (RootSessionIdTailConfigItem item : configItems) {
             if (item.getAppType() != null && item.getTail() != null) {
                 if (item.getAppType().contains(appType) && item.getTail().contains(tail)) {
-//                    return item.getConfig().get("default_probability");
-                    return true;
+                    return item.getConfig().get("default_probability");
                 }
             }
 
         }
 
         // 未匹配到任何配置
-        return false;
+        return null;
     }
 
 }

+ 230 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRorMorning.java

@@ -0,0 +1,230 @@
+package com.tzld.piaoquan.ad.engine.service.predict.v2;
+
+import com.alibaba.fastjson.JSON;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.piaoquan.ad.engine.commons.enums.RedisPrefixEnum;
+import com.tzld.piaoquan.ad.engine.commons.redis.AdRedisHelper;
+import com.tzld.piaoquan.ad.engine.service.feature.Feature;
+import com.tzld.piaoquan.ad.engine.service.feature.FeatureService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 基于 ROR的广告预测策略
+ * <p>
+ * 核心逻辑:
+ * 1. 根据用户的历史行为特征(启动次数launchs、留存率ror、人群分层ad_level)计算展示广告的概率阈值
+ * 2. 通过 mid 的 hash 值生成伪随机分数
+ * 3. 如果分数 <= 阈值,则展示广告;否则不展示
+ * <p>
+ * 用于控制不同用户群体的广告曝光频率,实现精细化运营
+ */
+@Slf4j
+@Service
+public class PredictStrategyByRorMorning extends BasicPredict {
+
+    /** 特征服务,用于获取用户行为特征 */
+    @Autowired
+    private FeatureService featureService;
+
+    /** Redis 客户端,用于获取基于用户特征的概率配置 */
+    @Autowired
+    private AdRedisHelper adRedisHelper;
+
+    /**
+     * Apollo 动态配置:根据 rootSessionId 尾号和 appType 进行流量分桶
+     * <p>
+     * 配置格式示例:
+     * <pre>
+     * [
+     *   {
+     *     "appType": ["0", "3"],
+     *     "tail": ["0", "1", "2"],
+     *     "config": {"default_probability": 0.5}
+     *   }
+     * ]
+     * </pre>
+     */
+    @ApolloJsonValue("${experiment.ror.root.session.morning.id.tail.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    @Value("${experiment.ror.show.log.switch:0}")
+    private String experimentRorShowLogSwitch;
+
+    private static final String TABLE_NAME = "alg_mid_history_behavior_1month";
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+    public String name() {
+        return "launch_layer_ror_morning";
+    }
+
+    /**
+     * 核心预测方法:决定是否向用户展示广告
+     *
+     * @param ctx 预测上下文,包含 mid、appType、rootSessionId 等信息
+     * @return 预测结果 Map,包含:
+     *         - ad_predict: 1=不展示广告,2=展示广告
+     *         - score: 用户的伪随机分数
+     *         - threshold: 广告展示概率阈值
+     *         - launchs/ror/ad_level: 用户行为特征
+     *         - 返回 null 表示跳过该策略
+     */
+    @Override
+    public Map<String, Object> predict(PredictContext ctx) {
+
+        try {
+            String rootSessionId = ctx.getRootSessionId();
+
+            // 前置校验:配置为空或 rootSessionId 为空时,返回 null(跳过该策略)
+            if (CollectionUtils.isEmpty(configItems) || StringUtils.isBlank(rootSessionId)) {
+                return null;
+            }
+
+            String appType = ctx.getAppType();
+
+            // 获取默认概率阈值(基于 rootSessionId 尾号和 appType 匹配配置)
+            Boolean matchResult = getDefaultProbability(rootSessionId, appType);
+
+            // 用户不在实验分桶内,跳过该策略
+            if (!matchResult) {
+                return null;
+            }
+
+            Map<String, Object> rtnMap = new HashMap<>();
+
+            // 用户行为特征变量(来自离线特征表 alg_mid_history_behavior_1month)
+            String launchs = null;   // 启动次数分桶(如 "0-5", "5-10" 等)
+            String ror = null;       // 留存率分桶
+            String adLevel = null;   // 广告等级(用户对广告的敏感度分层)
+            String return30day = null;   // 用户回流率分桶
+
+            // 根据 mid 获取用户近一个月的历史行为特征
+            Feature feature = featureService.getMidBehaviorFeature(TABLE_NAME, ctx.getMid());
+            if ("1".equals(experimentRorShowLogSwitch)) {
+                log.info("[PredictStrategyByRorMorning] mid:{}, feature:{}, featureStr: {}", ctx.getMid(),feature,JSON.toJSONString(feature));
+            }
+            // 安全地提取特征值(多层 null 检查)
+            if (feature != null && feature.getUserFeature() != null && feature.getUserFeature().get(TABLE_NAME) != null) {
+                Map<String, String> algMidHistoryBehavior1month = feature.getUserFeature().get(TABLE_NAME);
+                launchs = algMidHistoryBehavior1month.get("launchs");
+                ror = algMidHistoryBehavior1month.get("ror");
+                adLevel = algMidHistoryBehavior1month.get("ad_level");
+                return30day = algMidHistoryBehavior1month.get("return_30day");
+            }
+
+            // 计算最终的广告展示概率阈值
+            // 优先使用 Redis 中基于用户特征的精细化阈值,否则使用默认阈值
+            Double showAdProbability = getShowAdProbability(launchs, ror, adLevel,return30day);
+
+            if (showAdProbability == null) {
+                return null;
+            }
+
+            // 基于 mid 的 hash 值生成 [0, 1) 范围内的伪随机分数
+            // 同一个 mid 在同一小时内(RandW 每小时更新)会得到相同的分数
+            double score = this.calcScoreByMid(ctx.getMid());
+
+            // 核心决策逻辑:分数 <= 阈值 → 展示广告
+            if (score < showAdProbability) {
+                // 展示广告,ad_predict = 2
+                rtnMap.putAll(rtnAdPredict(ctx));
+                rtnMap.put("model", this.name());
+            } else {
+                // 不展示广告,ad_predict = 1
+                rtnMap.putAll(rtnNoAdPredict(ctx));
+                rtnMap.put("no_ad_strategy", this.name());
+            }
+
+            // 记录决策相关的特征和参数,用于日志分析和效果追踪
+            rtnMap.put("score", score);
+            rtnMap.put("threshold", showAdProbability);
+            rtnMap.put("launchs", launchs);
+            rtnMap.put("return_30day", return30day);
+            rtnMap.put("ror", ror);
+            rtnMap.put("ad_level", adLevel);
+            return rtnMap;
+        } catch (Exception e) {
+            log.error("[PredictStrategyByRorMorning] predict error, ctx: {}", ctx, e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取广告展示概率阈值
+     * <p>
+     * 策略:根据用户的 (ad_level, launchs, ror) 组合从 Redis 查询对应的概率值
+     * 如果查询失败或无数据,则使用默认概率
+     *
+     * @param launchs            启动次数分桶
+     * @param ror                留存率分桶
+     * @param ad_level           人群分层
+     *
+     * @return 广告展示概率阈值 [0, 1]
+     */
+    private Double getShowAdProbability(String launchs, String ror, String ad_level,String return30day) {
+        // 任一特征为空,使用默认概率
+        if (StringUtils.isAnyBlank(launchs, ror, ad_level,return30day)) {
+            return null;
+        }
+        try {
+            // 构建 Redis key:格式为 "ad_level:launchs:ror",例如 "有转化:10:000"
+            String keyId = ad_level + ":" + launchs + ":" + ror + ":" + return30day;
+            String key = String.format(RedisPrefixEnum.AD_USER_ROR_BEHAVIOR.getPrefix(), keyId);
+
+            // 从 Redis 获取概率值
+            String probability = adRedisHelper.get(key);
+
+            // 解析概率值,如果为 null 则使用默认值
+            return StringUtils.isBlank(probability) ? null : Double.parseDouble(probability);
+        } catch (Exception e) {
+            // 解析失败(如非数字字符串)或 Redis 异常,记录错误并使用默认值
+            log.error("[PredictStrategyByRorMorning] getShowAdProbability error, launchs: {}, ror: {}, ad_level: {}, e = ", launchs, ror, ad_level, e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取默认概率阈值
+     * <p>
+     * 根据 rootSessionId 的最后一位字符(尾号)和 appType 匹配配置,用于流量分桶实验
+     *
+     * @param rootSessionId 根会话 ID
+     * @param appType       应用类型
+     * @return 默认概率,如果不匹配任何配置则返回 null
+     */
+    private Boolean getDefaultProbability(String rootSessionId, String appType) {
+        // 前置校验
+        if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId) || appType == null) {
+            return false;
+        }
+
+        // 提取 rootSessionId 的最后一个字符作为尾号,用于流量分桶
+        String tail = rootSessionId.substring(rootSessionId.length() - 1);
+
+        // 遍历配置项,查找同时匹配 appType 和尾号的配置
+        for (RootSessionIdTailConfigItem item : configItems) {
+            if (item.getAppType() != null && item.getTail() != null) {
+                if (item.getAppType().contains(appType) && item.getTail().contains(tail)) {
+//                    return item.getConfig().get("default_probability");
+                    return true;
+                }
+            }
+
+        }
+
+        // 未匹配到任何配置
+        return false;
+    }
+
+}

+ 6 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/impl/RankServiceImpl.java

@@ -89,6 +89,12 @@ public class RankServiceImpl implements RankService {
         RankStrategy rankStrategy = getRankStrategy(scoreParam);
         List<AdRankItem> adRankItems = rankStrategy.adItemRank(request, scoreParam);
         logHubService.scoreLogUpload(scoreParam, request.getAdIdList(), adRankItems, request, scoreParam.getExpCode());
+        
+        // 防御性检查:避免空列表导致 IndexOutOfBoundsException
+        if (CollectionUtils.isEmpty(adRankItems)) {
+            log.warn("adItemRank: adRankItems is empty, request={}", request);
+            return null;
+        }
         return adRankItems.get(0);
     }