12
0

80 Коммиты 2f069cbaee ... a78fca9264

Автор SHA1 Сообщение Дата
  jiandong.liu a78fca9264 Merge branch 'ljd/feature-morning-abtest' into test 4 дней назад
  jiandong.liu 1528849626 feat: 5-6点出广告 5 дней назад
  fanjinyang c749e0b45f Merge branch '20260302_feature_fjy_engine_l3_l4' of algorithm/ad-engine into master 5 дней назад
  yaodaoseng a734b1a6db L3、L4被召回 5 дней назад
  fanjinyang 8de4932cda Merge branch '20260228_feature_fjy_ecpm_60' of algorithm/ad-engine into master 1 неделя назад
  yaodaoseng 87cf070f2b ecpm调整到 60 1 неделя назад
  fanjinyang aae73ff2a3 Merge branch '20260225_feature_fjy_833_ecpm' of algorithm/ad-engine into master 1 неделя назад
  yaodaoseng cde58cecaa update 1 неделя назад
  jiachanghui 9e01c290d4 Merge branch 'ctcvr_rank_model_833_v3' of algorithm/ad-engine into master 1 месяц назад
  jch 2548376863 添加曝光权重 1 месяц назад
  jiachanghui 1d8f56b9ac Merge branch 'ctcvr_rank_model_833_v3' of algorithm/ad-engine into master 1 месяц назад
  jch 2b137de56a 本地测试 1 месяц назад
  jch e1a5258270 ctcvr_model_v3_calibration 1 месяц назад
  fanjinyang c00d9b6e6b Merge branch '20260129_feature_fjy_random_sort' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng bede81c1e3 834随机实验 1 месяц назад
  fanjinyang bff23d55b2 Merge branch '20260129_feature_fjy_random_sort' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng a9f98f8109 834随机实验 1 месяц назад
  yaodaoseng 0c4800b703 834随机实验 1 месяц назад
  yaodaoseng ed0475f856 834随机实验 1 месяц назад
  yaodaoseng 0d1d0e55b5 688加埋点 1 месяц назад
  yaodaoseng 891b7d70e1 下掉摸底 1 месяц назад
  fanjinyang e9ea866900 Merge branch '20260128_feature_fjy_recommend-feature' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng 89265416fe update recommend-feature version 1 месяц назад
  fanjinyang e078dffe87 Merge branch '20260128_feature_fjy_recommend-feature' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng 3c195e0a04 改回 dnnV2 1 месяц назад
  yaodaoseng 3ba19b1435 update recommend-feature pom version 1 месяц назад
  jiandong.liu c04c391ef2 增加排查日志 1 месяц назад
  fanjinyang 997eca0df7 Merge branch '20260126_feature_fjy_ctcvr_jiaozhun' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng c1c3caec34 833实验 1 месяц назад
  fanjinyang f9ec105c0a Merge branch '20260126_feature_fjy_ctcvr_jiaozhun' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng a2dfd16b9d 833实验 1 месяц назад
  yaodaoseng 87c142b7fa 833实验 1 месяц назад
  jiandong.liu 4648340d46 Merge branch 'ljd/feature-20260113' 1 месяц назад
  jiandong.liu 58d82b6086 增加排查日志 1 месяц назад
  jiandong.liu 7ba58b7fd2 Merge remote-tracking branch 'origin/master' 1 месяц назад
  jiandong.liu 5af09dd5d2 fix keep-alive-time 1 месяц назад
  fanjinyang d08da7bd32 Merge branch '20260114_feature_fjy_chubuchu_modi' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng bdf9b435ea 出不出 摸底 1 месяц назад
  jiandong.liu 2fa85f8692 Merge remote-tracking branch 'origin/master' 1 месяц назад
  jiandong.liu 9001a83a7b rollback 1 месяц назад
  jiandong.yh 19bcef2898 优化 1 месяц назад
  fanjinyang 2dafa8187b Merge branch '20260109_feature_fjy_chubuchu_morning' of algorithm/ad-engine into master 1 месяц назад
  zhaohaipeng e6adcc6d76 Merge branch 'feature_20250109_zhaohaipeng_log_optimize' of algorithm/ad-engine into master 1 месяц назад
  yaodaoseng 968322f13d 出不出 v5 1 месяц назад
  zhaohaipeng d897e0b008 feat:日志优化 1 месяц назад
  fanjinyang e173935a0a Merge branch '20260106_feature_fjy_chubuchu_v4' of algorithm/ad-engine into master 2 месяцев назад
  yaodaoseng 864df1bbaa 出不出 v4 2 месяцев назад
  yaodaoseng 1b7a828b2f 出不出 v4 2 месяцев назад
  liujiandong 50775381b6 Merge branch 'ljd/feature-20251230-youhua' of algorithm/ad-engine into master 2 месяцев назад
  jiandong.liu ca451c9e0c Merge branch 'master' into ljd/feature-20251230-youhua 2 месяцев назад
  jiandong.liu 4f2a36d621 日志开关 2 месяцев назад
  fanjinyang 8b5924f76c Merge branch '20260104_feature_fjy_holiday_optimization' of algorithm/ad-engine into master 2 месяцев назад
  yaodaoseng 020ff4f9a1 节日查询优化 2 месяцев назад
  fanjinyang 78352b0130 Merge branch '20251231_feature_fjy_chubuchu_v3' of algorithm/ad-engine into master 2 месяцев назад
  yaodaoseng fd65c16275 出不出广v3 2 месяцев назад
  fanjinyang ebe5e4a35b Merge branch '20251231_feature_fjy_chubuchu_v3' of algorithm/ad-engine into master 2 месяцев назад
  yaodaoseng fd7aa8fa67 出不出广v3 2 месяцев назад
  yaodaoseng e9549fa445 出不出广v3 2 месяцев назад
  liujiandong fc4b6ffdab Merge branch 'ljd/feature-20251230-youhua' of algorithm/ad-engine into master 2 месяцев назад
  jiandong.liu fd37dd4715 cpu性能优化 2 месяцев назад
  jiandong.liu 017b4e96d7 cpu性能优化 2 месяцев назад
  jiandong.liu a0551483e3 去掉无用日志 2 месяцев назад
  jiandong.liu cff68737e1 去掉无用日志 2 месяцев назад
  jiandong.liu 4a1d2f8dbe Merge remote-tracking branch 'origin/master' 2 месяцев назад
  jiandong.liu 9f148ba57b add log 2 месяцев назад
  fanjinyang 97e2cbc6f3 Merge branch '20251224_feature_fjy_ad_chubuchu_v2' of algorithm/ad-engine into master 2 месяцев назад
  yaodaoseng cbf122c8a4 出不出广v2 2 месяцев назад
  yaodaoseng eec1079f9d Merge branch 'master' into 20251224_feature_fjy_ad_chubuchu_v2 2 месяцев назад
  liujiandong e75568e999 Merge branch 'ljd/feature-20251224' of algorithm/ad-engine into master 2 месяцев назад
  jiandong.liu 803397cdbf 异常优化 2 месяцев назад
  jiandong.liu f05155f52e add log 2 месяцев назад
  yaodaoseng f8872cf182 出不出广v2 2 месяцев назад
  yaodaoseng 676ba950ae 出不出广v2 2 месяцев назад
  jiandong.liu e8c4d8055d 耗时优化 2 месяцев назад
  liujiandong cda236b665 Merge branch 'ljd/feature-20251224' of algorithm/ad-engine into master 2 месяцев назад
  jiandong.liu e2d46ae44f add log 2 месяцев назад
  yaodaoseng 51476df3b9 ror实验 2 месяцев назад
  yaodaoseng d578c7a668 ror实验 2 месяцев назад
  yaodaoseng e21e6608f0 ror实验 2 месяцев назад
  fanjinyang d4020c1baf Merge branch '20251219_feature_fjy_ad_chubuchu' of algorithm/ad-engine into master 2 месяцев назад
38 измененных файлов с 4676 добавлено и 194 удалено
  1. 1 1
      ad-engine-commons/pom.xml
  2. 5 1
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/enums/RedisPrefixEnum.java
  3. 0 1
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/feign/manager/service/HolidayService.java
  4. 0 2
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/helper/DnnCidDataHelper.java
  5. 121 46
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/score/model/PAIModelV1.java
  6. 1 1
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/AbUtil.java
  7. 42 4
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/ExtractorUtils.java
  8. 40 1
      ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/SimilarityUtils.java
  9. 2 0
      ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/AdRecommendController.java
  10. 9 7
      ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/ControllerAspect.java
  11. 1 1
      ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/PredictController.java
  12. 20 1
      ad-engine-server/src/main/resources/application-dev.yml
  13. 9 1
      ad-engine-server/src/main/resources/application.yml
  14. 4 2
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/feature/FeatureService.java
  15. 10 16
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/log/impl/LogHubServiceImpl.java
  16. 2 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/PredictModelService.java
  17. 154 46
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/impl/PredictModelServiceImpl.java
  18. 111 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByAppTypeTail.java
  19. 215 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByFissionRate.java
  20. 215 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByFissionRateCopy.java
  21. 102 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByMorning56.java
  22. 32 17
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRor.java
  23. 229 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRorCopy.java
  24. 230 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRorMorning.java
  25. 98 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyChubuchuModi.java
  26. 11 1
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/remote/FeatureV2RemoteService.java
  27. 12 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/impl/RankServiceImpl.java
  28. 1 8
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/scorer/PAIScorer.java
  29. 1 1
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/FeaturePrinterStrategy.java
  30. 8 2
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBasic.java
  31. 4 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy679.java
  32. 4 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy680.java
  33. 4 1
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy683.java
  34. 4 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy687.java
  35. 56 33
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy688.java
  36. 985 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy833.java
  37. 948 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy834.java
  38. 985 0
      ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy840.java

+ 1 - 1
ad-engine-commons/pom.xml

@@ -26,7 +26,7 @@
         <dependency>
             <groupId>com.tzld.piaoquan</groupId>
             <artifactId>recommend-feature-client</artifactId>
-            <version>1.1.24</version>
+            <version>1.1.32</version>
         </dependency>
         <dependency>
             <groupId>com.tzld.piaoquan</groupId>

+ 5 - 1
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/enums/RedisPrefixEnum.java

@@ -4,7 +4,11 @@ public enum RedisPrefixEnum {
 
     ADVANCE_SHOW_AD_FLAG("ad:advance:show:ad:flag:%s", "是否提前出广告标识,0-否;1-是"),
     ADVER_IS_API_EQ_0_IDS("ad:adver:isapi:0", "未回传广告主ID集合"),
-    AD_USER_ROR_BEHAVIOR("ad:user:ror:behavior:%s", "用户ror行为特征")
+    AD_USER_ROR_BEHAVIOR("ad:user:ror:behavior:%s", "用户ror行为特征"),
+    AD_USER_ROR_BEHAVIOR_COPY("ad:user:ror:behavior_copy:%s", "用户ror行为特征副本"),
+    AD_USER_FISSION_RATE_BEHAVIOR("ad:user:fission:rate:behavior:%s", "用户fission_rate行为特征"),
+    AD_USER_FISSION_RATE_BEHAVIOR_COPY("ad:user:fission:rate:behavior_copy:%s", "用户fission_rate行为特征副本"),
+    ADVANCE_HOLIDAY_FLAG("ad:advance:holiday:show:ad:flag:%s", "是否是节日,0-否;1-是"),
     ;
     private String prefix;
     private String desc;

+ 0 - 1
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/feign/manager/service/HolidayService.java

@@ -51,7 +51,6 @@ public class HolidayService {
      */
     public HolidayCalendarVO getTodayHoliday() {
         try {
-            log.info("调用节日查询接口");
             CommonResponse<HolidayCalendarVO> response = holidayCalendarFeign.isHoliday();
             
             if (response != null && response.isSuccess()) {

+ 0 - 2
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/helper/DnnCidDataHelper.java

@@ -70,7 +70,6 @@ public class DnnCidDataHelper {
     // 更新CID集合的方法
     private Set<Long> updateCidSet(String cidPath, String modelVersionPath) {
         try {
-            log.info("start init cid data");
             String modelVersion = readCsvPathFromOss(modelVersionPath);
             if (StringUtils.isEmpty(modelVersion)) {
                 log.error("modelVersion is null");
@@ -114,7 +113,6 @@ public class DnnCidDataHelper {
                         }
                     }
                 }
-                log.info("success get csv file size = {}", newCidSet.size());
             } else {
                 log.error("not found csv file");
             }

+ 121 - 46
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/score/model/PAIModelV1.java

@@ -6,7 +6,6 @@ import com.aliyun.openservices.eas.predict.request.TFDataType;
 import com.aliyun.openservices.eas.predict.request.TFRequest;
 import com.aliyun.openservices.eas.predict.response.TFResponse;
 import com.tzld.piaoquan.recommend.feature.domain.ad.base.AdRankItem;
-import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang.math.NumberUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -30,14 +29,6 @@ public class PAIModelV1 {
 
     private static final PredictClient client;
 
-    static {
-        model = new PAIModelV1();
-        client = new PredictClient(new HttpConfig());
-        client.setEndpoint("1894469520484605.vpc.cn-hangzhou.pai-eas.aliyuncs.com");
-        client.setToken("ODI1MmUxODgzZDc3ODM0ZmQwZWU0YTVjZjdlOWVlMGFlZGJjNTlkYQ==");
-        client.setModelName("ad_rank_dnn_v11_easyrec");
-    }
-
     private static final String[] sparseUserStrFeatures = {
             "brand", "region", "city", "cate1", "cate2", "user_cid_click_list", "user_cid_conver_list",
             "user_vid_return_tags_2h", "user_vid_return_tags_1d", "user_vid_return_tags_3d", "user_vid_return_tags_7d",
@@ -68,11 +59,16 @@ public class PAIModelV1 {
             "profession", "category_name"
     };
 
-    private final String[] userFeatures = {
+    private static final String[] userFeatures = {
             "viewAll", "clickAll", "converAll", "incomeAll", "ctr_all", "ctcvr_all", "cvr_all", "ecpm_all"
     };
 
-    private final String[] itemFeatures = {
+    /**
+     * 预计算的 userFeatures 小写映射,避免运行时重复调用 toLowerCase()
+     */
+    private static final String[] userFeaturesLowerCase;
+
+    private static final String[] itemFeatures = {
             "actionstatic_click", "actionstatic_ctcvr", "actionstatic_ctr", "actionstatic_view", "b2_12h_click", "b2_12h_conver",
             "b2_12h_conver_x_ctcvr", "b2_12h_conver_x_log_view", "b2_12h_ctcvr", "b2_12h_ctr", "b2_12h_cvr", "b2_12h_ecpm",
             "b2_1d_click", "b2_1d_conver", "b2_1d_conver_x_ctcvr", "b2_1d_conver_x_log_view", "b2_1d_ctcvr", "b2_1d_ctr",
@@ -122,13 +118,50 @@ public class PAIModelV1 {
             "vid_rank_ecpm_3d", "vid_rank_ecpm_7d"
     };
 
+    /**
+     * 预计算的 itemFeatures key 映射,避免运行时重复执行 replace 操作
+     * key: feature name, value: 转换后的 key(用于从 featureMap 中获取值)
+     */
+    private static final String[] itemFeatureKeys;
+
+    static {
+        model = new PAIModelV1();
+        client = new PredictClient(new HttpConfig());
+        client.setEndpoint("1894469520484605.vpc.cn-hangzhou.pai-eas.aliyuncs.com");
+        client.setToken("ODI1MmUxODgzZDc3ODM0ZmQwZWU0YTVjZjdlOWVlMGFlZGJjNTlkYQ==");
+        client.setModelName("ad_rank_dnn_v11_easyrec");
+
+        // 预计算 itemFeatures 的 key 映射
+        itemFeatureKeys = new String[itemFeatures.length];
+        for (int i = 0; i < itemFeatures.length; i++) {
+            itemFeatureKeys[i] = itemFeatures[i].replace("_x_", "*").replace("_view", "(view)");
+        }
+
+        // 预计算 userFeatures 的小写映射
+        userFeaturesLowerCase = new String[userFeatures.length];
+        for (int i = 0; i < userFeatures.length; i++) {
+            userFeaturesLowerCase[i] = userFeatures[i].toLowerCase();
+        }
+    }
+
 
     public List<Float> score(final List<AdRankItem> items,
                              final Map<String, String> userFeatureMap,
                              final Map<String, String> sceneFeatureMap) {
+        long totalStart = System.currentTimeMillis();
+        long buildUserFeatureTime = 0;
+        long buildItemFeatureTime = 0;
+        long buildRequestTime = 0;
+        long predictTime = 0;
+        long parseResponseTime = 0;
+        
+        final int size = items.size();
+        
         try {
             TFRequest request = new TFRequest();
 
+            // 阶段1: 构建用户特征
+            long stageStart = System.currentTimeMillis();
             for (String feature : sparseUserStrFeatures) {
                 String v = userFeatureMap.getOrDefault(feature, "");
                 request.addFeed(feature, TFDataType.DT_STRING, new long[]{1}, new String[]{v});
@@ -144,65 +177,107 @@ public class PAIModelV1 {
                 request.addFeed(feature, TFDataType.DT_INT64, new long[]{1}, new long[]{v});
             }
 
+            for (int i = 0; i < userFeatures.length; i++) {
+                double v = NumberUtils.toDouble(userFeatureMap.getOrDefault(userFeatures[i], "0.0"), 0.0);
+                request.addFeed(userFeaturesLowerCase[i], TFDataType.DT_DOUBLE, new long[]{1}, new double[]{v});
+            }
+            buildUserFeatureTime = System.currentTimeMillis() - stageStart;
 
-            for (String feature : userFeatures) {
-                double v = NumberUtils.toDouble(userFeatureMap.getOrDefault(feature, "0.0"), 0.0);
-                request.addFeed(feature.toLowerCase(), TFDataType.DT_DOUBLE, new long[]{1}, new double[]{v});
+            // 阶段2: 构建广告Item特征(优化版本)
+            stageStart = System.currentTimeMillis();
+            
+            // 预提取所有 item 的 featureMap 引用,避免重复调用 get 方法
+            Map[] featureMaps = new Map[size];
+            for (int i = 0; i < size; i++) {
+                featureMaps[i] = items.get(i).getFeatureMap();
             }
-            Map<String, double[]> doubleFeed = new HashMap<>();
-            Map<String, long[]> longFeed = new HashMap<>();
-            Map<String, String[]> strFeed = new HashMap<>();
-            for (int i = 0; i < items.size(); i++) {
-                for (String feature : itemFeatures) {
-                    String key = feature.replace("_x_", "*").replace("_view", "(view)");
-                    double[] doubles = doubleFeed.computeIfAbsent(feature, k -> new double[items.size()]);
-                    if (MapUtils.isEmpty(items.get(i).getFeatureMap())) {
+            
+            // 预分配所有数组
+            double[][] doubleArrays = new double[itemFeatures.length][size];
+            long[][] longArrays = new long[sparseAdLongFeatures.length][size];
+            String[][] strArrays = new String[sparseAdStrFeatures.length][size];
+            
+            // 按 feature 遍历(外层),提高缓存局部性
+            // 处理 double 类型特征
+            for (int f = 0; f < itemFeatures.length; f++) {
+                String key = itemFeatureKeys[f];
+                double[] doubles = doubleArrays[f];
+                for (int i = 0; i < size; i++) {
+                    Map<String, String> featureMap = featureMaps[i];
+                    if (featureMap == null || featureMap.isEmpty()) {
                         doubles[i] = 0.0;
-                        continue;
+                    } else {
+                        doubles[i] = NumberUtils.toDouble(featureMap.getOrDefault(key, "0.0"), 0.0);
                     }
-                    double v = NumberUtils.toDouble(items.get(i).getFeatureMap().getOrDefault(key, "0.0"), 0.0);
-                    doubles[i] = v;
                 }
-
-                for (String feature : sparseAdLongFeatures) {
-                    long[] longs = longFeed.computeIfAbsent(feature, k -> new long[items.size()]);
-                    if (MapUtils.isEmpty(items.get(i).getFeatureMap())) {
+            }
+            
+            // 处理 long 类型特征
+            for (int f = 0; f < sparseAdLongFeatures.length; f++) {
+                String feature = sparseAdLongFeatures[f];
+                long[] longs = longArrays[f];
+                for (int i = 0; i < size; i++) {
+                    Map<String, String> featureMap = featureMaps[i];
+                    if (featureMap == null || featureMap.isEmpty()) {
                         longs[i] = 0L;
-                        continue;
+                    } else {
+                        longs[i] = NumberUtils.toLong(featureMap.getOrDefault(feature, "0"), 0L);
                     }
-                    long v = NumberUtils.toLong(items.get(i).getFeatureMap().getOrDefault(feature, "0"), 0L);
-                    longs[i] = v;
                 }
-
-                for (String feature : sparseAdStrFeatures) {
-                    String[] strs = strFeed.computeIfAbsent(feature, k -> new String[items.size()]);
-                    if (MapUtils.isEmpty(items.get(i).getFeatureMap())) {
+            }
+            
+            // 处理 String 类型特征
+            for (int f = 0; f < sparseAdStrFeatures.length; f++) {
+                String feature = sparseAdStrFeatures[f];
+                String[] strs = strArrays[f];
+                for (int i = 0; i < size; i++) {
+                    Map<String, String> featureMap = featureMaps[i];
+                    if (featureMap == null || featureMap.isEmpty()) {
                         strs[i] = "";
-                        continue;
+                    } else {
+                        strs[i] = featureMap.getOrDefault(feature, "");
                     }
-                    String v = items.get(i).getFeatureMap().getOrDefault(feature, "");
-                    strs[i] = v;
                 }
             }
-            for (Map.Entry<String, double[]> entry : doubleFeed.entrySet()) {
-                request.addFeed(entry.getKey(), TFDataType.DT_DOUBLE, new long[]{items.size()}, entry.getValue());
+            buildItemFeatureTime = System.currentTimeMillis() - stageStart;
+
+            // 阶段3: 构建请求体
+            stageStart = System.currentTimeMillis();
+            long[] shape = new long[]{size};
+            
+            for (int f = 0; f < itemFeatures.length; f++) {
+                request.addFeed(itemFeatures[f], TFDataType.DT_DOUBLE, shape, doubleArrays[f]);
             }
 
-            for (Map.Entry<String, long[]> entry : longFeed.entrySet()) {
-                request.addFeed(entry.getKey(), TFDataType.DT_INT64, new long[]{items.size()}, entry.getValue());
+            for (int f = 0; f < sparseAdLongFeatures.length; f++) {
+                request.addFeed(sparseAdLongFeatures[f], TFDataType.DT_INT64, shape, longArrays[f]);
             }
 
-            for (Map.Entry<String, String[]> entry : strFeed.entrySet()) {
-                request.addFeed(entry.getKey(), TFDataType.DT_STRING, new long[]{items.size()}, entry.getValue());
+            for (int f = 0; f < sparseAdStrFeatures.length; f++) {
+                request.addFeed(sparseAdStrFeatures[f], TFDataType.DT_STRING, shape, strArrays[f]);
             }
             request.addFetch("probs");
+            buildRequestTime = System.currentTimeMillis() - stageStart;
+
+            // 阶段4: PAI-EAS 远程调用
+            stageStart = System.currentTimeMillis();
             TFResponse response = client.predict(request);
+            predictTime = System.currentTimeMillis() - stageStart;
+
+            // 阶段5: 解析响应
+            stageStart = System.currentTimeMillis();
             List<Float> result = response.getFloatVals("probs");
+            parseResponseTime = System.currentTimeMillis() - stageStart;
+
             if (!CollectionUtils.isEmpty(result)) {
                 return result;
             }
         } catch (Exception e) {
-            LOGGER.error("PAIModel score error", e);
+            long totalTime = System.currentTimeMillis() - totalStart;
+            LOGGER.error("PAIModelV1.score error: total={}ms, itemSize={}, buildUserFeature={}ms, " +
+                         "buildItemFeature={}ms, buildRequest={}ms, predict={}ms, parseResponse={}ms",
+                         totalTime, items.size(), buildUserFeatureTime, buildItemFeatureTime, 
+                         buildRequestTime, predictTime, parseResponseTime, e);
         }
         return new ArrayList<>(Collections.nCopies(items.size(), 0.0f));
     }

+ 1 - 1
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/AbUtil.java

@@ -8,7 +8,7 @@ import java.util.stream.Collectors;
 
 public class AbUtil {
 
-    public static final List<String> adAlgExpCode = Arrays.asList("679", "680", "683", "687", "688");
+    public static final List<String> adAlgExpCode = Arrays.asList("679", "680", "683", "687", "688", "833", "834", "840");
 
     public static Set<String> unfoldAllExpCode(List<Map<String, String>> adAbMap) {
         if (CollectionUtils.isEmpty(adAbMap)) {

+ 42 - 4
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/ExtractorUtils.java

@@ -64,18 +64,45 @@ public class ExtractorUtils {
     }
 
     public static Double[] funcC34567ForTagsNew(String tags, String title) {
+        return funcC34567ForTagsNewWithCache(tags, title, null);
+    }
+
+    /**
+     * 优化版本:支持缓存 title 和 tag 的分词结果
+     * @param tags 标签字符串,逗号分隔
+     * @param title 标题
+     * @param titleWords title 的分词结果缓存,如果为 null 则现场分词
+     * @param tagWordsCache tag 分词结果缓存(请求级别),可以为 null
+     * @return [匹配数量, 最大相似度, 平均相似度]
+     */
+    public static Double[] funcC34567ForTagsNewWithCache(String tags, String title, List<String> titleWords, Map<String, List<String>> tagWordsCache) {
+        if (tags == null || tags.isEmpty() || title == null || title.isEmpty()) {
+            return EMPTY_RESULT;
+        }
+
         String[] tagsList = tags.split(",");
         int d1 = 0;
-        List<String> d2 = new ArrayList<>();
         double d3 = 0.0;
         double d4 = 0.0;
 
+        // 只分词一次 title,复用结果
+        List<String> cachedTitleWords = (titleWords != null) ? titleWords : SimilarityUtils.segment(title);
+
         for (String tag : tagsList) {
+            if (tag == null || tag.isEmpty()) {
+                continue;
+            }
             if (title.contains(tag)) {
                 d1++;
-                d2.add(tag);
             }
-            double score = SimilarityUtils.word2VecSimilarity(tag, title);
+            // 使用请求级别的 tag 分词缓存
+            List<String> tagWords;
+            if (tagWordsCache != null) {
+                tagWords = tagWordsCache.computeIfAbsent(tag, SimilarityUtils::segment);
+            } else {
+                tagWords = SimilarityUtils.segment(tag);
+            }
+            float score = SimilarityUtils.word2VecSimilarityWithWords(tagWords, cachedTitleWords);
             if (score > d3) {
                 d3 = score;
             }
@@ -84,10 +111,21 @@ public class ExtractorUtils {
 
         d4 = (tagsList.length > 0) ? d4 / tagsList.length : d4;
 
-        // 使用数组来返回多个值
         return new Double[]{(double) d1, d3, d4};
     }
 
+    /**
+     * 兼容旧接口
+     */
+    public static Double[] funcC34567ForTagsNewWithCache(String tags, String title, List<String> titleWords) {
+        return funcC34567ForTagsNewWithCache(tags, title, titleWords, null);
+    }
+
+    /**
+     * 空结果常量,避免重复创建数组
+     */
+    private static final Double[] EMPTY_RESULT = new Double[]{0.0, 0.0, 0.0};
+
     public static Double calDiv(double a, double b) {
         if (a == 0 || b == 0) {
             return 0D;

+ 40 - 1
ad-engine-commons/src/main/java/com/tzld/piaoquan/ad/engine/commons/util/SimilarityUtils.java

@@ -52,12 +52,51 @@ public final class SimilarityUtils {
         }
     }
 
-
+    /**
+     * 原始方法,保持兼容
+     */
     public static float word2VecSimilarity(String str1, String str2) {
         List<String> words1 = Segment.getWords(str1);
         List<String> words2 = Segment.getWords(str2);
         return vec.sentenceSimilarity(words1, words2);
     }
 
+    /**
+     * 优化版本:复用已分词的结果,避免重复分词
+     * @param words1 已分词的词列表
+     * @param words2 已分词的词列表
+     * @return 相似度分数
+     */
+    public static float word2VecSimilarityWithWords(List<String> words1, List<String> words2) {
+        if (words1 == null || words1.isEmpty() || words2 == null || words2.isEmpty()) {
+            return 0.0f;
+        }
+        return vec.sentenceSimilarity(words1, words2);
+    }
+
+    /**
+     * 优化版本:对一个文本与多个文本计算相似度,复用第一个文本的分词结果
+     * @param str1 第一个文本
+     * @param str2 第二个文本
+     * @param str1Words 第一个文本已分词的结果(如果为null则现场分词)
+     * @return 相似度分数
+     */
+    public static float word2VecSimilarityWithCache(String str1, String str2, List<String> str1Words) {
+        List<String> words1 = (str1Words != null) ? str1Words : Segment.getWords(str1);
+        List<String> words2 = Segment.getWords(str2);
+        return vec.sentenceSimilarity(words1, words2);
+    }
+
+    /**
+     * 分词方法,供外部缓存使用
+     * @param text 待分词文本
+     * @return 分词结果
+     */
+    public static List<String> segment(String text) {
+        if (text == null || text.isEmpty()) {
+            return java.util.Collections.emptyList();
+        }
+        return Segment.getWords(text);
+    }
 
 }

+ 2 - 0
ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/AdRecommendController.java

@@ -54,6 +54,7 @@ public class AdRecommendController {
                 contentMap.put("adScore", rankResult.getScore());
                 Double ctcvrScore = rankResult.getScoreMap().get("ctcvrScore");
                 if (ctcvrScore != null && ctcvrScore > 0) {
+                    contentMap.put("ctcvrScore", ctcvrScore);
                     if (rankResult.getExt().get("ecpm") != null) {
                         contentMap.put("ecpm", rankResult.getExt().get("ecpm"));
                     }
@@ -82,6 +83,7 @@ public class AdRecommendController {
                     participateCompetitionType.add("guarantee");
                 }
                 contentMap.put("participateCompetitionType", StringUtils.join(participateCompetitionType, ","));
+                contentMap.put("coefficientRate", rankResult.getExt().get("coefficientRate"));
                 map.put("content", contentMap);
                 return map;
             }

+ 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;

+ 1 - 1
ad-engine-server/src/main/java/com/tzld/piaoquan/ad/engine/server/controller/PredictController.java

@@ -33,7 +33,7 @@ public class PredictController {
     @RequestMapping("/ab/model")
     public Map<String,Object> adPredictByAbTestModel(@RequestBody ThresholdPredictModelRequestParam param){
 
-        return predictModelService.adPredict(param);
+        return predictModelService.adPredictWithDataFill(param);
     }
 
 

+ 20 - 1
ad-engine-server/src/main/resources/application-dev.yml

@@ -26,9 +26,28 @@ spring:
       #username: wx2023_ad
       #password: wx2023_adP@assword1234
 
+  redis:
+    hostName: r-bp1ps6my7lzg8rdhwx682.redis.rds.aliyuncs.com
+    port: 6379
+    password: Wqsd@2019
+    lettuce:
+      pool:
+        max-active: 200 # 连接池最大连接数(使用负值表示没有限制) 默认 8
+        max-idle: 20 # 连接池中的最大空闲连接 默认 8
+        min-idle: 10 # 连接池中的最小空闲连接 默认 0
+
+  redis-ad:
+    hostName: r-bp1ps6my7lzg8rdhwx682.redis.rds.aliyuncs.com
+    port: 6379
+    password: Wqsd@2019
+    timeout: 1000
+    lettuce:
+      pool:
+        max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) 默认 8
+        max-idle: 20 # 连接池中的最大空闲连接 默认 8
+        min-idle: 10 # 连接池中的最小空闲连接 默认 0
   redis-algorithm:
     hostName: r-bp1ps6my7lzg8rdhwx682.redis.rds.aliyuncs.com
-    #hostName: r-bp1fogs2mflr1ybfot.redis.rds.aliyuncs.com
     port: 6379
     password: Wqsd@2019
     timeout: 1000

+ 9 - 1
ad-engine-server/src/main/resources/application.yml

@@ -128,10 +128,18 @@ grpc:
   client:
     recommend-feature:
       negotiationType: PLAINTEXT
+      # KeepAlive 配置(防止空闲连接被网关关闭)
+      enable-keep-alive: true
+      keep-alive-time: 20s
+      keep-alive-timeout: 5s
+      keep-alive-without-calls: true
+      max-inbound-message-size: 20MB
+      negotiation-type: plaintext
     recommend-server:
+      max-inbound-message-size: 20MB
       negotiationType: PLAINTEXT
     GLOBAL:
-      max-inbound-message-size: 5535509
+      max-inbound-message-size: 20MB
 model:
   xgboost:
     path: xgboost

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

@@ -30,7 +30,6 @@ public class FeatureService {
 
     public Feature getFeature(Collection<String> cidList, Collection<String> adVerIdList, List<Long> skuIdList, ScoreParam param) {
         AdRequestContext context = param.getRequestContext();
-
         List<FeatureKeyProto> protos = new ArrayList<>();
         for (String cidStr : cidList) {
 
@@ -63,7 +62,6 @@ public class FeatureService {
                 protos.add(genWithCidAndVid("alg_cid_feature_vid_cf", cidStr, param.getVideoId().toString()));
             }
         }
-
         // skuid
         for (Long skuId : skuIdList) {
             if (StringUtils.isNotEmpty(param.getMid())) {
@@ -117,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);
             }
         });

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

@@ -9,6 +9,8 @@ public interface PredictModelService {
 
     public Map<String,Object> adPredict(ThresholdPredictModelRequestParam requestParam);
 
+    public Map<String, Object> adPredictWithDataFill(ThresholdPredictModelRequestParam requestParam);
+
     public Map<String,Object> adRecommendPredictByRoiModel(RoiPredictModelRequestParam requestParam);
 }
 

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

@@ -79,9 +79,22 @@ public class PredictModelServiceImpl implements PredictModelService {
     @Autowired
     private PredictStrategyBy820 predictStrategyBy820;
     @Autowired
+    private PredictStrategyByMorning56 predictStrategyByMorning56;
+    @Autowired
     private PredictStrategyBy823 predictStrategyBy823;
     @Autowired
     private PredictStrategyByRor predictStrategyByRor;
+    @Autowired
+    private PredictStrategyByRorMorning predictStrategyByRorMorning;
+    @Autowired
+    private PredictStrategyByFissionRate predictStrategyByFissionRate;
+    @Autowired
+    private PredictStrategyByRorCopy predictStrategyByRorCopy;
+    @Autowired
+    private PredictStrategyByFissionRateCopy predictStrategyByFissionRateCopy;
+    @Autowired
+    private PredictStrategyByAppTypeTail predictStrategyByAppTypeTail;
+
 
     @Autowired
     private UserService userService;
@@ -118,6 +131,9 @@ public class PredictModelServiceImpl implements PredictModelService {
     @Value("${experiment.817.ad.hour:5,8}")
     private String experiment817WithAdHour;
 
+    @Value("${experiment.morning56.ad.hour:5,6}")
+    private String experimentMorning56WithAdHour;
+
     @Autowired
     private AdRedisHelper adRedisHelper;
 
@@ -128,6 +144,21 @@ public class PredictModelServiceImpl implements PredictModelService {
     List<Integer> appIdArr = Arrays.asList(new Integer[]{0, 3, 4, 5, 6, 17, 18, 19, 21, 22});
 
 
+    public Map<String, Object> adPredictWithDataFill(ThresholdPredictModelRequestParam requestParam) {
+
+        Map<String, Object> result = adPredict(requestParam);
+
+        if (MapUtils.isNotEmpty(result)) {
+            PredictContext predictContext = ConvertUtil.predictParam2Context(requestParam);
+            Map<String, Object> ecpmExperiment = predictStrategyByAppTypeTail.predict(predictContext);
+            // 填充 ecpm实验 参数
+            if (MapUtils.isNotEmpty(ecpmExperiment)) {
+                result.putAll(ecpmExperiment);
+            }
+        }
+        return result;
+    }
+
     public Map<String, Object> adPredict(ThresholdPredictModelRequestParam requestParam) {
         Map<String, Object> result = new HashMap<>();
         result.put("pqtId", requestParam.getPqtId());
@@ -166,10 +197,25 @@ public class PredictModelServiceImpl implements PredictModelService {
                 expCodes.add(expCode);
             }
             boolean execute817 = false;
+            boolean in817time = false;
+            boolean inMorning56time = false;
             // 不出广告时间判定
             int hourOfDay = DateUtils.getCurrentHour();
             if (0 <= hourOfDay && hourOfDay < 8 && !isAdvanceShowAd()) {
                 // 0点到8点 && 不是节日
+
+                // 判断是否在morning56时间范围(5-6点)
+                boolean isInMorning56Time = false;
+                try{
+                    String[] split = experimentMorning56WithAdHour.split(",");
+                    if ( Integer.parseInt(split[0]) <= hourOfDay && hourOfDay < Integer.parseInt(split[1]) ) {
+                        isInMorning56Time = true;
+                    }
+                }catch (Exception e){
+                    log.error("experimentMorning56WithAdHour配置异常", e);
+                }
+
+                // 判断是否在817时间范围
                 boolean isIn817Time = false;
                 try {
                     String[] split = experiment817WithAdHour.split(",");
@@ -179,9 +225,14 @@ public class PredictModelServiceImpl implements PredictModelService {
                 } catch (Exception e) {
                     log.error("experiment817WithAdHour配置异常", e);
                 }
-                // 在 817实验时间范围 && 817实验开启
-                if (isIn817Time && expCodes.contains("817")) {
-                    execute817 = true;
+
+                // 优先判断morning56(5-6点)
+                if (isInMorning56Time) {
+                    inMorning56time = true;
+                } else if (isIn817Time) {
+                    // 在 817实验时间范围 && 817实验开启
+                    in817time = true;
+                    execute817 = expCodes.contains("817");
                 } else {
                     result.put("ad_predict", 1);
                     result.put("no_ad_strategy", "no_ad_time_with_fixed_time");
@@ -198,6 +249,22 @@ public class PredictModelServiceImpl implements PredictModelService {
 
 
             PredictContext predictContext = ConvertUtil.predictParam2Context(requestParam);
+
+            // morning56策略(5-6点,所有用户执行)
+            if(inMorning56time){
+                return predictStrategyByMorning56.predict(predictContext);
+            }
+
+            if(in817time){
+
+                // 早间ror熔断
+                Map<String, Object> userRorPredictMorning = predictStrategyByRorMorning.predict(predictContext);
+                if (MapUtils.isNotEmpty(userRorPredictMorning)) {
+                    return userRorPredictMorning;
+                }
+            }
+
+
             // 817熔断
             if (execute817) {
                 return predictStrategyBy817.predict(predictContext);
@@ -212,9 +279,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)) {
@@ -226,16 +293,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;
+            //     }
+            // }
 
             if (expCodes.contains("673")) {
                 Map<String, Object> predictResult = predictStrategyBy673.predict(predictContext);
@@ -247,6 +314,7 @@ public class PredictModelServiceImpl implements PredictModelService {
                     return predictResult;
                 }
             }
+
             // ror行为策略
             Map<String, Object> userRorPredict = predictStrategyByRor.predict(predictContext);
             if (MapUtils.isNotEmpty(userRorPredict)) {
@@ -257,6 +325,38 @@ public class PredictModelServiceImpl implements PredictModelService {
                 return userRorPredict;
             }
 
+            // ror行为策略
+            Map<String, Object> userRorPredictCopy = predictStrategyByRorCopy.predict(predictContext);
+            if (MapUtils.isNotEmpty(userRorPredictCopy)) {
+                // 填充 819 参数
+                if (MapUtils.isNotEmpty(predictExtInfo)) {
+                    userRorPredictCopy.putAll(predictExtInfo);
+                }
+                return userRorPredictCopy;
+            }
+
+
+            // fission_rate行为策略
+            Map<String, Object> userFissionRatePredict = predictStrategyByFissionRate.predict(predictContext);
+            if (MapUtils.isNotEmpty(userFissionRatePredict)) {
+                // 填充 819 参数
+                if (MapUtils.isNotEmpty(predictExtInfo)) {
+                    userFissionRatePredict.putAll(predictExtInfo);
+                }
+                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 userFissionRatePredictCopy;
+            }
+
+
             Map<String, Object> predictResult;
             if (expCodes.contains("599")) {
                 predictResult = predictStrategyBy599.predict(predictContext);
@@ -736,34 +836,42 @@ public class PredictModelServiceImpl implements PredictModelService {
      *
      * @return true-当前时间在6点之后且为节日,false-不满足条件
      */
-    public boolean isEarlyMorningHoliday() {
-        try {
-            // 获取当前小时
-            int currentHour = DateUtils.getCurrentHour();
-
-            // 判断时间是否在6点之后
-            if (currentHour >= 6) {
-                log.info("当前时间{}点在6点之后,开始检查是否为节日", currentHour);
-
-                // 调用节日服务判断今日是否为节日
-                boolean isHoliday = holidayService.isTodayHoliday();
-
-                if (isHoliday) {
-                    log.info("当前时间{}点6点之后且今日是节日,返回true", currentHour);
-                    return true;
-                } else {
-                    log.info("当前时间{}点在6点之后但今日不是节日,返回false", currentHour);
-                    return false;
-                }
-            } else {
-                log.debug("当前时间{}点不在6点之后,返回false", currentHour);
-                return false;
-            }
-
-        } catch (Exception e) {
-            log.error("判断早晨节日时间异常", e);
-            // 异常情况下返回false,确保系统稳定性
-            return false;
-        }
-    }
+	    public boolean isEarlyMorningHoliday() {
+	        try {
+	            // 获取当前小时,6点之前直接返回 false,无需查询节日服务或缓存
+	            int currentHour = DateUtils.getCurrentHour();
+	            if (currentHour < 6) {
+	                return false;
+	            }
+
+	            String day = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now());
+	            String redisKey = String.format(RedisPrefixEnum.ADVANCE_HOLIDAY_FLAG.getPrefix(), day);
+	            String flag = adRedisHelper.get(redisKey);
+
+	            // 先从缓存读取当日节日标记
+	            if (StringUtils.isNotBlank(flag)) {
+	                boolean isHoliday = StringUtils.equals("1", flag);
+	                return isHoliday;
+	            }
+
+	            // 调用节日服务判断今日是否为节日
+	            boolean isHoliday = holidayService.isTodayHoliday();
+
+	            // 查询结果回种缓存,保存 10 小时
+	            String cacheVal = isHoliday ? "1" : "0";
+	            boolean cacheResult = adRedisHelper.set(redisKey, cacheVal, 10 * 60 * 60);
+	            log.info("写入早晨节日标记缓存: {} -> {}, ttl=10h, success={}", redisKey, cacheVal, cacheResult);
+
+	            if (isHoliday) {
+	                return true;
+	            } else {
+	                return false;
+	            }
+
+	        } catch (Exception e) {
+	            log.error("判断早晨节日时间异常", e);
+	            // 异常情况下返回false,确保系统稳定性
+	            return false;
+	        }
+	    }
 }

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

@@ -0,0 +1,111 @@
+package com.tzld.piaoquan.ad.engine.service.predict.v2;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 基于 appType 和 rootSessionId 尾号的广告预测策略
+ * <p>
+ * 核心逻辑:
+ * 1. 根据 appType 和 rootSessionId 的尾号进行匹配
+ * 2. 只做数据填充(如 ecpmValue),不决定是否出广告
+ * 3. 返回匹配的配置信息,供后续流程使用
+ * <p>
+ * 配置格式示例:
+ * <pre>
+ * [
+ *   {
+ *     "appType": ["0", "3"],
+ *     "tail": ["0", "1", "2"],
+ *     "config": {
+ *       "ecpmValue": 60
+ *     }
+ *   },
+ *   {
+ *     "appType": ["1", "4"],
+ *     "tail": ["0", "1", "2"],
+ *     "config": {
+ *       "ecpmValue": 50
+ *     }
+ *   }
+ * ]
+ * </pre>
+ */
+@Slf4j
+@Service
+public class PredictStrategyByAppTypeTail extends BasicPredict {
+
+    /**
+     * Apollo 动态配置:根据 rootSessionId 尾号和 appType 进行匹配
+     */
+    @ApolloJsonValue("${experiment.app.type.tail.ecpm.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+    public String name() {
+        return "app_type_tail_ecpm_strategy";
+    }
+
+    /**
+     * 核心预测方法:根据 appType 和 rootSessionId 尾号填充数据
+     *
+     * @param ctx 预测上下文,包含 appType、rootSessionId 等信息
+     * @return 预测结果 Map,包含:
+     *         - 匹配的配置信息(如 ecpmValue)
+     *         - 返回 null 表示未匹配到任何配置,跳过该策略
+     */
+    @Override
+    public Map<String, Object> predict(PredictContext ctx) {
+        try {
+            String rootSessionId = ctx.getRootSessionId();
+            String appType = ctx.getAppType();
+
+            // 前置校验:配置为空或必要参数为空时,返回 null(跳过该策略)
+            if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId, appType)) {
+                return null;
+            }
+
+            // 提取 rootSessionId 的最后一个字符作为尾号
+            String tail = rootSessionId.substring(rootSessionId.length() - 1);
+
+            // 遍历配置项,查找同时匹配 appType 和尾号的配置
+            for (RootSessionIdTailConfigItem item : configItems) {
+                if (item.getAppType() != null && item.getTail() != null) {
+                    // 检查是否同时匹配 appType 和 tail
+                    if (item.getAppType().contains(appType) && item.getTail().contains(tail)) {
+                        // 匹配成功,填充数据
+                        Map<String, Object> rtnMap = new HashMap<>();
+                        
+                        // 将配置中的所有参数填充到返回结果中
+                        if (item.getConfig() != null) {
+                            rtnMap.putAll(item.getConfig());
+                        }
+                        rtnMap.put("ecpm_model",name());
+                        rtnMap.put("ecpm_rootSessionId", ctx.getRootSessionId());
+                        
+                        return rtnMap;
+                    }
+                }
+            }
+
+            // 未匹配到任何配置,返回 null
+            return null;
+
+        } catch (Exception e) {
+            log.error("[PredictStrategyByAppTypeTail] predict error, ctx: {}", ctx, e);
+            return null;
+        }
+    }
+}
+

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

@@ -0,0 +1,215 @@
+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 PredictStrategyByFissionRate 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.fission.rate.root.session.id.tail.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    @Value("${experiment.fission.rate.show.log.switch:0}")
+    private String experimentFissionRateShowLogSwitch;
+
+    private static final String TABLE_NAME = "alg_mid_history_behavior_1month";
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+    public String name() {
+        return "fission_rate_strategy";
+    }
+
+    /**
+     * 核心预测方法:决定是否向用户展示广告
+     *
+     * @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 = getDefaultFissionRate(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(experimentFissionRateShowLogSwitch)) {
+                log.info("[PredictStrategyByFissionRate] 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 showAdFissionRate = getShowAdFissionRate(launchs, ror, adLevel, return30day);
+
+            if (showAdFissionRate == null) {
+                return null;
+            }
+
+            // 记录决策相关的特征和参数,用于日志分析和效果追踪
+            rtnMap.putAll(rtnAdPredict(ctx));
+            rtnMap.put("model", this.name());
+            rtnMap.put("launchs", launchs);
+            rtnMap.put("ror", ror);
+            rtnMap.put("ad_level", adLevel);
+            rtnMap.put("return_30day", return30day);
+            rtnMap.put("fission_rate", showAdFissionRate);
+            return rtnMap;
+        } catch (Exception e) {
+            log.error("[PredictStrategyByFissionRate] 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 getShowAdFissionRate(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_FISSION_RATE_BEHAVIOR.getPrefix(), keyId);
+
+            // 从 Redis 获取概率值
+            String fissionRate = adRedisHelper.get(key);
+
+            // 解析概率值,如果为 null 则使用默认值
+            return StringUtils.isBlank(fissionRate) ? null : Double.parseDouble(fissionRate);
+        } catch (Exception e) {
+            // 解析失败(如非数字字符串)或 Redis 异常,记录错误并使用默认值
+            log.error("getShowAdFissionRate 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 getDefaultFissionRate(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_fission_rate");
+                    return true;
+                }
+            }
+
+        }
+
+        // 未匹配到任何配置
+        return false;
+    }
+
+}

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

@@ -0,0 +1,215 @@
+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 PredictStrategyByFissionRateCopy 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.fission.rate.root.session.id.tail.copy.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    @Value("${experiment.fission.rate.show.log.switch:0}")
+    private String experimentFissionRateShowLogSwitch;
+
+    private static final String TABLE_NAME = "alg_mid_history_behavior_1month";
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+    public String name() {
+        return "fission_rate_strategy_copy";
+    }
+
+    /**
+     * 核心预测方法:决定是否向用户展示广告
+     *
+     * @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 = getDefaultFissionRate(rootSessionId, appType);
+
+            // 用户不在实验分桶内,跳过该策略
+            if (!matchResult) {
+                return null;
+            }
+
+            Map<String, Object> rtnMap = new HashMap<>();
+
+            // 用户行为特征变量(来自离线特征表 alg_mid_history_behavior_1month)
+            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());
+            if ("1".equals(experimentFissionRateShowLogSwitch)) {
+                log.info("[PredictStrategyByFissionRate] 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 = 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 showAdFissionRate = getShowAdFissionRate(launchs, ror, adLevel, return30day);
+
+            if (showAdFissionRate == null) {
+                return null;
+            }
+
+            // 记录决策相关的特征和参数,用于日志分析和效果追踪
+            rtnMap.putAll(rtnAdPredict(ctx));
+            rtnMap.put("model", this.name());
+            rtnMap.put("launchs", launchs);
+            rtnMap.put("ror", ror);
+            rtnMap.put("ad_level", adLevel);
+            rtnMap.put("return_30day", return30day);
+            rtnMap.put("fission_rate", showAdFissionRate);
+            return rtnMap;
+        } catch (Exception e) {
+            log.error("[PredictStrategyByFissionRate] 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 getShowAdFissionRate(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_FISSION_RATE_BEHAVIOR_COPY.getPrefix(), keyId);
+
+            // 从 Redis 获取概率值
+            String fissionRate = adRedisHelper.get(key);
+
+            // 解析概率值,如果为 null 则使用默认值
+            return StringUtils.isBlank(fissionRate) ? null : Double.parseDouble(fissionRate);
+        } catch (Exception e) {
+            // 解析失败(如非数字字符串)或 Redis 异常,记录错误并使用默认值
+            log.error("getShowAdFissionRate 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 getDefaultFissionRate(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_fission_rate");
+                    return true;
+                }
+            }
+
+        }
+
+        // 未匹配到任何配置
+        return false;
+    }
+
+}

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

@@ -0,0 +1,102 @@
+package com.tzld.piaoquan.ad.engine.service.predict.v2;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.piaoquan.ad.engine.commons.util.JSONUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Slf4j
+@Service
+public class PredictStrategyByMorning56 extends BasicPredict {
+
+    @ApolloJsonValue("${experiment.morning56.root.session.id.tail.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    @Override
+    public String name() {
+        return "morning56";
+    }
+
+    @Override
+    public Map<String, Object> predict(PredictContext ctx) {
+
+        String rootSessionId = ctx.getRootSessionId();
+        String userLayer = ctx.getUserLayer();
+        String shareType = ctx.getShareType();
+        if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId, userLayer, shareType)) {
+            Map<String, Object> returnMap = new HashMap<>();
+            returnMap.putAll(rtnNoAdPredict(ctx));
+            returnMap.put("msg","no_config_error");
+            return returnMap;
+        }
+
+        double score = this.calcScoreByMid(ctx.getMid());
+        String tail = rootSessionId.substring(rootSessionId.length() - 1);
+        if (StringUtils.isNotBlank(shareType)) {
+            shareType = shareType.replace("return", "")
+                    .replace("mids", "");
+        }
+
+        for (RootSessionIdTailConfigItem item : configItems) {
+            if (item.getTail().contains(tail)) {
+                Map<String, Object> returnMap = new HashMap<>();
+                double threshold;
+                String thresholdKey = "";
+
+                // 生成可用的Key列表
+                List<String> keys = Arrays.asList(userLayer + "_" + shareType, "default_" + shareType, "default_threshold");
+                for (String key : keys) {
+                    if (item.getConfig().containsKey(key)) {
+                        thresholdKey = key;
+                        break;
+                    }
+                }
+
+
+                threshold = item.getConfig().getOrDefault(thresholdKey, 0.0);
+
+                if (score < threshold) {
+                    returnMap.putAll(rtnAdPredict(ctx));
+                    returnMap.put("model", this.name());
+                } else {
+                    returnMap.putAll(rtnNoAdPredict(ctx));
+                    returnMap.put("no_ad_strategy", this.name());
+                }
+
+                returnMap.put("score", score);
+                returnMap.put("threshold", threshold);
+                returnMap.put("userLayer", userLayer);
+
+                JSONObject logJson = new JSONObject();
+                logJson.putAll(returnMap);
+                logJson.put("mid", ctx.getMid());
+                logJson.put("appType", ctx.getAppType());
+                logJson.put("rootSessionIdTail", tail);
+                logJson.put("shareType", shareType);
+
+                logJson.put("expId", "morning56Exp");
+                logJson.put("thresholdParamKey", thresholdKey);
+                logJson.put("adPlatformType", ctx.getAdPlatformType());
+                logJson.put("abCode", ctx.getAdAbCode());
+                logJson.put("configItem", item);
+                logJson.put("rootSessionId", ctx.getRootSessionId());
+
+                log.info("广告跳出选择 -- morning56实验结果: {}, 参数: {}",
+                        JSONUtils.toJson(returnMap), logJson.toJSONString());
+
+                return returnMap;
+            }
+        }
+
+        Map<String, Object> returnMap = new HashMap<>();
+        returnMap.putAll(rtnNoAdPredict(ctx));
+        returnMap.put("msg","no_config_error");
+        return returnMap;
+    }
+
+}

+ 32 - 17
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/predict/v2/PredictStrategyByRor.java

@@ -1,5 +1,6 @@
 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;
@@ -9,6 +10,7 @@ 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;
@@ -54,6 +56,9 @@ public class PredictStrategyByRor extends BasicPredict {
     @ApolloJsonValue("${experiment.ror.root.session.id.tail.config:[]}")
     private List<RootSessionIdTailConfigItem> configItems;
 
+    @Value("${experiment.ror.show.log.switch:1}")
+    private String experimentRorShowLogSwitch;
+
     private static final String TABLE_NAME = "alg_mid_history_behavior_1month";
 
     /**
@@ -89,10 +94,10 @@ public class PredictStrategyByRor extends BasicPredict {
             String appType = ctx.getAppType();
 
             // 获取默认概率阈值(基于 rootSessionId 尾号和 appType 匹配配置)
-            Double defaultProbability = getDefaultProbability(rootSessionId, appType);
+            Boolean matchResult = getDefaultProbability(rootSessionId, appType);
 
             // 用户不在实验分桶内,跳过该策略
-            if (defaultProbability == null) {
+            if (!matchResult) {
                 return null;
             }
 
@@ -102,28 +107,36 @@ public class PredictStrategyByRor extends BasicPredict {
             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("[PredictStrategyByRor] mid:{}, feature:{}, featureStr: {}", ctx.getMid(),feature,JSON.toJSONString(feature));
+            }
             // 安全地提取特征值(多层 null 检查)
-            if (feature != null && feature.getUserFeature() != null && feature.getUserFeature().get("") != 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, defaultProbability);
+            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) {
+            if (score < showAdProbability) {
                 // 展示广告,ad_predict = 2
                 rtnMap.putAll(rtnAdPredict(ctx));
                 rtnMap.put("model", this.name());
@@ -137,6 +150,7 @@ public class PredictStrategyByRor extends BasicPredict {
             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;
@@ -155,28 +169,28 @@ public class PredictStrategyByRor extends BasicPredict {
      * @param launchs            启动次数分桶
      * @param ror                留存率分桶
      * @param ad_level           人群分层
-     * @param defaultProbability 默认概率(兜底值)
+     *
      * @return 广告展示概率阈值 [0, 1]
      */
-    private double getShowAdProbability(String launchs, String ror, String ad_level, Double defaultProbability) {
+    private Double getShowAdProbability(String launchs, String ror, String ad_level,String return30day) {
         // 任一特征为空,使用默认概率
-        if (StringUtils.isAnyBlank(launchs, ror, ad_level)) {
-            return defaultProbability;
+        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;
+            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 probability == null ? defaultProbability : Double.parseDouble(probability);
+            return StringUtils.isBlank(probability) ? null : Double.parseDouble(probability);
         } catch (Exception e) {
             // 解析失败(如非数字字符串)或 Redis 异常,记录错误并使用默认值
             log.error("getShowAdProbability error, launchs: {}, ror: {}, ad_level: {}, e = ", launchs, ror, ad_level, e);
-            return defaultProbability;
+            return null;
         }
     }
 
@@ -189,10 +203,10 @@ public class PredictStrategyByRor extends BasicPredict {
      * @param appType       应用类型
      * @return 默认概率,如果不匹配任何配置则返回 null
      */
-    private Double getDefaultProbability(String rootSessionId, String appType) {
+    private Boolean getDefaultProbability(String rootSessionId, String appType) {
         // 前置校验
         if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId) || appType == null) {
-            return null;
+            return false;
         }
 
         // 提取 rootSessionId 的最后一个字符作为尾号,用于流量分桶
@@ -202,14 +216,15 @@ public class PredictStrategyByRor 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 item.getConfig().get("default_probability");
+                    return true;
                 }
             }
 
         }
 
         // 未匹配到任何配置
-        return null;
+        return false;
     }
 
 }

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

@@ -0,0 +1,229 @@
+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 PredictStrategyByRorCopy 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.id.tail.copy.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    @Value("${experiment.ror.show.log.switch:1}")
+    private String experimentRorShowLogSwitch;
+
+    private static final String TABLE_NAME = "alg_mid_history_behavior_1month";
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+    public String name() {
+        return "launch_layer_ror_copy";
+    }
+
+    /**
+     * 核心预测方法:决定是否向用户展示广告
+     *
+     * @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 匹配配置)
+            Double defaultProbability = getDefaultProbability(rootSessionId, appType);
+
+            // 用户不在实验分桶内,跳过该策略
+            if (defaultProbability == null) {
+                return null;
+            }
+
+            Map<String, Object> rtnMap = new HashMap<>();
+
+            // 用户行为特征变量(来自离线特征表 alg_mid_history_behavior_1month)
+            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());
+            if ("1".equals(experimentRorShowLogSwitch)) {
+                log.info("[PredictStrategyByRor] 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 = 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,defaultProbability);
+
+            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("[PredictStrategyByRor] 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, Double defaultProbability) {
+        // 任一特征为空,使用默认概率
+        if (StringUtils.isAnyBlank(launchs, ror, ad_level,return30day)) {
+            return defaultProbability;
+        }
+        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_COPY.getPrefix(), keyId);
+
+            // 从 Redis 获取概率值
+            String probability = adRedisHelper.get(key);
+
+            // 解析概率值,如果为 null 则使用默认值
+            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 defaultProbability;
+        }
+    }
+
+    /**
+     * 获取默认概率阈值
+     * <p>
+     * 根据 rootSessionId 的最后一位字符(尾号)和 appType 匹配配置,用于流量分桶实验
+     *
+     * @param rootSessionId 根会话 ID
+     * @param appType       应用类型
+     * @return 默认概率,如果不匹配任何配置则返回 null
+     */
+    private Double getDefaultProbability(String rootSessionId, String appType) {
+        // 前置校验
+        if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId) || appType == null) {
+            return null;
+        }
+
+        // 提取 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 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;
+    }
+
+}

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

@@ -0,0 +1,98 @@
+package com.tzld.piaoquan.ad.engine.service.predict.v2;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+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.commons.util.JSONUtils;
+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.*;
+
+/**
+ * 基于 ROR的广告预测策略
+ * <p>
+ * 核心逻辑:
+ * 1. 根据用户的历史行为特征(启动次数launchs、留存率ror、人群分层ad_level)计算展示广告的概率阈值
+ * 2. 通过 mid 的 hash 值生成伪随机分数
+ * 3. 如果分数 <= 阈值,则展示广告;否则不展示
+ * <p>
+ * 用于控制不同用户群体的广告曝光频率,实现精细化运营
+ */
+@Slf4j
+@Service
+public class PredictStrategyChubuchuModi extends BasicPredict {
+    /**
+     * Apollo 动态配置:根据 rootSessionId 尾号和 appType 进行流量分桶
+     * <p>
+     * 配置格式示例:
+     * <pre>
+     * [
+     *   {
+     *     "appType": ["0", "3"],
+     *     "tail": ["0", "1", "2"]
+     *   }
+     * ]
+     * </pre>
+     */
+    @ApolloJsonValue("${experiment.chubuchu.modi.root.session.id.tail.config:[]}")
+    private List<RootSessionIdTailConfigItem> configItems;
+
+    /**
+     * 策略名称标识
+     */
+    @Override
+
+public String name() {
+        return "chubuchu_modi";
+    }
+
+
+    @Override
+    public Map<String, Object> predict(PredictContext ctx) {
+
+        try {
+            String rootSessionId = ctx.getRootSessionId();
+            if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId)) {
+                return Collections.emptyMap();
+            }
+            String appType = ctx.getAppType();
+            String tail = rootSessionId.substring(rootSessionId.length() - 1);
+
+            for (RootSessionIdTailConfigItem item : configItems) {
+                if (item.getAppType().contains(appType) && item.getTail().contains(tail)) {
+                    Map<String, Object> returnMap = new HashMap<>();
+                    double threshold = 0.5;
+                    // 取随机,各50%概率
+                    double score = this.calcScoreByMid(ctx.getMid());
+                    if (score < threshold) {
+                        returnMap.putAll(rtnAdPredict(ctx));
+                        returnMap.put("model", this.name());
+                    } else {
+                        returnMap.putAll(rtnNoAdPredict(ctx));
+                        returnMap.put("model", this.name());
+                        returnMap.put("no_ad_strategy", this.name());
+                    }
+                    returnMap.put("score", score);
+                    returnMap.put("threshold", threshold);
+
+                    return returnMap;
+                }
+            }
+
+            return Collections.emptyMap();
+        } catch (Exception e) {
+            log.error("[PredictStrategyChubuchuModi] predict error, ctx: {}", ctx, e);
+            return null;
+        }
+    }
+
+}

+ 11 - 1
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/remote/FeatureV2RemoteService.java

@@ -24,9 +24,19 @@ public class FeatureV2RemoteService {
 
     public Map<String, String> getFeature(List<FeatureKeyProto> protos) {
         if (CollectionUtils.isEmpty(protos)) {
+            log.debug("FeatureV2RemoteService.getFeature: protos is empty");
+            return Collections.emptyMap();
+        }
+        try {
+            long start = System.currentTimeMillis();
+            Map<String, String> result = client.multiGetFeature(protos);
+            log.debug("FeatureV2RemoteService.getFeature: protos.size={}, result.size={}, costTime={}",
+                    protos.size(), result != null ? result.size() : 0, System.currentTimeMillis() - start);
+            return result;
+        } catch (Exception e) {
+            log.error("FeatureV2RemoteService.getFeature: unexpected exception", e);
             return Collections.emptyMap();
         }
-        return client.multiGetFeature(protos);
     }
 
 }

+ 12 - 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);
     }
 
@@ -104,6 +110,12 @@ public class RankServiceImpl implements RankService {
                 return ServiceBeanFactory.getBean(RankStrategyBy680.class);
             case "683":
                 return ServiceBeanFactory.getBean(RankStrategyBy683.class);
+            case "833":
+                return ServiceBeanFactory.getBean(RankStrategyBy833.class);
+            case "834":
+                return ServiceBeanFactory.getBean(RankStrategyBy834.class);
+            case "840":
+                return ServiceBeanFactory.getBean(RankStrategyBy840.class);
             default:
                 return ServiceBeanFactory.getBean(RankStrategyByWeight.class);
         }

+ 1 - 8
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/scorer/PAIScorer.java

@@ -45,14 +45,7 @@ public class PAIScorer extends AbstractScorer {
             return rankItems;
         }
 
-        long startTime = System.currentTimeMillis();
-
-        List<AdRankItem> result = rankByJava(sceneFeatureMap, userFeatureMap, rankItems);
-
-        LOGGER.debug("ctr ranker time java items size={}, time={} ", result != null ? result.size() : 0,
-                System.currentTimeMillis() - startTime);
-
-        return result;
+        return rankByJava(sceneFeatureMap, userFeatureMap, rankItems);
     }
 
 //    private List<AdRankItem> rankByJava(final Map<String, String> sceneFeatureMap,

+ 1 - 1
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/FeaturePrinterStrategy.java

@@ -41,7 +41,7 @@ public class FeaturePrinterStrategy extends RankStrategyBasic {
         adRankItem.setCampaignId(dto.getCampaignId());
         adRankItem.setCpm(ObjUtil.nullOrDefault(dto.getCpm(), 90).doubleValue());
         adRankItem.setSkuId(dto.getSkuId());
-        if (noApiAdVerIds.contains(dto.getAdVerId())) {
+        if (noApiAdVerIds != null && noApiAdVerIds.contains(dto.getAdVerId())) {
             adRankItem.getExt().put("isApi", "0");
         } else {
             adRankItem.getExt().put("isApi", "1");

+ 8 - 2
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBasic.java

@@ -1,9 +1,7 @@
 package com.tzld.piaoquan.ad.engine.service.score.strategy;
 
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
-import com.alibaba.fastjson.TypeReference;
 import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
 import com.tzld.piaoquan.ad.engine.commons.dto.AdPlatformCreativeDTO;
 import com.tzld.piaoquan.ad.engine.commons.enums.CrowdLayerEnum;
@@ -184,6 +182,7 @@ public abstract class RankStrategyBasic implements RankStrategy {
 
     protected Feature getFeature(ScoreParam param, RankRecommendRequestParam request) {
         List<AdPlatformCreativeDTO> adIdList = request.getAdIdList();
+        log.info("getFeature adIdListSize:{}", adIdList.size());
         Feature finalFeature = null;
 
         // 分批处理 AdPlatformCreativeDTO 列表
@@ -915,6 +914,13 @@ public abstract class RankStrategyBasic implements RankStrategy {
 
     protected void putMetaFeature(AdRankItem adRankItem, Feature feature, Map<String, String> reqFeature,
                                   Map<String, String> sceneFeatureMap, RankRecommendRequestParam request) {
+        if (feature == null) {
+            log.warn("putMetaFeature: feature is null, skip processing. adVerId={}", adRankItem.getAdVerId());
+            adRankItem.getMetaFeatureMap().put("reqFeature", reqFeature);
+            adRankItem.getMetaFeatureMap().put("sceneFeature", sceneFeatureMap);
+            return;
+        }
+
         Map<String, Map<String, String>> userFeature = feature.getUserFeature();
         Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();
         Map<String, Map<String, Map<String, String>>> allAdVerFeature = feature.getAdVerFeature();

+ 4 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy679.java

@@ -71,6 +71,10 @@ public class RankStrategyBy679 extends RankStrategyBasic {
         // 特征处理
         // feature1
         Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
 
         Map<String, Map<String, String>> userFeature = feature.getUserFeature();
         Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();

+ 4 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy680.java

@@ -81,6 +81,10 @@ public class RankStrategyBy680 extends RankStrategyBasic {
         // 特征处理
         // feature1
         Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
 
         Map<String, Map<String, String>> userFeature = feature.getUserFeature();
         Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();

+ 4 - 1
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy683.java

@@ -92,7 +92,10 @@ public class RankStrategyBy683 extends RankStrategyBasic {
         // 特征处理
         // feature1
         Feature feature = this.getFeature(scoreParam, request);
-
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
 
         Map<String, Map<String, String>> userFeature = feature.getUserFeature();
         Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();

+ 4 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy687.java

@@ -67,6 +67,10 @@ public class RankStrategyBy687 extends RankStrategyBasic {
 
         // 获取所有创意对应的客户列表
         Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
 
         List<AdRankItem> rankItems = new ArrayList<>(recallCreativeList.size());
         for (AdPlatformCreativeDTO dto : recallCreativeList) {

+ 56 - 33
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy688.java

@@ -39,6 +39,12 @@ import static com.tzld.piaoquan.ad.engine.commons.math.Const.*;
 @Component
 public class RankStrategyBy688 extends RankStrategyBasic {
 
+    /**
+     * 空 Map 常量,避免频繁创建空 HashMap
+     */
+    private static final Map<String, String> EMPTY_STRING_MAP = Collections.emptyMap();
+    private static final Map<String, Map<String, String>> EMPTY_NESTED_MAP = Collections.emptyMap();
+
     private Map<String, double[]> bucketsMap = new HashMap<>();
 
     private Map<String, Double> bucketsLen = new HashMap<>();
@@ -92,7 +98,10 @@ public class RankStrategyBy688 extends RankStrategyBasic {
         // 特征处理
         // feature1
         Feature feature = this.getFeature(scoreParam, request);
-
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
 
         Map<String, Map<String, String>> userFeature = feature.getUserFeature();
         Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();
@@ -102,22 +111,22 @@ public class RankStrategyBy688 extends RankStrategyBasic {
         Map<String, String> reqFeature = this.getReqFeature(scoreParam, request);
 
         Map<String, String> userFeatureMap = new HashMap<>();
-        Map<String, String> c1Feature = userFeature.getOrDefault("alg_mid_feature_ad_action", new HashMap<>());
+        Map<String, String> c1Feature = userFeature.getOrDefault("alg_mid_feature_ad_action", EMPTY_STRING_MAP);
         List<TupleMapEntry<Tuple5>> midActionList = this.handleC1Feature(c1Feature, userFeatureMap);
 
         Map<String, Double> midTimeDiffMap = this.parseC1FeatureListToTimeDiffMap(midActionList, ts);
         Map<String, Double> actionStaticMap = this.parseC1FeatureListToActionStaticMap(midActionList);
 
-        Map<String, String> d2Feature = videoFeature.getOrDefault("alg_cid_feature_vid_cf_rank", new HashMap<>());
-        Map<String, String> d3Feature = videoFeature.getOrDefault("alg_vid_feature_basic_info", new HashMap<>());
+        Map<String, String> d2Feature = videoFeature.getOrDefault("alg_cid_feature_vid_cf_rank", EMPTY_STRING_MAP);
+        Map<String, String> d3Feature = videoFeature.getOrDefault("alg_vid_feature_basic_info", EMPTY_STRING_MAP);
 
         Map<String, Map<String, Double>> vidRankMaps = this.parseD2FeatureMap(d2Feature);
 
-        Map<String, String> e1Feature = userFeature.getOrDefault("alg_mid_feature_return_tags", new HashMap<>());
-        Map<String, String> e2Feature = userFeature.getOrDefault("alg_mid_feature_share_tags", new HashMap<>());
+        Map<String, String> e1Feature = userFeature.getOrDefault("alg_mid_feature_return_tags", EMPTY_STRING_MAP);
+        Map<String, String> e2Feature = userFeature.getOrDefault("alg_mid_feature_share_tags", EMPTY_STRING_MAP);
 
-        Map<String, String> g1Feature = userFeature.getOrDefault("mid_return_video_cate", new HashMap<>());
-        Map<String, String> g2Feature = userFeature.getOrDefault("mid_share_video_cate", new HashMap<>());
+        Map<String, String> g1Feature = userFeature.getOrDefault("mid_return_video_cate", EMPTY_STRING_MAP);
+        Map<String, String> g2Feature = userFeature.getOrDefault("mid_share_video_cate", EMPTY_STRING_MAP);
 
 
         userFeatureMap.put("brand", reqFeature.getOrDefault("brand", ""));
@@ -182,12 +191,12 @@ public class RankStrategyBy688 extends RankStrategyBasic {
                     setGuaranteeWeight(map, dto.getAdVerId(), adRankItem.getExt(), isGuaranteedFlow, reqFeature);
                     String cidStr = dto.getCreativeId().toString();
                     Map<String, String> cidFeatureMap = adRankItem.getFeatureMap();
-                    Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, new HashMap<>());
-                    Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", new HashMap<>());
+                    Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+                    Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
 
-                    Map<String, Map<String, String>> adVerFeature = allAdVerFeature.getOrDefault(dto.getAdVerId(), new HashMap<>());
-                    Map<String, Map<String, String>> skuFeature = allSkuFeature.getOrDefault(String.valueOf(dto.getSkuId()), new HashMap<>());
-                    Map<String, String> d1Feature = cidFeature.getOrDefault("alg_cid_feature_vid_cf", new HashMap<>());
+                    Map<String, Map<String, String>> adVerFeature = allAdVerFeature.getOrDefault(dto.getAdVerId(), EMPTY_NESTED_MAP);
+                    Map<String, Map<String, String>> skuFeature = allSkuFeature.getOrDefault(String.valueOf(dto.getSkuId()), EMPTY_NESTED_MAP);
+                    Map<String, String> d1Feature = cidFeature.getOrDefault("alg_cid_feature_vid_cf", EMPTY_STRING_MAP);
 
                     this.handleB1Feature(b1Feature, cidFeatureMap, cidStr);
                     this.handleB2ToB5AndB8ToB9Feature(cidFeature, adVerFeature, cidFeatureMap);
@@ -232,15 +241,17 @@ public class RankStrategyBy688 extends RankStrategyBasic {
 
         long time2 = System.currentTimeMillis();
         // feature3
+        // 请求级别的 tag 分词缓存,所有广告共享(同一用户的 tags 相同)
+        Map<String, List<String>> tagWordsCache = new ConcurrentHashMap<>();
         CountDownLatch cdl2 = new CountDownLatch(adRankItems.size() * 2);
         for (AdRankItem item : adRankItems) {
             String cidStr = String.valueOf(item.getAdId());
-            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, new HashMap<>());
-            Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", new HashMap<>());
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+            Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
             String title = b1Feature.getOrDefault("cidtitle", "");
             ThreadPoolFactory.defaultPool().submit(() -> {
                 try {
-                    this.handleE1AndE2Feature(e1Feature, e2Feature, title, item.getFeatureMap(), scoreParam);
+                    this.handleE1AndE2Feature(e1Feature, e2Feature, title, item.getFeatureMap(), scoreParam, tagWordsCache);
                 } finally {
                     cdl2.countDown();
                 }
@@ -296,13 +307,17 @@ public class RankStrategyBy688 extends RankStrategyBasic {
             double calibratedScore = originalScore / (originalScore + (1 - originalScore) / negSampleRate);
             // 该创意尚未在模型中训练,打分不可靠
             if (CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSet()) && !DnnCidDataHelper.getCidSet().contains(item.getAdId())) {
-                Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), new HashMap<>());
-                Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", new HashMap<>());
+                Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+                Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
                 double view = Double.parseDouble(b3Feature.getOrDefault("ad_view_14d", "0"));
                 double conver = Double.parseDouble(b3Feature.getOrDefault("ad_conversion_14d", "0"));
                 double smoothCxr = NumUtil.divSmoothV1(conver, view, 1.64);
                 //模型打分和统计计算取打分更低的
-                calibratedScore = Math.min(smoothCxr, calibratedScore);
+                item.getScoreMap().put("cvcvrItemValue", 1.0);
+                if(smoothCxr <= calibratedScore){
+                    calibratedScore = smoothCxr;
+                    item.getScoreMap().put("cvcvrItemValue", 2.0);
+                }
             }
             item.setLrScore(calibratedScore);
             item.getScoreMap().put("originCtcvrScore", originalScore);
@@ -437,12 +452,12 @@ public class RankStrategyBy688 extends RankStrategyBasic {
     }
 
     private void handleB2ToB5AndB8ToB9Feature(Map<String, Map<String, String>> c1Feature, Map<String, Map<String, String>> adVerFeature, Map<String, String> cidFeatureMap) {
-        Map<String, String> b2Feature = adVerFeature.getOrDefault("alg_cid_feature_adver_action", new HashMap<>());
-        Map<String, String> b3Feature = c1Feature.getOrDefault("alg_cid_feature_cid_action", new HashMap<>());
-        Map<String, String> b4Feature = c1Feature.getOrDefault("alg_cid_feature_region_action", new HashMap<>());
-        Map<String, String> b5Feature = c1Feature.getOrDefault("alg_cid_feature_app_action", new HashMap<>());
-        Map<String, String> b8Feature = c1Feature.getOrDefault("alg_cid_feature_brand_action", new HashMap<>());
-        Map<String, String> b9Feature = c1Feature.getOrDefault("alg_cid_feature_weChatVersion_action", new HashMap<>());
+        Map<String, String> b2Feature = adVerFeature.getOrDefault("alg_cid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> b3Feature = c1Feature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+        Map<String, String> b4Feature = c1Feature.getOrDefault("alg_cid_feature_region_action", EMPTY_STRING_MAP);
+        Map<String, String> b5Feature = c1Feature.getOrDefault("alg_cid_feature_app_action", EMPTY_STRING_MAP);
+        Map<String, String> b8Feature = c1Feature.getOrDefault("alg_cid_feature_brand_action", EMPTY_STRING_MAP);
+        Map<String, String> b9Feature = c1Feature.getOrDefault("alg_cid_feature_weChatVersion_action", EMPTY_STRING_MAP);
 
         List<String> timeList = Arrays.asList("1h", "2h", "3h", "6h", "12h", "1d", "3d", "7d", "yesterday", "today");
         List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
@@ -480,8 +495,8 @@ public class RankStrategyBy688 extends RankStrategyBasic {
     }
 
     private void handleB6ToB7Feature(Map<String, Map<String, String>> c1Feature, Map<String, String> cidFeatureMap) {
-        Map<String, String> b6Feature = c1Feature.getOrDefault("alg_cid_feature_week_action", new HashMap<>());
-        Map<String, String> b7Feature = c1Feature.getOrDefault("alg_cid_feature_hour_action", new HashMap<>());
+        Map<String, String> b6Feature = c1Feature.getOrDefault("alg_cid_feature_week_action", EMPTY_STRING_MAP);
+        Map<String, String> b7Feature = c1Feature.getOrDefault("alg_cid_feature_hour_action", EMPTY_STRING_MAP);
 
         List<String> timeList = Arrays.asList("7d", "14d");
         List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
@@ -652,8 +667,8 @@ public class RankStrategyBy688 extends RankStrategyBasic {
     private void handleH1AndH2Feature(Map<String, Map<String, String>> skuFeature,
                                       Map<String, Map<String, String>> adVerFeature,
                                       Map<String, String> cidFeatureMap) {
-        Map<String, String> h1Feature = adVerFeature.getOrDefault("alg_mid_feature_adver_action", new HashMap<>());
-        Map<String, String> h2Feature = skuFeature.getOrDefault("alg_mid_feature_sku_action", new HashMap<>());
+        Map<String, String> h1Feature = adVerFeature.getOrDefault("alg_mid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> h2Feature = skuFeature.getOrDefault("alg_mid_feature_sku_action", EMPTY_STRING_MAP);
         List<String> timeList = Arrays.asList("3d", "7d", "30d");
         List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
                 new Tuple2<>(h1Feature, "adverid"),
@@ -692,11 +707,18 @@ public class RankStrategyBy688 extends RankStrategyBasic {
     }
 
     private void handleE1AndE2Feature(Map<String, String> e1Feature, Map<String, String> e2Feature, String title,
-                                      Map<String, String> featureMap, ScoreParam scoreParam) {
+                                      Map<String, String> featureMap, ScoreParam scoreParam,
+                                      Map<String, List<String>> tagWordsCache) {
         if (StringUtils.isEmpty(title)) {
             return;
         }
 
+        // 预先分词 title,在整个方法中复用,避免重复分词
+        List<String> titleWords = null;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            titleWords = SimilarityUtils.segment(title);
+        }
+
         List<Tuple2<Map<String, String>, String>> tuple2List = Arrays.asList(new Tuple2<>(e1Feature, "e1"), new Tuple2<>(e2Feature, "e2"));
 
         List<String> tagsFieldList = Arrays.asList("tags_3d", "tags_7d", "tags_14d");
@@ -710,10 +732,10 @@ public class RankStrategyBy688 extends RankStrategyBasic {
             for (String tagsField : tagsFieldList) {
                 if (StringUtils.isNotEmpty(feature.get(tagsField))) {
                     String tags = feature.get(tagsField);
-                    // Double[] doubles = ExtractorUtils.funcC34567ForTags(tags, title);
                     Double[] doubles;
                     if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
-                        doubles = ExtractorUtils.funcC34567ForTagsNew(tags, title);
+                        // 使用缓存的 title 分词结果和请求级别的 tag 分词缓存
+                        doubles = ExtractorUtils.funcC34567ForTagsNewWithCache(tags, title, titleWords, tagWordsCache);
                     } else {
                         doubles = ExtractorUtils.funcC34567ForTags(tags, title);
                     }
@@ -877,7 +899,8 @@ public class RankStrategyBy688 extends RankStrategyBasic {
     }
 
     private Map<String, String> featureBucket(Map<String, String> featureMap) {
-        Map<String, String> newFeatureMap = new ConcurrentHashMap<>(featureMap.size());
+        // 使用 HashMap 替代 ConcurrentHashMap,分桶操作是单线程的
+        Map<String, String> newFeatureMap = new HashMap<>(featureMap.size());
         for (Map.Entry<String, String> entry : featureMap.entrySet()) {
             try {
                 String name = entry.getKey();

+ 985 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy833.java

@@ -0,0 +1,985 @@
+package com.tzld.piaoquan.ad.engine.service.score.strategy;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.piaoquan.ad.engine.commons.dto.AdPlatformCreativeDTO;
+import com.tzld.piaoquan.ad.engine.commons.helper.DnnCidDataHelper;
+import com.tzld.piaoquan.ad.engine.commons.param.RankRecommendRequestParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScoreParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScorerUtils;
+import com.tzld.piaoquan.ad.engine.commons.thread.ThreadPoolFactory;
+import com.tzld.piaoquan.ad.engine.commons.util.*;
+import com.tzld.piaoquan.ad.engine.service.entity.CorrectCpaParam;
+import com.tzld.piaoquan.ad.engine.service.entity.GuaranteeView;
+import com.tzld.piaoquan.ad.engine.service.feature.Feature;
+import com.tzld.piaoquan.recommend.feature.domain.ad.base.AdRankItem;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang.math.NumberUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.xm.Similarity;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.tzld.piaoquan.ad.engine.commons.math.Const.*;
+
+@Slf4j
+@Component
+public class RankStrategyBy833 extends RankStrategyBasic {
+
+    /**
+     * 空 Map 常量,避免频繁创建空 HashMap
+     */
+    private static final Map<String, String> EMPTY_STRING_MAP = Collections.emptyMap();
+    private static final Map<String, Map<String, String>> EMPTY_NESTED_MAP = Collections.emptyMap();
+
+    private Map<String, double[]> bucketsMap = new HashMap<>();
+
+    private Map<String, Double> bucketsLen = new HashMap<>();
+
+    @Value("${word2vec.exp:694}")
+    private String word2vecExp;
+
+    @ApolloJsonValue("${rank.score.params.833:{}}")
+    private Map<String, String> paramsMap;
+
+    // FIXME(zhoutian): 可能需要独立配置
+    @ApolloJsonValue("${rank.score.weight.680:{}}")
+    private Map<String, Double> weightMap;
+
+    /**
+     * 人群分层&创意的权重
+     * 格式:{layer_creativeId: weight}
+     */
+    @ApolloJsonValue("${rank.score.weight.layer.and.creative:{}}")
+    private Map<String, Double> layerAndCreativeWeightMap;
+
+    @ApolloJsonValue("${rank.score.neg_sample_rate:0.01}")
+    Double negSampleRate;
+
+    Set<String> sparseFeatureSet;
+
+
+    @PostConstruct
+    public void afterInit() {
+        this.readBucketFile();
+        this.initSparseFeatureNames();
+    }
+
+
+    @Override
+    public List<AdRankItem> adItemRank(RankRecommendRequestParam request, ScoreParam scoreParam) {
+        Map<String, Double> weightParam = ObjUtil.nullOrDefault(weightMap, new HashMap<>());
+
+
+        Map<Long, Double> creativeScoreCoefficient = getCreativeScoreCoefficient();
+        Set<String> noApiAdVerIds = getNoApiAdVerIds();
+
+        long ts = System.currentTimeMillis() / 1000;
+
+        String brand = scoreParam.getRequestContext().getMachineinfoBrand();
+        if (StringUtils.isNotEmpty(brand)) {
+            scoreParam.getRequestContext().setMachineinfoBrand(brand + "-n");
+        }
+
+        long start = System.currentTimeMillis();
+        //过滤创意
+        filterRequestAdList(request, scoreParam);
+        // 特征处理
+        // feature1
+        Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
+
+        Map<String, Map<String, String>> userFeature = feature.getUserFeature();
+        Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();
+        Map<String, Map<String, Map<String, String>>> allAdVerFeature = feature.getAdVerFeature();
+        Map<String, Map<String, Map<String, String>>> allCidFeature = feature.getCidFeature();
+        Map<String, Map<String, Map<String, String>>> allSkuFeature = feature.getSkuFeature();
+        Map<String, String> reqFeature = this.getReqFeature(scoreParam, request);
+
+        Map<String, String> userFeatureMap = new HashMap<>();
+        Map<String, String> c1Feature = userFeature.getOrDefault("alg_mid_feature_ad_action", EMPTY_STRING_MAP);
+        List<TupleMapEntry<Tuple5>> midActionList = this.handleC1Feature(c1Feature, userFeatureMap);
+
+        Map<String, Double> midTimeDiffMap = this.parseC1FeatureListToTimeDiffMap(midActionList, ts);
+        Map<String, Double> actionStaticMap = this.parseC1FeatureListToActionStaticMap(midActionList);
+
+        Map<String, String> d2Feature = videoFeature.getOrDefault("alg_cid_feature_vid_cf_rank", EMPTY_STRING_MAP);
+        Map<String, String> d3Feature = videoFeature.getOrDefault("alg_vid_feature_basic_info", EMPTY_STRING_MAP);
+
+        Map<String, Map<String, Double>> vidRankMaps = this.parseD2FeatureMap(d2Feature);
+
+        Map<String, String> e1Feature = userFeature.getOrDefault("alg_mid_feature_return_tags", EMPTY_STRING_MAP);
+        Map<String, String> e2Feature = userFeature.getOrDefault("alg_mid_feature_share_tags", EMPTY_STRING_MAP);
+
+        Map<String, String> g1Feature = userFeature.getOrDefault("mid_return_video_cate", EMPTY_STRING_MAP);
+        Map<String, String> g2Feature = userFeature.getOrDefault("mid_share_video_cate", EMPTY_STRING_MAP);
+
+
+        userFeatureMap.put("brand", reqFeature.getOrDefault("brand", ""));
+        userFeatureMap.put("region", reqFeature.getOrDefault("region", ""));
+        userFeatureMap.put("city", reqFeature.getOrDefault("city", ""));
+        userFeatureMap.put("vid", reqFeature.getOrDefault("vid", ""));
+        userFeatureMap.put("apptype", reqFeature.getOrDefault("apptype", ""));
+        userFeatureMap.put("is_first_layer", reqFeature.getOrDefault("is_first_layer", ""));
+        userFeatureMap.put("root_source_scene", reqFeature.getOrDefault("root_source_scene", ""));
+        userFeatureMap.put("root_source_channel", reqFeature.getOrDefault("root_source_channel", ""));
+
+
+        userFeatureMap.put("cate1", d3Feature.get("merge_first_level_cate"));
+        userFeatureMap.put("cate2", d3Feature.get("merge_second_level_cate"));
+        userFeatureMap.put("user_vid_return_tags_2h", e1Feature.getOrDefault("tags_2h", null));
+        userFeatureMap.put("user_vid_return_tags_1d", e1Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_return_tags_3d", e1Feature.getOrDefault("tags_3d", null));
+        userFeatureMap.put("user_vid_return_tags_7d", e1Feature.getOrDefault("tags_7d", null));
+        userFeatureMap.put("user_vid_return_tags_14d", e1Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("title_split", d3Feature.getOrDefault("title_split", null));
+        userFeatureMap.put("user_vid_share_tags_1d", e2Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_share_tags_14d", e2Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("user_vid_return_cate1_14d", g1Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_return_cate2_14d", g1Feature.getOrDefault("cate2_14d", null));
+        userFeatureMap.put("user_vid_share_cate1_14d", g2Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_share_cate2_14d", g2Feature.getOrDefault("cate2_14d", null));
+
+        Map<String, String> sceneFeatureMap = this.handleSceneFeature(ts);
+        long time1 = System.currentTimeMillis();
+
+        boolean isGuaranteedFlow = getIsGuaranteedFlow(scoreParam);
+        Map<String, GuaranteeView> map = getGuaranteeViewMap(request, isGuaranteedFlow);
+        Map<Long, CorrectCpaParam> correctCpaMap = getCorrectCpaParamMap(request, scoreParam, reqFeature);
+        List<AdRankItem> adRankItems = new ArrayList<>();
+        Random random = new Random();
+        List<Future<AdRankItem>> futures = new ArrayList<>();
+        CountDownLatch cdl1 = new CountDownLatch(request.getAdIdList().size());
+        for (AdPlatformCreativeDTO dto : request.getAdIdList()) {
+            Future<AdRankItem> future = ThreadPoolFactory.feature().submit(() -> {
+                AdRankItem adRankItem = new AdRankItem();
+                try {
+                    adRankItem.setAdId(dto.getCreativeId());
+                    adRankItem.setCreativeCode(dto.getCreativeCode());
+                    adRankItem.setAdVerId(dto.getAdVerId());
+                    adRankItem.setVideoId(request.getVideoId());
+                    adRankItem.setCpa(dto.getCpa());
+                    adRankItem.setId(dto.getAdId());
+                    adRankItem.setCampaignId(dto.getCampaignId());
+                    adRankItem.setCpm(ObjUtil.nullOrDefault(dto.getCpm(), 90).doubleValue());
+                    adRankItem.setSkuId(dto.getSkuId());
+                    adRankItem.setCustomerId(dto.getCustomerId());
+                    adRankItem.setProfession(dto.getProfession());
+                    adRankItem.setRandom(random.nextInt(1000));
+                    if (noApiAdVerIds.contains(dto.getAdVerId())) {
+                        adRankItem.getExt().put("isApi", "0");
+                    } else {
+                        adRankItem.getExt().put("isApi", "1");
+                    }
+                    adRankItem.getExt().put("recallsources", dto.getRecallSources());
+                    adRankItem.getExt().put("correctCpaMap", JSONObject.toJSONString(correctCpaMap.get(dto.getAdId())));
+                    adRankItem.getExt().put("correctionFactor", correctCpaMap.get(dto.getAdId()).getCorrectionFactor());
+                    setGuaranteeWeight(map, dto.getAdVerId(), adRankItem.getExt(), isGuaranteedFlow, reqFeature);
+                    String cidStr = dto.getCreativeId().toString();
+                    Map<String, String> cidFeatureMap = adRankItem.getFeatureMap();
+                    Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+                    Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+
+                    Map<String, Map<String, String>> adVerFeature = allAdVerFeature.getOrDefault(dto.getAdVerId(), EMPTY_NESTED_MAP);
+                    Map<String, Map<String, String>> skuFeature = allSkuFeature.getOrDefault(String.valueOf(dto.getSkuId()), EMPTY_NESTED_MAP);
+                    Map<String, String> d1Feature = cidFeature.getOrDefault("alg_cid_feature_vid_cf", EMPTY_STRING_MAP);
+
+                    this.handleB1Feature(b1Feature, cidFeatureMap, cidStr);
+                    this.handleB2ToB5AndB8ToB9Feature(cidFeature, adVerFeature, cidFeatureMap);
+                    this.handleB6ToB7Feature(cidFeature, cidFeatureMap);
+                    this.handleC1UIFeature(midTimeDiffMap, actionStaticMap, cidFeatureMap, cidStr);
+                    this.handleD1Feature(d1Feature, cidFeatureMap);
+                    this.handleD2Feature(vidRankMaps, cidFeatureMap, cidStr);
+                    this.handleH1AndH2Feature(skuFeature, adVerFeature, cidFeatureMap);
+                    cidFeatureMap.put("cid", dto.getCreativeId() != null ? String.valueOf(dto.getCreativeId()) : "");
+                    cidFeatureMap.put("adid", dto.getAdId() != null ? String.valueOf(dto.getAdId()) : "");
+                    cidFeatureMap.put("adverid", dto.getAdVerId() != null ? dto.getAdVerId() : "");
+                    cidFeatureMap.put("profession", dto.getProfession() != null ? dto.getProfession() : "");
+                    cidFeatureMap.put("category_name", dto.getCategoryName() != null ? dto.getCategoryName() : "");
+                    cidFeatureMap.put("material_md5", dto.getMaterialMd5() != null ? dto.getMaterialMd5() : "");
+                    //DNN模型没训练过的cid才不传入广告相关的稀疏特征
+                    if (CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSetV2()) && !DnnCidDataHelper.getCidSetV2().contains(adRankItem.getAdId())) {
+                        cidFeatureMap.put("cid", "");
+                        cidFeatureMap.put("adid", "");
+                        cidFeatureMap.put("adverid", "");
+                    }
+                    return adRankItem;
+                } finally {
+                    cdl1.countDown();
+                }
+            });
+            futures.add(future);
+        }
+        try {
+            cdl1.await(300, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        for (Future<AdRankItem> future : futures) {
+            try {
+                if (future.isDone()) {
+                    adRankItems.add(future.get());
+                }
+            } catch (Exception e) {
+                log.error("Feature handle error", e);
+            }
+        }
+
+        long time2 = System.currentTimeMillis();
+        // feature3
+        // 请求级别的 tag 分词缓存,所有广告共享(同一用户的 tags 相同)
+        Map<String, List<String>> tagWordsCache = new ConcurrentHashMap<>();
+        CountDownLatch cdl2 = new CountDownLatch(adRankItems.size() * 2);
+        for (AdRankItem item : adRankItems) {
+            String cidStr = String.valueOf(item.getAdId());
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+            Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+            String title = b1Feature.getOrDefault("cidtitle", "");
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleE1AndE2Feature(e1Feature, e2Feature, title, item.getFeatureMap(), scoreParam, tagWordsCache);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleD3AndB1Feature(d3Feature, title, item.getFeatureMap(), scoreParam);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+        }
+        try {
+            cdl2.await(150, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+
+        long time3 = System.currentTimeMillis();
+        // 分桶
+        userFeatureMap = this.featureBucket(userFeatureMap);
+        CountDownLatch cdl4 = new CountDownLatch(adRankItems.size());
+        for (AdRankItem adRankItem : adRankItems) {
+            ThreadPoolFactory.feature().submit(() -> {
+                try {
+                    Map<String, String> featureMap = adRankItem.getFeatureMap();
+                    adRankItem.setFeatureMap(this.featureBucket(featureMap));
+                } finally {
+                    cdl4.countDown();
+                }
+            });
+        }
+        try {
+            cdl4.await(100, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        long time4 = System.currentTimeMillis();
+        // 打分排序
+        // getScorerPipeline
+
+        if (CollectionUtils.isEmpty(adRankItems)) {
+            log.error("adRankItems is empty");
+        }
+        List<AdRankItem> result = ScorerUtils.getScorerPipeline(ScorerUtils.PAI_SCORE_CONF_20250804).scoring(sceneFeatureMap, userFeatureMap, adRankItems);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("scoring result is empty");
+        }
+        long time5 = System.currentTimeMillis();
+        int viewLimit = NumberUtils.toInt(paramsMap.getOrDefault("viewLimit", "3000"));
+        // calibrate score for negative sampling or cold start
+        for (AdRankItem item : result) {
+            double originalScore = item.getLrScore();
+            double calibratedScore = originalScore / (originalScore + (1 - originalScore) / negSampleRate);
+            // 该创意尚未在模型中训练,打分不可靠
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+            Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+            double view3Day = Double.parseDouble(b3Feature.getOrDefault("ad_view_3d", "0"));
+            if ((CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSetV2()) && !DnnCidDataHelper.getCidSetV2().contains(item.getAdId()))
+                    || view3Day <= viewLimit) {
+                double view = Double.parseDouble(b3Feature.getOrDefault("ad_view_14d", "0"));
+                double conver = Double.parseDouble(b3Feature.getOrDefault("ad_conversion_14d", "0"));
+                double smoothCxr = NumUtil.divSmoothV1(conver, view, 1.64);
+                //模型打分和统计计算取打分更低的
+                item.getScoreMap().put("cvcvrItemValue", 1.0);
+                if (smoothCxr <= calibratedScore) {
+                    calibratedScore = smoothCxr;
+                    item.getScoreMap().put("cvcvrItemValue", 2.0);
+                }
+            }
+            item.setLrScore(calibratedScore);
+            item.getScoreMap().put("originCtcvrScore", originalScore);
+            item.getScoreMap().put("modelCtcvrScore", calibratedScore);
+            item.getScoreMap().put("ctcvrScore", calibratedScore);
+        }
+
+        String calibModelName = paramsMap.getOrDefault("calibModelName", "dnnV3");
+        calculateCtcvrScore(result, request, scoreParam, calibModelName, reqFeature);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("calculateCtcvrScore result is empty");
+        }
+        // loop
+        double cpmCoefficient = weightParam.getOrDefault("cpmCoefficient", 0.9);
+        boolean isGuaranteeType = false;
+        // 查询人群分层信息
+        String peopleLayer = Optional.of(reqFeature)
+                .map(f -> f.get("layer"))
+                .map(s -> s.replace("-炸", ""))
+                .orElse(null);
+
+        // 控制曝光参数
+        String expOldKey = paramsMap.getOrDefault("expOldKey", "ad_view_yesterday");
+        double expOldThreshold = NumberUtils.toDouble(paramsMap.getOrDefault("expOldThreshold", "1000"));
+        String expNewKey = paramsMap.getOrDefault("expNewKey", "ad_view_today");
+        double expNewThreshold = NumberUtils.toDouble(paramsMap.getOrDefault("expNewThreshold", "3000"));
+        double expLowerWeight = NumberUtils.toDouble(paramsMap.getOrDefault("expLowerWeight", "0.2"));
+        double expUpperWeight = NumberUtils.toDouble(paramsMap.getOrDefault("expUpperWeight", "1.0"));
+        double expScale = NumberUtils.toDouble(paramsMap.getOrDefault("expScale", "10.0"));
+        for (AdRankItem item : result) {
+            double bid = item.getCpa();
+            if (scoreParam.getExpCodeSet().contains(correctCpaExp1) || scoreParam.getExpCodeSet().contains(correctCpaExp2)) {
+                Double correctionFactor = (Double) item.getExt().get("correctionFactor");
+                item.getScoreMap().put("correctionFactor", correctionFactor);
+                bid = bid * correctionFactor;
+            }
+            item.getScoreMap().put("ecpm", item.getLrScore() * bid * 1000);
+            if (isGuaranteedFlow && item.getExt().get("isGuaranteed") != null && (boolean) item.getExt().get("isGuaranteed")) {
+                isGuaranteeType = true;
+            }
+
+            // 控制曝光权重
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+            Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+            double expWeight = getExpWeight(b3Feature,
+                    expOldKey, expOldThreshold,
+                    expNewKey, expNewThreshold,
+                    expLowerWeight, expUpperWeight, expScale);
+
+            String layerAndCreativeWeightMapKey = getLayerAndCreativeWeightMapKey(peopleLayer, String.valueOf(item.getAdId()));
+            // 人群分层&创意的权重
+            double layerAndCreativeWeight = getLayerAndCreativeWeight(layerAndCreativeWeightMapKey);
+            double scoreCoefficient = creativeScoreCoefficient.getOrDefault(item.getAdId(), 1d);
+            double guaranteeScoreCoefficient = getGuaranteeScoreCoefficient(isGuaranteedFlow, item.getExt());
+            double score = expWeight * item.getLrScore() * bid * scoreCoefficient * guaranteeScoreCoefficient * layerAndCreativeWeight;
+            item.getScoreMap().put("guaranteeScoreCoefficient", guaranteeScoreCoefficient);
+            item.getScoreMap().put("cpa", item.getCpa());
+            item.getScoreMap().put("cpm", item.getCpm());
+            item.getScoreMap().put("bid", bid);
+            item.getScoreMap().put("cpmCoefficient", cpmCoefficient);
+            item.getScoreMap().put("scoreCoefficient", scoreCoefficient);
+            item.getFeatureMap().putAll(userFeatureMap);
+            item.getFeatureMap().putAll(sceneFeatureMap);
+
+            // 没有转化回传的广告主,使用后台配置的CPM
+            if (noApiAdVerIds.contains(item.getAdVerId())) {
+                score = item.getCpm() * cpmCoefficient / 1000;
+            }
+            item.setScore(score);
+        }
+
+
+        result.sort(ComparatorUtil.equalsRandomComparator());
+
+        String logModelName = paramsMap.getOrDefault("logModelName", "dnnV3");
+        if (CollectionUtils.isNotEmpty(result)) {
+            AdRankItem top1Item = result.get(0);
+            List<String> participateCompetitionType = new ArrayList<>();
+            participateCompetitionType.add("engine");
+            top1Item.getExt().put("isGuaranteeType", isGuaranteeType);
+            if (isGuaranteeType) {
+                participateCompetitionType.add("guarantee");
+            }
+            top1Item.getExt().put("participateCompetitionType", StringUtils.join(participateCompetitionType, ","));
+            Double modelCtcvrScore = top1Item.getScoreMap().get("modelCtcvrScore");
+            Double ctcvrScore = top1Item.getScoreMap().get("ctcvrScore");
+            if (scoreParam.getExpCodeSet().contains(checkoutEcpmExp)) {
+                top1Item.getExt().put("ecpm", ctcvrScore * top1Item.getCpa() * 1000);
+                String filterEcpmValue = paramsMap.getOrDefault("filterEcpm", filterEcpm);
+                top1Item.getExt().put("filterEcpm", filterEcpmValue);
+            } else {
+                top1Item.getExt().put("ecpm", modelCtcvrScore * top1Item.getCpa() * 1000);
+            }
+            putMetaFeature(top1Item, feature, reqFeature, sceneFeatureMap, request);
+            top1Item.getExt().put("model", logModelName);
+            String coefficientRate = paramsMap.getOrDefault("coefficientRate", "1");
+            top1Item.getExt().put("coefficientRate", coefficientRate);
+        }
+        long time6 = System.currentTimeMillis();
+        log.info("cost={}, getFeature={}, handleFeature={},  similar={}, bucketFeature={}, getScorerPipeline={}, " +
+                        "other={}, adIdSize={}, adRankItemsSize={}",
+                time6 - start, time1 - start, time2 - time1, time3 - time2, time4 - time3,
+                time5 - time4, time6 - time5, request.getAdIdList().size(), adRankItems.size());
+
+        return result;
+    }
+
+    /**
+     * 获取人群分层和创意的权重
+     *
+     * @param key
+     * @return
+     */
+    private Double getLayerAndCreativeWeight(String key) {
+        if (StringUtils.isBlank(key)) {
+            return 1d;
+        }
+        return layerAndCreativeWeightMap.getOrDefault(key, 1d);
+    }
+
+    /**
+     * 获取人群分层和创意的权重key
+     *
+     * @param layer
+     * @param creativeId
+     * @return
+     */
+    private String getLayerAndCreativeWeightMapKey(String layer, String creativeId) {
+        if (StringUtils.isBlank(layer) || StringUtils.isBlank(creativeId)) {
+            return null;
+        }
+        return layer + "_" + creativeId;
+    }
+
+
+    private void handleB1Feature(Map<String, String> b1Feature, Map<String, String> cidFeatureMap, String cid) {
+        cidFeatureMap.put("cid_" + cid, "0.1");
+        // if (StringUtils.isNotBlank(b1Feature.get("adid"))) {
+        //     String adId = b1Feature.get("adid");
+        //     cidFeatureMap.put("adid_" + adId, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("adverid"))) {
+            String adVerId = b1Feature.get("adverid");
+            cidFeatureMap.put("adverid_" + adVerId, "0.1");
+        }
+        // if (StringUtils.isNotBlank(b1Feature.get("targeting_conversion"))) {
+        //     String targetingConversion = b1Feature.get("targeting_conversion");
+        //     cidFeatureMap.put("targeting_conversion_" + targetingConversion, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("cpa"))) {
+            String cpa = b1Feature.get("cpa");
+            cidFeatureMap.put("cpa", cpa);
+        }
+    }
+
+    private void handleB2ToB5AndB8ToB9Feature(Map<String, Map<String, String>> c1Feature, Map<String, Map<String, String>> adVerFeature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b2Feature = adVerFeature.getOrDefault("alg_cid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> b3Feature = c1Feature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+        Map<String, String> b4Feature = c1Feature.getOrDefault("alg_cid_feature_region_action", EMPTY_STRING_MAP);
+        Map<String, String> b5Feature = c1Feature.getOrDefault("alg_cid_feature_app_action", EMPTY_STRING_MAP);
+        Map<String, String> b8Feature = c1Feature.getOrDefault("alg_cid_feature_brand_action", EMPTY_STRING_MAP);
+        Map<String, String> b9Feature = c1Feature.getOrDefault("alg_cid_feature_weChatVersion_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("1h", "2h", "3h", "6h", "12h", "1d", "3d", "7d", "yesterday", "today");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b2Feature, "b2"),
+                new Tuple2<>(b3Feature, "b3"),
+                new Tuple2<>(b4Feature, "b4"),
+                new Tuple2<>(b5Feature, "b5"),
+                new Tuple2<>(b8Feature, "b8"),
+                new Tuple2<>(b9Feature, "b9")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private void handleB6ToB7Feature(Map<String, Map<String, String>> c1Feature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b6Feature = c1Feature.getOrDefault("alg_cid_feature_week_action", EMPTY_STRING_MAP);
+        Map<String, String> b7Feature = c1Feature.getOrDefault("alg_cid_feature_hour_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("7d", "14d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b6Feature, "b6"),
+                new Tuple2<>(b7Feature, "b7")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private List<TupleMapEntry<Tuple5>> handleC1Feature(Map<String, String> c1Feature, Map<String, String> featureMap) {
+
+        //用户近1年内是否有转化
+        if (c1Feature.containsKey("user_has_conver_1y") && c1Feature.get("user_has_conver_1y") != null) {
+            featureMap.put("user_has_conver_1y", c1Feature.get("user_has_conver_1y"));
+        }
+        //用户历史转化过品类
+        if (c1Feature.containsKey("user_conver_ad_class") && c1Feature.get("user_conver_ad_class") != null) {
+            featureMap.put("user_conver_ad_class", c1Feature.get("user_conver_ad_class"));
+        }
+
+        // 用户特征
+        List<TupleMapEntry<Tuple5>> midActionList = new ArrayList<>();
+        if (c1Feature.containsKey("action")) {
+            String action = c1Feature.get("action");
+            midActionList = Arrays.stream(action.split(","))
+                    .map(r -> {
+                        String[] rList = r.split(":");
+                        Tuple5 tuple5 = new Tuple5(rList[1], rList[2], rList[3], rList[4], rList[5]);
+                        return new TupleMapEntry<>(rList[0], tuple5);
+                    })
+                    // TODO 倒排
+                    .sorted((a, b) -> Integer.compare(Integer.parseInt(b.value.f1), Integer.parseInt(a.value.f1)))
+                    .collect(Collectors.toList());
+        }
+
+        double viewAll = midActionList.size();
+        double clickAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f2)).sum();
+        double converAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f3)).sum();
+        double incomeAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f4)).sum();
+        featureMap.put("viewAll", String.valueOf(viewAll));
+        featureMap.put("clickAll", String.valueOf(clickAll));
+        featureMap.put("converAll", String.valueOf(converAll));
+        featureMap.put("incomeAll", String.valueOf(incomeAll));
+        featureMap.put("ctr_all", String.valueOf(NumUtil.div(clickAll, viewAll)));
+        featureMap.put("ctcvr_all", String.valueOf(NumUtil.div(converAll, viewAll)));
+        featureMap.put("cvr_all", String.valueOf(NumUtil.div(clickAll, converAll)));
+        featureMap.put("ecpm_all", String.valueOf(NumUtil.div(incomeAll * 1000, viewAll)));
+        if (CollectionUtils.isNotEmpty(midActionList)) {
+            List<String> cidClickList = new ArrayList<>();
+            List<String> cidConverList = new ArrayList<>();
+            for (TupleMapEntry<Tuple5> tupleMapEntry : midActionList) {
+                String cid = tupleMapEntry.key;
+                String click = tupleMapEntry.value.f2;
+                String conver = tupleMapEntry.value.f3;
+                if (Objects.equals(click, "1")) {
+                    cidClickList.add(cid);
+                }
+                if (Objects.equals(conver, "1")) {
+                    cidConverList.add(cid);
+                }
+            }
+            featureMap.put("user_cid_click_list", String.join(",", cidClickList));
+            featureMap.put("user_cid_conver_list", String.join(",", cidConverList));
+        }
+        return midActionList;
+    }
+
+    private void handleC1UIFeature(Map<String, Double> midTimeDiffMap, Map<String, Double> midActionStatic, Map<String, String> featureMap, String cid) {
+        if (midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+            featureMap.put("timediff_view", String.valueOf(midTimeDiffMap.getOrDefault("timediff_view_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_click_" + cid)) {
+            featureMap.put("timediff_click", String.valueOf(midTimeDiffMap.getOrDefault("timediff_click_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_conver_" + cid)) {
+            featureMap.put("timediff_conver", String.valueOf(midTimeDiffMap.getOrDefault("timediff_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid)) {
+            featureMap.put("actionstatic_view", String.valueOf(midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            featureMap.put("actionstatic_click", String.valueOf(midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            featureMap.put("actionstatic_conver", String.valueOf(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_income_" + cid)) {
+            featureMap.put("actionstatic_income", String.valueOf(midActionStatic.getOrDefault("actionstatic_income_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double ctr = NumUtil.div(
+                    midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0),
+                    midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)
+            );
+            featureMap.put("actionstatic_ctr", String.valueOf(ctr));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            double ctcvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0));
+            featureMap.put("actionstatic_ctcvr", String.valueOf(ctcvr));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double cvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0));
+            featureMap.put("actionstatic_cvr", String.valueOf(cvr));
+        }
+    }
+
+    private void handleD1Feature(Map<String, String> d1Feature, Map<String, String> featureMap) {
+        for (String prefix : Arrays.asList("3h", "6h", "12h", "1d", "3d", "7d")) {
+            double view = Double.parseDouble(d1Feature.getOrDefault("ad_view_" + prefix, "0"));
+            double click = Double.parseDouble(d1Feature.getOrDefault("ad_click_" + prefix, "0"));
+            double conver = Double.parseDouble(d1Feature.getOrDefault("ad_conversion_" + prefix, "0"));
+            double income = Double.parseDouble(d1Feature.getOrDefault("ad_income_" + prefix, "0"));
+            double cpc = NumUtil.div(income, click);
+            double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+            featureMap.put("d1_feature_" + prefix + "_ctr", String.valueOf(ctr));
+            featureMap.put("d1_feature_" + prefix + "_ctcvr", String.valueOf(NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_conver", String.valueOf(conver));
+            featureMap.put("d1_feature_" + prefix + "_ecpm", String.valueOf(ctr * cpc * 1000));
+        }
+    }
+
+    private void handleD2Feature(Map<String, Map<String, Double>> vidRankMaps, Map<String, String> featureMap, String cid) {
+        if (MapUtils.isEmpty(vidRankMaps)) {
+            return;
+        }
+
+        List<String> prefixes1 = Arrays.asList("ctr", "ctcvr", "ecpm");
+        // List<String> prefixes1 = Arrays.asList("ctr", "ctcvr");
+        List<String> prefixes2 = Arrays.asList("1d", "3d", "7d", "14d");
+
+        for (String prefix1 : prefixes1) {
+            for (String prefix2 : prefixes2) {
+                String combinedKey = prefix1 + "_" + prefix2;
+                if (vidRankMaps.containsKey(combinedKey)) {
+                    Double rank = vidRankMaps.get(combinedKey).getOrDefault(cid, 0.0);
+                    if (rank >= 1.0) {
+                        featureMap.put("vid_rank_" + combinedKey, String.valueOf(NumUtil.div(1, rank)));
+                    }
+                }
+            }
+        }
+    }
+
+    private void handleH1AndH2Feature(Map<String, Map<String, String>> skuFeature,
+                                      Map<String, Map<String, String>> adVerFeature,
+                                      Map<String, String> cidFeatureMap) {
+        Map<String, String> h1Feature = adVerFeature.getOrDefault("alg_mid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> h2Feature = skuFeature.getOrDefault("alg_mid_feature_sku_action", EMPTY_STRING_MAP);
+        List<String> timeList = Arrays.asList("3d", "7d", "30d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(h1Feature, "adverid"),
+                new Tuple2<>(h2Feature, "skuid")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                String timeValue = feature.getOrDefault(time, "");
+                if (StringUtils.isNotEmpty(timeValue)) {
+                    String[] split = timeValue.split(",");
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "view" + "_" + time, split[0]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "click" + "_" + time, split[1]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "conver" + "_" + time, split[2]);
+                }
+            }
+        }
+
+
+    }
+
+    private void handleD3AndB1Feature(Map<String, String> d3Feature, String cTitle, Map<String, String> featureMap,
+                                      ScoreParam scoreParam) {
+        if (MapUtils.isEmpty(d3Feature) || !d3Feature.containsKey("title") || StringUtils.isEmpty(cTitle)) {
+            return;
+        }
+        String vTitle = d3Feature.get("title");
+        double score;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            score = SimilarityUtils.word2VecSimilarity(cTitle, vTitle);
+        } else {
+            score = Similarity.conceptSimilarity(cTitle, vTitle);
+        }
+        featureMap.put("ctitle_vtitle_similarity", String.valueOf(score));
+    }
+
+    private void handleE1AndE2Feature(Map<String, String> e1Feature, Map<String, String> e2Feature, String title,
+                                      Map<String, String> featureMap, ScoreParam scoreParam,
+                                      Map<String, List<String>> tagWordsCache) {
+        if (StringUtils.isEmpty(title)) {
+            return;
+        }
+
+        // 预先分词 title,在整个方法中复用,避免重复分词
+        List<String> titleWords = null;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            titleWords = SimilarityUtils.segment(title);
+        }
+
+        List<Tuple2<Map<String, String>, String>> tuple2List = Arrays.asList(new Tuple2<>(e1Feature, "e1"), new Tuple2<>(e2Feature, "e2"));
+
+        List<String> tagsFieldList = Arrays.asList("tags_3d", "tags_7d", "tags_14d");
+        for (Tuple2<Map<String, String>, String> tuple2 : tuple2List) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            if (MapUtils.isEmpty(feature)) {
+                continue;
+            }
+
+            for (String tagsField : tagsFieldList) {
+                if (StringUtils.isNotEmpty(feature.get(tagsField))) {
+                    String tags = feature.get(tagsField);
+                    Double[] doubles;
+                    if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+                        // 使用缓存的 title 分词结果和请求级别的 tag 分词缓存
+                        doubles = ExtractorUtils.funcC34567ForTagsNewWithCache(tags, title, titleWords, tagWordsCache);
+                    } else {
+                        doubles = ExtractorUtils.funcC34567ForTags(tags, title);
+                    }
+                    featureMap.put(prefix + "_" + tagsField + "_matchnum", String.valueOf(doubles[0]));
+                    featureMap.put(prefix + "_" + tagsField + "_maxscore", String.valueOf(doubles[1]));
+                    featureMap.put(prefix + "_" + tagsField + "_avgscore", String.valueOf(doubles[2]));
+                }
+            }
+        }
+    }
+
+    private Map<String, Double> parseC1FeatureListToTimeDiffMap(List<TupleMapEntry<Tuple5>> midActionList, long ts) {
+        Map<String, Double> midTimeDiffMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double tsHistory = Double.parseDouble(entry.value.f1);
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double d = (ts - tsHistory) / 3600 / 24;
+            if (!midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+                midTimeDiffMap.put("timediff_view_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_click_" + cid) && click > 0) {
+                midTimeDiffMap.put("timediff_click_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_conver_" + cid) && conver > 0) {
+                midTimeDiffMap.put("timediff_conver_" + cid, NumUtil.div(1, d));
+            }
+        }
+        return midTimeDiffMap;
+    }
+
+    private Map<String, Double> parseC1FeatureListToActionStaticMap(List<TupleMapEntry<Tuple5>> midActionList) {
+        Map<String, Double> midActionStaticsMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double income = Double.parseDouble(entry.value.f4);
+
+            Double viewSum = midActionStaticsMap.getOrDefault("actionstatic_view_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_view_" + cid, 1 + viewSum);
+
+            Double clickSum = midActionStaticsMap.getOrDefault("actionstatic_click_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_click_" + cid, clickSum + click);
+
+            Double converSum = midActionStaticsMap.getOrDefault("actionstatic_conver_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_conver_" + cid, converSum + conver);
+
+            Double incomSum = midActionStaticsMap.getOrDefault("actionstatic_income_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_income_" + cid, incomSum + income);
+        }
+
+        return midActionStaticsMap;
+    }
+
+    private Map<String, Map<String, Double>> parseD2FeatureMap(Map<String, String> d2Feature) {
+        Map<String, Map<String, Double>> vidRankMaps = new HashMap<>();
+        for (Map.Entry<String, String> entry : d2Feature.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            Map<String, Double> valueMap = Arrays.stream(value.split(",")).map(r -> r.split(":")).collect(Collectors.toMap(rList -> rList[0], rList -> Double.parseDouble(rList[2])));
+            vidRankMaps.put(key, valueMap);
+        }
+        return vidRankMaps;
+    }
+
+    private void readBucketFile() {
+        if (MapUtils.isNotEmpty(bucketsMap)) {
+            return;
+        }
+        synchronized (this) {
+            String bucketFile = "20250217_ad_bucket_688.txt";
+            InputStream resourceStream = this.getClass().getClassLoader().getResourceAsStream(bucketFile);
+            if (resourceStream != null) {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
+                    Map<String, double[]> bucketsMap = new HashMap<>();
+                    Map<String, Double> bucketsLen = new HashMap<>();
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        // 替换空格和换行符,过滤空行
+                        line = line.replace(" ", "").replaceAll("\n", "");
+                        if (!line.isEmpty()) {
+                            String[] rList = line.split("\t");
+                            if (rList.length == 3) {
+                                String key = rList[0];
+                                double value1 = Double.parseDouble(rList[1]);
+                                bucketsLen.put(key, value1);
+                                double[] value2 = Arrays.stream(rList[2].split(",")).mapToDouble(Double::valueOf).toArray();
+                                bucketsMap.put(key, value2);
+                            }
+                        }
+                    }
+                    this.bucketsMap = bucketsMap;
+                    this.bucketsLen = bucketsLen;
+                } catch (IOException e) {
+                    log.error("something is wrong in parse bucket file: ", e);
+                }
+                log.info("load bucket file success: {}", bucketFile);
+            } else {
+                log.error("no bucket file");
+            }
+        }
+    }
+
+    private void initSparseFeatureNames() {
+        this.sparseFeatureSet = new HashSet<String>() {{
+            add("brand");
+            add("region");
+            add("city");
+            add("vid");
+            add("cate1");
+            add("cate2");
+            add("cid");
+            add("adid");
+            add("adverid");
+            add("user_cid_click_list");
+            add("user_cid_conver_list");
+            add("user_vid_return_tags_2h");
+            add("user_vid_return_tags_1d");
+            add("user_vid_return_tags_3d");
+            add("user_vid_return_tags_7d");
+            add("user_vid_return_tags_14d");
+            add("apptype");
+            add("hour");
+            add("hour_quarter");
+            add("root_source_scene");
+            add("root_source_channel");
+            add("is_first_layer");
+            add("title_split");
+            add("profession");
+            add("user_vid_share_tags_1d");
+            add("user_vid_share_tags_14d");
+            add("user_vid_return_cate1_14d");
+            add("user_vid_return_cate2_14d");
+            add("user_vid_share_cate1_14d");
+            add("user_vid_share_cate2_14d");
+            add("user_has_conver_1y");
+            add("user_adverid_view_3d");
+            add("user_adverid_click_3d");
+            add("user_adverid_conver_3d");
+            add("user_adverid_view_7d");
+            add("user_adverid_click_7d");
+            add("user_adverid_conver_7d");
+            add("user_adverid_view_30d");
+            add("user_adverid_click_30d");
+            add("user_adverid_conver_30d");
+            add("user_skuid_view_3d");
+            add("user_skuid_click_3d");
+            add("user_skuid_conver_3d");
+            add("user_skuid_view_7d");
+            add("user_skuid_click_7d");
+            add("user_skuid_conver_7d");
+            add("user_skuid_view_30d");
+            add("user_skuid_click_30d");
+            add("user_skuid_conver_30d");
+            add("user_conver_ad_class");
+            add("category_name");
+            add("material_md5");
+        }};
+    }
+
+    private Map<String, String> featureBucket(Map<String, String> featureMap) {
+        // 使用 HashMap 替代 ConcurrentHashMap,分桶操作是单线程的
+        Map<String, String> newFeatureMap = new HashMap<>(featureMap.size());
+        for (Map.Entry<String, String> entry : featureMap.entrySet()) {
+            try {
+                String name = entry.getKey();
+                if (this.sparseFeatureSet.contains(name)) {
+                    if (entry.getValue() != null) {
+                        newFeatureMap.put(name, entry.getValue());
+                    }
+                    continue;
+                }
+                double score = Double.parseDouble(entry.getValue());
+                // 注意:0值、不在分桶文件中的特征,会被过滤掉。
+                if (score > 1E-8) {
+                    if (this.bucketsMap.containsKey(name) && this.bucketsLen.containsKey(name)) {
+                        double[] buckets = this.bucketsMap.get(name);
+                        double bucketNum = this.bucketsLen.get(name);
+                        Double scoreNew = 1.0 / bucketNum * (ExtractorUtils.findInsertPosition(buckets, score) + 1.0);
+                        newFeatureMap.put(name, String.valueOf(scoreNew));
+                    } else {
+                        newFeatureMap.put(name, String.valueOf(score));
+                    }
+                }
+            } catch (Exception e) {
+                log.error("featureBucket error: ", e);
+            }
+        }
+        return newFeatureMap;
+    }
+
+    private double getExpWeight(Map<String, String> featureMap,
+                                String expOldKey, double expOldThreshold,
+                                String expNewKey, double expNewThreshold,
+                                double expLowerWeight, double expUpperWeight, double expScale) {
+        try {
+            if (null != featureMap) {
+                double oldView = Double.parseDouble(featureMap.getOrDefault(expOldKey, "0"));
+                if (oldView < expOldThreshold) {
+                    double newView = Double.parseDouble(featureMap.getOrDefault(expNewKey, "0"));
+                    return getExpWeight(expLowerWeight, expUpperWeight, expScale, expNewThreshold, newView);
+                }
+            }
+        } catch (Exception e) {
+            log.error("getExpWeight error: ", e);
+        }
+        return 1.0;
+    }
+
+    private double getExpWeight(double lowerWeight, double upperWeight, double scale, double upperExp, double exp) {
+        if (exp >= upperExp) {
+            return 1.0;
+        }
+        double weight = Math.log(exp + 1) / scale;
+        return Math.min(Math.max(lowerWeight, weight), upperWeight);
+    }
+}

+ 948 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy834.java

@@ -0,0 +1,948 @@
+package com.tzld.piaoquan.ad.engine.service.score.strategy;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.piaoquan.ad.engine.commons.dto.AdPlatformCreativeDTO;
+import com.tzld.piaoquan.ad.engine.commons.helper.DnnCidDataHelper;
+import com.tzld.piaoquan.ad.engine.commons.param.RankRecommendRequestParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScoreParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScorerUtils;
+import com.tzld.piaoquan.ad.engine.commons.thread.ThreadPoolFactory;
+import com.tzld.piaoquan.ad.engine.commons.util.*;
+import com.tzld.piaoquan.ad.engine.service.entity.CorrectCpaParam;
+import com.tzld.piaoquan.ad.engine.service.entity.GuaranteeView;
+import com.tzld.piaoquan.ad.engine.service.feature.Feature;
+import com.tzld.piaoquan.recommend.feature.domain.ad.base.AdRankItem;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.xm.Similarity;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.tzld.piaoquan.ad.engine.commons.math.Const.*;
+
+@Slf4j
+@Component
+public class RankStrategyBy834 extends RankStrategyBasic {
+
+    /**
+     * 空 Map 常量,避免频繁创建空 HashMap
+     */
+    private static final Map<String, String> EMPTY_STRING_MAP = Collections.emptyMap();
+    private static final Map<String, Map<String, String>> EMPTY_NESTED_MAP = Collections.emptyMap();
+
+    private Map<String, double[]> bucketsMap = new HashMap<>();
+
+    private Map<String, Double> bucketsLen = new HashMap<>();
+
+    @Value("${word2vec.exp:694}")
+    private String word2vecExp;
+
+
+    // FIXME(zhoutian): 可能需要独立配置
+    @ApolloJsonValue("${rank.score.weight.680:{}}")
+    private Map<String, Double> weightMap;
+
+    /**
+     * 人群分层&创意的权重
+     * 格式:{layer_creativeId: weight}
+     */
+    @ApolloJsonValue("${rank.score.weight.layer.and.creative:{}}")
+    private Map<String, Double> layerAndCreativeWeightMap;
+
+    @ApolloJsonValue("${rank.score.neg_sample_rate:0.01}")
+    Double negSampleRate;
+
+    Set<String> sparseFeatureSet;
+
+
+    @PostConstruct
+    public void afterInit() {
+        this.readBucketFile();
+        this.initSparseFeatureNames();
+    }
+
+
+    @Override
+    public List<AdRankItem> adItemRank(RankRecommendRequestParam request, ScoreParam scoreParam) {
+        Map<String, Double> weightParam = ObjUtil.nullOrDefault(weightMap, new HashMap<>());
+
+
+        Map<Long, Double> creativeScoreCoefficient = getCreativeScoreCoefficient();
+        Set<String> noApiAdVerIds = getNoApiAdVerIds();
+
+        long ts = System.currentTimeMillis() / 1000;
+
+        String brand = scoreParam.getRequestContext().getMachineinfoBrand();
+        if (StringUtils.isNotEmpty(brand)) {
+            scoreParam.getRequestContext().setMachineinfoBrand(brand + "-n");
+        }
+
+        long start = System.currentTimeMillis();
+        //过滤创意
+        filterRequestAdList(request, scoreParam);
+        // 特征处理
+        // feature1
+        Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
+
+        Map<String, Map<String, String>> userFeature = feature.getUserFeature();
+        Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();
+        Map<String, Map<String, Map<String, String>>> allAdVerFeature = feature.getAdVerFeature();
+        Map<String, Map<String, Map<String, String>>> allCidFeature = feature.getCidFeature();
+        Map<String, Map<String, Map<String, String>>> allSkuFeature = feature.getSkuFeature();
+        Map<String, String> reqFeature = this.getReqFeature(scoreParam, request);
+
+        Map<String, String> userFeatureMap = new HashMap<>();
+        Map<String, String> c1Feature = userFeature.getOrDefault("alg_mid_feature_ad_action", EMPTY_STRING_MAP);
+        List<TupleMapEntry<Tuple5>> midActionList = this.handleC1Feature(c1Feature, userFeatureMap);
+
+        Map<String, Double> midTimeDiffMap = this.parseC1FeatureListToTimeDiffMap(midActionList, ts);
+        Map<String, Double> actionStaticMap = this.parseC1FeatureListToActionStaticMap(midActionList);
+
+        Map<String, String> d2Feature = videoFeature.getOrDefault("alg_cid_feature_vid_cf_rank", EMPTY_STRING_MAP);
+        Map<String, String> d3Feature = videoFeature.getOrDefault("alg_vid_feature_basic_info", EMPTY_STRING_MAP);
+
+        Map<String, Map<String, Double>> vidRankMaps = this.parseD2FeatureMap(d2Feature);
+
+        Map<String, String> e1Feature = userFeature.getOrDefault("alg_mid_feature_return_tags", EMPTY_STRING_MAP);
+        Map<String, String> e2Feature = userFeature.getOrDefault("alg_mid_feature_share_tags", EMPTY_STRING_MAP);
+
+        Map<String, String> g1Feature = userFeature.getOrDefault("mid_return_video_cate", EMPTY_STRING_MAP);
+        Map<String, String> g2Feature = userFeature.getOrDefault("mid_share_video_cate", EMPTY_STRING_MAP);
+
+
+        userFeatureMap.put("brand", reqFeature.getOrDefault("brand", ""));
+        userFeatureMap.put("region", reqFeature.getOrDefault("region", ""));
+        userFeatureMap.put("city", reqFeature.getOrDefault("city", ""));
+        userFeatureMap.put("vid", reqFeature.getOrDefault("vid", ""));
+        userFeatureMap.put("apptype", reqFeature.getOrDefault("apptype", ""));
+        userFeatureMap.put("is_first_layer", reqFeature.getOrDefault("is_first_layer", ""));
+        userFeatureMap.put("root_source_scene", reqFeature.getOrDefault("root_source_scene", ""));
+        userFeatureMap.put("root_source_channel", reqFeature.getOrDefault("root_source_channel", ""));
+
+
+        userFeatureMap.put("cate1", d3Feature.get("merge_first_level_cate"));
+        userFeatureMap.put("cate2", d3Feature.get("merge_second_level_cate"));
+        userFeatureMap.put("user_vid_return_tags_2h", e1Feature.getOrDefault("tags_2h", null));
+        userFeatureMap.put("user_vid_return_tags_1d", e1Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_return_tags_3d", e1Feature.getOrDefault("tags_3d", null));
+        userFeatureMap.put("user_vid_return_tags_7d", e1Feature.getOrDefault("tags_7d", null));
+        userFeatureMap.put("user_vid_return_tags_14d", e1Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("title_split", d3Feature.getOrDefault("title_split", null));
+        userFeatureMap.put("user_vid_share_tags_1d", e2Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_share_tags_14d", e2Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("user_vid_return_cate1_14d", g1Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_return_cate2_14d", g1Feature.getOrDefault("cate2_14d", null));
+        userFeatureMap.put("user_vid_share_cate1_14d", g2Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_share_cate2_14d", g2Feature.getOrDefault("cate2_14d", null));
+
+        Map<String, String> sceneFeatureMap = this.handleSceneFeature(ts);
+        long time1 = System.currentTimeMillis();
+
+        boolean isGuaranteedFlow = getIsGuaranteedFlow(scoreParam);
+        Map<String, GuaranteeView> map = getGuaranteeViewMap(request, isGuaranteedFlow);
+        Map<Long, CorrectCpaParam> correctCpaMap = getCorrectCpaParamMap(request, scoreParam, reqFeature);
+        List<AdRankItem> adRankItems = new ArrayList<>();
+        Random random = new Random();
+        List<Future<AdRankItem>> futures = new ArrayList<>();
+        CountDownLatch cdl1 = new CountDownLatch(request.getAdIdList().size());
+        for (AdPlatformCreativeDTO dto : request.getAdIdList()) {
+            Future<AdRankItem> future = ThreadPoolFactory.feature().submit(() -> {
+                AdRankItem adRankItem = new AdRankItem();
+                try {
+                    adRankItem.setAdId(dto.getCreativeId());
+                    adRankItem.setCreativeCode(dto.getCreativeCode());
+                    adRankItem.setAdVerId(dto.getAdVerId());
+                    adRankItem.setVideoId(request.getVideoId());
+                    adRankItem.setCpa(dto.getCpa());
+                    adRankItem.setId(dto.getAdId());
+                    adRankItem.setCampaignId(dto.getCampaignId());
+                    adRankItem.setCpm(ObjUtil.nullOrDefault(dto.getCpm(), 90).doubleValue());
+                    adRankItem.setSkuId(dto.getSkuId());
+                    adRankItem.setCustomerId(dto.getCustomerId());
+                    adRankItem.setProfession(dto.getProfession());
+                    adRankItem.setRandom(random.nextInt(1000));
+                    if (noApiAdVerIds.contains(dto.getAdVerId())) {
+                        adRankItem.getExt().put("isApi", "0");
+                    } else {
+                        adRankItem.getExt().put("isApi", "1");
+                    }
+                    adRankItem.getExt().put("recallsources", dto.getRecallSources());
+                    adRankItem.getExt().put("correctCpaMap", JSONObject.toJSONString(correctCpaMap.get(dto.getAdId())));
+                    adRankItem.getExt().put("correctionFactor", correctCpaMap.get(dto.getAdId()).getCorrectionFactor());
+                    setGuaranteeWeight(map, dto.getAdVerId(), adRankItem.getExt(), isGuaranteedFlow, reqFeature);
+                    String cidStr = dto.getCreativeId().toString();
+                    Map<String, String> cidFeatureMap = adRankItem.getFeatureMap();
+                    Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+                    Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+
+                    Map<String, Map<String, String>> adVerFeature = allAdVerFeature.getOrDefault(dto.getAdVerId(), EMPTY_NESTED_MAP);
+                    Map<String, Map<String, String>> skuFeature = allSkuFeature.getOrDefault(String.valueOf(dto.getSkuId()), EMPTY_NESTED_MAP);
+                    Map<String, String> d1Feature = cidFeature.getOrDefault("alg_cid_feature_vid_cf", EMPTY_STRING_MAP);
+
+                    this.handleB1Feature(b1Feature, cidFeatureMap, cidStr);
+                    this.handleB2ToB5AndB8ToB9Feature(cidFeature, adVerFeature, cidFeatureMap);
+                    this.handleB6ToB7Feature(cidFeature, cidFeatureMap);
+                    this.handleC1UIFeature(midTimeDiffMap, actionStaticMap, cidFeatureMap, cidStr);
+                    this.handleD1Feature(d1Feature, cidFeatureMap);
+                    this.handleD2Feature(vidRankMaps, cidFeatureMap, cidStr);
+                    this.handleH1AndH2Feature(skuFeature, adVerFeature, cidFeatureMap);
+                    cidFeatureMap.put("cid", dto.getCreativeId() != null ? String.valueOf(dto.getCreativeId()) : "");
+                    cidFeatureMap.put("adid", dto.getAdId() != null ? String.valueOf(dto.getAdId()) : "");
+                    cidFeatureMap.put("adverid", dto.getAdVerId() != null ? dto.getAdVerId() : "");
+                    cidFeatureMap.put("profession", dto.getProfession() != null ? dto.getProfession() : "");
+                    cidFeatureMap.put("category_name", dto.getCategoryName() != null ? dto.getCategoryName() : "");
+                    cidFeatureMap.put("material_md5", dto.getMaterialMd5() != null ? dto.getMaterialMd5() : "");
+                    //DNN模型没训练过的cid才不传入广告相关的稀疏特征
+                    if (CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSet()) && !DnnCidDataHelper.getCidSet().contains(adRankItem.getAdId())) {
+                        cidFeatureMap.put("cid", "");
+                        cidFeatureMap.put("adid", "");
+                        cidFeatureMap.put("adverid", "");
+                    }
+                    return adRankItem;
+                } finally {
+                    cdl1.countDown();
+                }
+            });
+            futures.add(future);
+        }
+        try {
+            cdl1.await(300, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        for (Future<AdRankItem> future : futures) {
+            try {
+                if (future.isDone()) {
+                    adRankItems.add(future.get());
+                }
+            } catch (Exception e) {
+                log.error("Feature handle error", e);
+            }
+        }
+
+        long time2 = System.currentTimeMillis();
+        // feature3
+        // 请求级别的 tag 分词缓存,所有广告共享(同一用户的 tags 相同)
+        Map<String, List<String>> tagWordsCache = new ConcurrentHashMap<>();
+        CountDownLatch cdl2 = new CountDownLatch(adRankItems.size() * 2);
+        for (AdRankItem item : adRankItems) {
+            String cidStr = String.valueOf(item.getAdId());
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+            Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+            String title = b1Feature.getOrDefault("cidtitle", "");
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleE1AndE2Feature(e1Feature, e2Feature, title, item.getFeatureMap(), scoreParam, tagWordsCache);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleD3AndB1Feature(d3Feature, title, item.getFeatureMap(), scoreParam);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+        }
+        try {
+            cdl2.await(150, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+
+        long time3 = System.currentTimeMillis();
+        // 分桶
+        userFeatureMap = this.featureBucket(userFeatureMap);
+        CountDownLatch cdl4 = new CountDownLatch(adRankItems.size());
+        for (AdRankItem adRankItem : adRankItems) {
+            ThreadPoolFactory.feature().submit(() -> {
+                try {
+                    Map<String, String> featureMap = adRankItem.getFeatureMap();
+                    adRankItem.setFeatureMap(this.featureBucket(featureMap));
+                } finally {
+                    cdl4.countDown();
+                }
+            });
+        }
+        try {
+            cdl4.await(100, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        long time4 = System.currentTimeMillis();
+        // 打分排序
+        // getScorerPipeline
+
+        if (CollectionUtils.isEmpty(adRankItems)) {
+            log.error("adRankItems is empty");
+        }
+        List<AdRankItem> result = ScorerUtils.getScorerPipeline(ScorerUtils.PAI_SCORE_CONF_20250214).scoring(sceneFeatureMap, userFeatureMap, adRankItems);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("scoring result is empty");
+        }
+        long time5 = System.currentTimeMillis();
+        // calibrate score for negative sampling or cold start
+        for (AdRankItem item : result) {
+            double originalScore = item.getLrScore();
+            double calibratedScore = originalScore / (originalScore + (1 - originalScore) / negSampleRate);
+            // 该创意尚未在模型中训练,打分不可靠
+            if (CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSet()) && !DnnCidDataHelper.getCidSet().contains(item.getAdId())) {
+                Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+                Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+                double view = Double.parseDouble(b3Feature.getOrDefault("ad_view_14d", "0"));
+                double conver = Double.parseDouble(b3Feature.getOrDefault("ad_conversion_14d", "0"));
+                double smoothCxr = NumUtil.divSmoothV1(conver, view, 1.64);
+                //模型打分和统计计算取打分更低的
+                item.getScoreMap().put("cvcvrItemValue", 1.0);
+                if(smoothCxr <= calibratedScore){
+                    calibratedScore = smoothCxr;
+                    item.getScoreMap().put("cvcvrItemValue", 2.0);
+                }
+            }
+            item.setLrScore(calibratedScore);
+            item.getScoreMap().put("originCtcvrScore", originalScore);
+            item.getScoreMap().put("modelCtcvrScore", calibratedScore);
+            item.getScoreMap().put("ctcvrScore", calibratedScore);
+        }
+
+        calculateCtcvrScore(result, request, scoreParam, "dnn", reqFeature);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("calculateCtcvrScore result is empty");
+        }
+        // loop
+        double cpmCoefficient = weightParam.getOrDefault("cpmCoefficient", 0.9);
+        boolean isGuaranteeType = false;
+        // 查询人群分层信息
+        String peopleLayer = Optional.of(reqFeature)
+                .map(f -> f.get("layer"))
+                .map(s -> s.replace("-炸", ""))
+                .orElse(null);
+        for (AdRankItem item : result) {
+            double bid = item.getCpa();
+            if (scoreParam.getExpCodeSet().contains(correctCpaExp1) || scoreParam.getExpCodeSet().contains(correctCpaExp2)) {
+                Double correctionFactor = (Double) item.getExt().get("correctionFactor");
+                item.getScoreMap().put("correctionFactor", correctionFactor);
+                bid = bid * correctionFactor;
+            }
+            item.getScoreMap().put("ecpm", item.getLrScore() * bid * 1000);
+            if (isGuaranteedFlow && item.getExt().get("isGuaranteed") != null && (boolean) item.getExt().get("isGuaranteed")) {
+                isGuaranteeType = true;
+            }
+
+            String layerAndCreativeWeightMapKey = getLayerAndCreativeWeightMapKey(peopleLayer, String.valueOf(item.getAdId()));
+            // 人群分层&创意的权重
+            double layerAndCreativeWeight = getLayerAndCreativeWeight(layerAndCreativeWeightMapKey);
+            double scoreCoefficient = creativeScoreCoefficient.getOrDefault(item.getAdId(), 1d);
+            double guaranteeScoreCoefficient = getGuaranteeScoreCoefficient(isGuaranteedFlow, item.getExt());
+            double score = item.getLrScore() * bid * scoreCoefficient * guaranteeScoreCoefficient * layerAndCreativeWeight;
+            item.getScoreMap().put("guaranteeScoreCoefficient", guaranteeScoreCoefficient);
+            item.getScoreMap().put("cpa", item.getCpa());
+            item.getScoreMap().put("cpm", item.getCpm());
+            item.getScoreMap().put("bid", bid);
+            item.getScoreMap().put("cpmCoefficient", cpmCoefficient);
+            item.getScoreMap().put("scoreCoefficient", scoreCoefficient);
+            item.getFeatureMap().putAll(userFeatureMap);
+            item.getFeatureMap().putAll(sceneFeatureMap);
+
+            // 没有转化回传的广告主,使用后台配置的CPM
+            if (noApiAdVerIds.contains(item.getAdVerId())) {
+                score = item.getCpm() * cpmCoefficient / 1000;
+            }
+            item.setScore(score);
+        }
+
+
+        result.sort(ComparatorUtil.equalsRandomComparator());
+
+        if (CollectionUtils.isNotEmpty(result)) {
+            AdRankItem top1Item = result.get(0);
+
+            // 判断第一名是否是保量广告
+            boolean top1IsGuaranteed = top1Item.getExt().get("isGuaranteed") != null
+                    && (boolean) top1Item.getExt().get("isGuaranteed");
+
+            // 如果第一名不是保量广告,则随机取一条,并取消保量标记
+            if (!top1IsGuaranteed && result.size() > 1) {
+                Collections.shuffle(result);
+                top1Item = result.get(0);
+                // 随机取出的广告取消保量标记
+                top1Item.getExt().put("isGuaranteed", false);
+                top1Item.getExt().put("guaranteeWeight", 1.0);
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(result)) {
+            AdRankItem top1Item = result.get(0);
+            List<String> participateCompetitionType = new ArrayList<>();
+            participateCompetitionType.add("engine");
+            top1Item.getExt().put("isGuaranteeType", isGuaranteeType);
+            if (isGuaranteeType) {
+                participateCompetitionType.add("guarantee");
+            }
+            top1Item.getExt().put("participateCompetitionType", StringUtils.join(participateCompetitionType, ","));
+            Double modelCtcvrScore = top1Item.getScoreMap().get("modelCtcvrScore");
+            Double ctcvrScore = top1Item.getScoreMap().get("ctcvrScore");
+            if (scoreParam.getExpCodeSet().contains(checkoutEcpmExp)) {
+                top1Item.getExt().put("ecpm", ctcvrScore * top1Item.getCpa() * 1000);
+                top1Item.getExt().put("filterEcpm", filterEcpm);
+            } else {
+                top1Item.getExt().put("ecpm", modelCtcvrScore * top1Item.getCpa() * 1000);
+            }
+            putMetaFeature(top1Item, feature, reqFeature, sceneFeatureMap, request);
+            top1Item.getExt().put("model", "dnn");
+        }
+        long time6 = System.currentTimeMillis();
+        log.info("cost={}, getFeature={}, handleFeature={},  similar={}, bucketFeature={}, getScorerPipeline={}, " +
+                        "other={}, adIdSize={}, adRankItemsSize={}",
+                time6 - start, time1 - start, time2 - time1, time3 - time2, time4 - time3,
+                time5 - time4, time6 - time5, request.getAdIdList().size(), adRankItems.size());
+
+        return result;
+    }
+
+    /**
+     * 获取人群分层和创意的权重
+     *
+     * @param key
+     * @return
+     */
+    private Double getLayerAndCreativeWeight(String key) {
+        if (StringUtils.isBlank(key)) {
+            return 1d;
+        }
+        return layerAndCreativeWeightMap.getOrDefault(key, 1d);
+    }
+
+    /**
+     * 获取人群分层和创意的权重key
+     *
+     * @param layer
+     * @param creativeId
+     * @return
+     */
+    private String getLayerAndCreativeWeightMapKey(String layer, String creativeId) {
+        if (StringUtils.isBlank(layer) || StringUtils.isBlank(creativeId)) {
+            return null;
+        }
+        return layer + "_" + creativeId;
+    }
+
+
+    private void handleB1Feature(Map<String, String> b1Feature, Map<String, String> cidFeatureMap, String cid) {
+        cidFeatureMap.put("cid_" + cid, "0.1");
+        // if (StringUtils.isNotBlank(b1Feature.get("adid"))) {
+        //     String adId = b1Feature.get("adid");
+        //     cidFeatureMap.put("adid_" + adId, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("adverid"))) {
+            String adVerId = b1Feature.get("adverid");
+            cidFeatureMap.put("adverid_" + adVerId, "0.1");
+        }
+        // if (StringUtils.isNotBlank(b1Feature.get("targeting_conversion"))) {
+        //     String targetingConversion = b1Feature.get("targeting_conversion");
+        //     cidFeatureMap.put("targeting_conversion_" + targetingConversion, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("cpa"))) {
+            String cpa = b1Feature.get("cpa");
+            cidFeatureMap.put("cpa", cpa);
+        }
+    }
+
+    private void handleB2ToB5AndB8ToB9Feature(Map<String, Map<String, String>> c1Feature, Map<String, Map<String, String>> adVerFeature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b2Feature = adVerFeature.getOrDefault("alg_cid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> b3Feature = c1Feature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+        Map<String, String> b4Feature = c1Feature.getOrDefault("alg_cid_feature_region_action", EMPTY_STRING_MAP);
+        Map<String, String> b5Feature = c1Feature.getOrDefault("alg_cid_feature_app_action", EMPTY_STRING_MAP);
+        Map<String, String> b8Feature = c1Feature.getOrDefault("alg_cid_feature_brand_action", EMPTY_STRING_MAP);
+        Map<String, String> b9Feature = c1Feature.getOrDefault("alg_cid_feature_weChatVersion_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("1h", "2h", "3h", "6h", "12h", "1d", "3d", "7d", "yesterday", "today");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b2Feature, "b2"),
+                new Tuple2<>(b3Feature, "b3"),
+                new Tuple2<>(b4Feature, "b4"),
+                new Tuple2<>(b5Feature, "b5"),
+                new Tuple2<>(b8Feature, "b8"),
+                new Tuple2<>(b9Feature, "b9")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private void handleB6ToB7Feature(Map<String, Map<String, String>> c1Feature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b6Feature = c1Feature.getOrDefault("alg_cid_feature_week_action", EMPTY_STRING_MAP);
+        Map<String, String> b7Feature = c1Feature.getOrDefault("alg_cid_feature_hour_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("7d", "14d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b6Feature, "b6"),
+                new Tuple2<>(b7Feature, "b7")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private List<TupleMapEntry<Tuple5>> handleC1Feature(Map<String, String> c1Feature, Map<String, String> featureMap) {
+
+        //用户近1年内是否有转化
+        if (c1Feature.containsKey("user_has_conver_1y") && c1Feature.get("user_has_conver_1y") != null) {
+            featureMap.put("user_has_conver_1y", c1Feature.get("user_has_conver_1y"));
+        }
+        //用户历史转化过品类
+        if (c1Feature.containsKey("user_conver_ad_class") && c1Feature.get("user_conver_ad_class") != null) {
+            featureMap.put("user_conver_ad_class", c1Feature.get("user_conver_ad_class"));
+        }
+
+        // 用户特征
+        List<TupleMapEntry<Tuple5>> midActionList = new ArrayList<>();
+        if (c1Feature.containsKey("action")) {
+            String action = c1Feature.get("action");
+            midActionList = Arrays.stream(action.split(","))
+                    .map(r -> {
+                        String[] rList = r.split(":");
+                        Tuple5 tuple5 = new Tuple5(rList[1], rList[2], rList[3], rList[4], rList[5]);
+                        return new TupleMapEntry<>(rList[0], tuple5);
+                    })
+                    // TODO 倒排
+                    .sorted((a, b) -> Integer.compare(Integer.parseInt(b.value.f1), Integer.parseInt(a.value.f1)))
+                    .collect(Collectors.toList());
+        }
+
+        double viewAll = midActionList.size();
+        double clickAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f2)).sum();
+        double converAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f3)).sum();
+        double incomeAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f4)).sum();
+        featureMap.put("viewAll", String.valueOf(viewAll));
+        featureMap.put("clickAll", String.valueOf(clickAll));
+        featureMap.put("converAll", String.valueOf(converAll));
+        featureMap.put("incomeAll", String.valueOf(incomeAll));
+        featureMap.put("ctr_all", String.valueOf(NumUtil.div(clickAll, viewAll)));
+        featureMap.put("ctcvr_all", String.valueOf(NumUtil.div(converAll, viewAll)));
+        featureMap.put("cvr_all", String.valueOf(NumUtil.div(clickAll, converAll)));
+        featureMap.put("ecpm_all", String.valueOf(NumUtil.div(incomeAll * 1000, viewAll)));
+        if (CollectionUtils.isNotEmpty(midActionList)) {
+            List<String> cidClickList = new ArrayList<>();
+            List<String> cidConverList = new ArrayList<>();
+            for (TupleMapEntry<Tuple5> tupleMapEntry : midActionList) {
+                String cid = tupleMapEntry.key;
+                String click = tupleMapEntry.value.f2;
+                String conver = tupleMapEntry.value.f3;
+                if (Objects.equals(click, "1")) {
+                    cidClickList.add(cid);
+                }
+                if (Objects.equals(conver, "1")) {
+                    cidConverList.add(cid);
+                }
+            }
+            featureMap.put("user_cid_click_list", String.join(",", cidClickList));
+            featureMap.put("user_cid_conver_list", String.join(",", cidConverList));
+        }
+        return midActionList;
+    }
+
+    private void handleC1UIFeature(Map<String, Double> midTimeDiffMap, Map<String, Double> midActionStatic, Map<String, String> featureMap, String cid) {
+        if (midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+            featureMap.put("timediff_view", String.valueOf(midTimeDiffMap.getOrDefault("timediff_view_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_click_" + cid)) {
+            featureMap.put("timediff_click", String.valueOf(midTimeDiffMap.getOrDefault("timediff_click_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_conver_" + cid)) {
+            featureMap.put("timediff_conver", String.valueOf(midTimeDiffMap.getOrDefault("timediff_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid)) {
+            featureMap.put("actionstatic_view", String.valueOf(midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            featureMap.put("actionstatic_click", String.valueOf(midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            featureMap.put("actionstatic_conver", String.valueOf(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_income_" + cid)) {
+            featureMap.put("actionstatic_income", String.valueOf(midActionStatic.getOrDefault("actionstatic_income_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double ctr = NumUtil.div(
+                    midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0),
+                    midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)
+            );
+            featureMap.put("actionstatic_ctr", String.valueOf(ctr));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            double ctcvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0));
+            featureMap.put("actionstatic_ctcvr", String.valueOf(ctcvr));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double cvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0));
+            featureMap.put("actionstatic_cvr", String.valueOf(cvr));
+        }
+    }
+
+    private void handleD1Feature(Map<String, String> d1Feature, Map<String, String> featureMap) {
+        for (String prefix : Arrays.asList("3h", "6h", "12h", "1d", "3d", "7d")) {
+            double view = Double.parseDouble(d1Feature.getOrDefault("ad_view_" + prefix, "0"));
+            double click = Double.parseDouble(d1Feature.getOrDefault("ad_click_" + prefix, "0"));
+            double conver = Double.parseDouble(d1Feature.getOrDefault("ad_conversion_" + prefix, "0"));
+            double income = Double.parseDouble(d1Feature.getOrDefault("ad_income_" + prefix, "0"));
+            double cpc = NumUtil.div(income, click);
+            double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+            featureMap.put("d1_feature_" + prefix + "_ctr", String.valueOf(ctr));
+            featureMap.put("d1_feature_" + prefix + "_ctcvr", String.valueOf(NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_conver", String.valueOf(conver));
+            featureMap.put("d1_feature_" + prefix + "_ecpm", String.valueOf(ctr * cpc * 1000));
+        }
+    }
+
+    private void handleD2Feature(Map<String, Map<String, Double>> vidRankMaps, Map<String, String> featureMap, String cid) {
+        if (MapUtils.isEmpty(vidRankMaps)) {
+            return;
+        }
+
+        List<String> prefixes1 = Arrays.asList("ctr", "ctcvr", "ecpm");
+        // List<String> prefixes1 = Arrays.asList("ctr", "ctcvr");
+        List<String> prefixes2 = Arrays.asList("1d", "3d", "7d", "14d");
+
+        for (String prefix1 : prefixes1) {
+            for (String prefix2 : prefixes2) {
+                String combinedKey = prefix1 + "_" + prefix2;
+                if (vidRankMaps.containsKey(combinedKey)) {
+                    Double rank = vidRankMaps.get(combinedKey).getOrDefault(cid, 0.0);
+                    if (rank >= 1.0) {
+                        featureMap.put("vid_rank_" + combinedKey, String.valueOf(NumUtil.div(1, rank)));
+                    }
+                }
+            }
+        }
+    }
+
+    private void handleH1AndH2Feature(Map<String, Map<String, String>> skuFeature,
+                                      Map<String, Map<String, String>> adVerFeature,
+                                      Map<String, String> cidFeatureMap) {
+        Map<String, String> h1Feature = adVerFeature.getOrDefault("alg_mid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> h2Feature = skuFeature.getOrDefault("alg_mid_feature_sku_action", EMPTY_STRING_MAP);
+        List<String> timeList = Arrays.asList("3d", "7d", "30d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(h1Feature, "adverid"),
+                new Tuple2<>(h2Feature, "skuid")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                String timeValue = feature.getOrDefault(time, "");
+                if (StringUtils.isNotEmpty(timeValue)) {
+                    String[] split = timeValue.split(",");
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "view" + "_" + time, split[0]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "click" + "_" + time, split[1]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "conver" + "_" + time, split[2]);
+                }
+            }
+        }
+
+
+    }
+
+    private void handleD3AndB1Feature(Map<String, String> d3Feature, String cTitle, Map<String, String> featureMap,
+                                      ScoreParam scoreParam) {
+        if (MapUtils.isEmpty(d3Feature) || !d3Feature.containsKey("title") || StringUtils.isEmpty(cTitle)) {
+            return;
+        }
+        String vTitle = d3Feature.get("title");
+        double score;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            score = SimilarityUtils.word2VecSimilarity(cTitle, vTitle);
+        } else {
+            score = Similarity.conceptSimilarity(cTitle, vTitle);
+        }
+        featureMap.put("ctitle_vtitle_similarity", String.valueOf(score));
+    }
+
+    private void handleE1AndE2Feature(Map<String, String> e1Feature, Map<String, String> e2Feature, String title,
+                                      Map<String, String> featureMap, ScoreParam scoreParam,
+                                      Map<String, List<String>> tagWordsCache) {
+        if (StringUtils.isEmpty(title)) {
+            return;
+        }
+
+        // 预先分词 title,在整个方法中复用,避免重复分词
+        List<String> titleWords = null;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            titleWords = SimilarityUtils.segment(title);
+        }
+
+        List<Tuple2<Map<String, String>, String>> tuple2List = Arrays.asList(new Tuple2<>(e1Feature, "e1"), new Tuple2<>(e2Feature, "e2"));
+
+        List<String> tagsFieldList = Arrays.asList("tags_3d", "tags_7d", "tags_14d");
+        for (Tuple2<Map<String, String>, String> tuple2 : tuple2List) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            if (MapUtils.isEmpty(feature)) {
+                continue;
+            }
+
+            for (String tagsField : tagsFieldList) {
+                if (StringUtils.isNotEmpty(feature.get(tagsField))) {
+                    String tags = feature.get(tagsField);
+                    Double[] doubles;
+                    if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+                        // 使用缓存的 title 分词结果和请求级别的 tag 分词缓存
+                        doubles = ExtractorUtils.funcC34567ForTagsNewWithCache(tags, title, titleWords, tagWordsCache);
+                    } else {
+                        doubles = ExtractorUtils.funcC34567ForTags(tags, title);
+                    }
+                    featureMap.put(prefix + "_" + tagsField + "_matchnum", String.valueOf(doubles[0]));
+                    featureMap.put(prefix + "_" + tagsField + "_maxscore", String.valueOf(doubles[1]));
+                    featureMap.put(prefix + "_" + tagsField + "_avgscore", String.valueOf(doubles[2]));
+                }
+            }
+        }
+    }
+
+    private Map<String, Double> parseC1FeatureListToTimeDiffMap(List<TupleMapEntry<Tuple5>> midActionList, long ts) {
+        Map<String, Double> midTimeDiffMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double tsHistory = Double.parseDouble(entry.value.f1);
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double d = (ts - tsHistory) / 3600 / 24;
+            if (!midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+                midTimeDiffMap.put("timediff_view_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_click_" + cid) && click > 0) {
+                midTimeDiffMap.put("timediff_click_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_conver_" + cid) && conver > 0) {
+                midTimeDiffMap.put("timediff_conver_" + cid, NumUtil.div(1, d));
+            }
+        }
+        return midTimeDiffMap;
+    }
+
+    private Map<String, Double> parseC1FeatureListToActionStaticMap(List<TupleMapEntry<Tuple5>> midActionList) {
+        Map<String, Double> midActionStaticsMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double income = Double.parseDouble(entry.value.f4);
+
+            Double viewSum = midActionStaticsMap.getOrDefault("actionstatic_view_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_view_" + cid, 1 + viewSum);
+
+            Double clickSum = midActionStaticsMap.getOrDefault("actionstatic_click_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_click_" + cid, clickSum + click);
+
+            Double converSum = midActionStaticsMap.getOrDefault("actionstatic_conver_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_conver_" + cid, converSum + conver);
+
+            Double incomSum = midActionStaticsMap.getOrDefault("actionstatic_income_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_income_" + cid, incomSum + income);
+        }
+
+        return midActionStaticsMap;
+    }
+
+    private Map<String, Map<String, Double>> parseD2FeatureMap(Map<String, String> d2Feature) {
+        Map<String, Map<String, Double>> vidRankMaps = new HashMap<>();
+        for (Map.Entry<String, String> entry : d2Feature.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            Map<String, Double> valueMap = Arrays.stream(value.split(",")).map(r -> r.split(":")).collect(Collectors.toMap(rList -> rList[0], rList -> Double.parseDouble(rList[2])));
+            vidRankMaps.put(key, valueMap);
+        }
+        return vidRankMaps;
+    }
+
+    private void readBucketFile() {
+        if (MapUtils.isNotEmpty(bucketsMap)) {
+            return;
+        }
+        synchronized (this) {
+            String bucketFile = "20250217_ad_bucket_688.txt";
+            InputStream resourceStream = this.getClass().getClassLoader().getResourceAsStream(bucketFile);
+            if (resourceStream != null) {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
+                    Map<String, double[]> bucketsMap = new HashMap<>();
+                    Map<String, Double> bucketsLen = new HashMap<>();
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        // 替换空格和换行符,过滤空行
+                        line = line.replace(" ", "").replaceAll("\n", "");
+                        if (!line.isEmpty()) {
+                            String[] rList = line.split("\t");
+                            if (rList.length == 3) {
+                                String key = rList[0];
+                                double value1 = Double.parseDouble(rList[1]);
+                                bucketsLen.put(key, value1);
+                                double[] value2 = Arrays.stream(rList[2].split(",")).mapToDouble(Double::valueOf).toArray();
+                                bucketsMap.put(key, value2);
+                            }
+                        }
+                    }
+                    this.bucketsMap = bucketsMap;
+                    this.bucketsLen = bucketsLen;
+                } catch (IOException e) {
+                    log.error("something is wrong in parse bucket file: ", e);
+                }
+                log.info("load bucket file success: {}", bucketFile);
+            } else {
+                log.error("no bucket file");
+            }
+        }
+    }
+
+    private void initSparseFeatureNames() {
+        this.sparseFeatureSet = new HashSet<String>() {{
+            add("brand");
+            add("region");
+            add("city");
+            add("vid");
+            add("cate1");
+            add("cate2");
+            add("cid");
+            add("adid");
+            add("adverid");
+            add("user_cid_click_list");
+            add("user_cid_conver_list");
+            add("user_vid_return_tags_2h");
+            add("user_vid_return_tags_1d");
+            add("user_vid_return_tags_3d");
+            add("user_vid_return_tags_7d");
+            add("user_vid_return_tags_14d");
+            add("apptype");
+            add("hour");
+            add("hour_quarter");
+            add("root_source_scene");
+            add("root_source_channel");
+            add("is_first_layer");
+            add("title_split");
+            add("profession");
+            add("user_vid_share_tags_1d");
+            add("user_vid_share_tags_14d");
+            add("user_vid_return_cate1_14d");
+            add("user_vid_return_cate2_14d");
+            add("user_vid_share_cate1_14d");
+            add("user_vid_share_cate2_14d");
+            add("user_has_conver_1y");
+            add("user_adverid_view_3d");
+            add("user_adverid_click_3d");
+            add("user_adverid_conver_3d");
+            add("user_adverid_view_7d");
+            add("user_adverid_click_7d");
+            add("user_adverid_conver_7d");
+            add("user_adverid_view_30d");
+            add("user_adverid_click_30d");
+            add("user_adverid_conver_30d");
+            add("user_skuid_view_3d");
+            add("user_skuid_click_3d");
+            add("user_skuid_conver_3d");
+            add("user_skuid_view_7d");
+            add("user_skuid_click_7d");
+            add("user_skuid_conver_7d");
+            add("user_skuid_view_30d");
+            add("user_skuid_click_30d");
+            add("user_skuid_conver_30d");
+            add("user_conver_ad_class");
+            add("category_name");
+            add("material_md5");
+        }};
+    }
+
+    private Map<String, String> featureBucket(Map<String, String> featureMap) {
+        // 使用 HashMap 替代 ConcurrentHashMap,分桶操作是单线程的
+        Map<String, String> newFeatureMap = new HashMap<>(featureMap.size());
+        for (Map.Entry<String, String> entry : featureMap.entrySet()) {
+            try {
+                String name = entry.getKey();
+                if (this.sparseFeatureSet.contains(name)) {
+                    if (entry.getValue() != null) {
+                        newFeatureMap.put(name, entry.getValue());
+                    }
+                    continue;
+                }
+                double score = Double.parseDouble(entry.getValue());
+                // 注意:0值、不在分桶文件中的特征,会被过滤掉。
+                if (score > 1E-8) {
+                    if (this.bucketsMap.containsKey(name) && this.bucketsLen.containsKey(name)) {
+                        double[] buckets = this.bucketsMap.get(name);
+                        double bucketNum = this.bucketsLen.get(name);
+                        Double scoreNew = 1.0 / bucketNum * (ExtractorUtils.findInsertPosition(buckets, score) + 1.0);
+                        newFeatureMap.put(name, String.valueOf(scoreNew));
+                    } else {
+                        newFeatureMap.put(name, String.valueOf(score));
+                    }
+                }
+            } catch (Exception e) {
+                log.error("featureBucket error: ", e);
+            }
+        }
+        return newFeatureMap;
+    }
+}

+ 985 - 0
ad-engine-service/src/main/java/com/tzld/piaoquan/ad/engine/service/score/strategy/RankStrategyBy840.java

@@ -0,0 +1,985 @@
+package com.tzld.piaoquan.ad.engine.service.score.strategy;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.piaoquan.ad.engine.commons.dto.AdPlatformCreativeDTO;
+import com.tzld.piaoquan.ad.engine.commons.helper.DnnCidDataHelper;
+import com.tzld.piaoquan.ad.engine.commons.param.RankRecommendRequestParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScoreParam;
+import com.tzld.piaoquan.ad.engine.commons.score.ScorerUtils;
+import com.tzld.piaoquan.ad.engine.commons.thread.ThreadPoolFactory;
+import com.tzld.piaoquan.ad.engine.commons.util.*;
+import com.tzld.piaoquan.ad.engine.service.entity.CorrectCpaParam;
+import com.tzld.piaoquan.ad.engine.service.entity.GuaranteeView;
+import com.tzld.piaoquan.ad.engine.service.feature.Feature;
+import com.tzld.piaoquan.recommend.feature.domain.ad.base.AdRankItem;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang.math.NumberUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.xm.Similarity;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.tzld.piaoquan.ad.engine.commons.math.Const.*;
+
+@Slf4j
+@Component
+public class RankStrategyBy840 extends RankStrategyBasic {
+
+    /**
+     * 空 Map 常量,避免频繁创建空 HashMap
+     */
+    private static final Map<String, String> EMPTY_STRING_MAP = Collections.emptyMap();
+    private static final Map<String, Map<String, String>> EMPTY_NESTED_MAP = Collections.emptyMap();
+
+    private Map<String, double[]> bucketsMap = new HashMap<>();
+
+    private Map<String, Double> bucketsLen = new HashMap<>();
+
+    @Value("${word2vec.exp:694}")
+    private String word2vecExp;
+
+    @ApolloJsonValue("${rank.score.params.840:{}}")
+    private Map<String, String> paramsMap;
+
+    // FIXME(zhoutian): 可能需要独立配置
+    @ApolloJsonValue("${rank.score.weight.680:{}}")
+    private Map<String, Double> weightMap;
+
+    /**
+     * 人群分层&创意的权重
+     * 格式:{layer_creativeId: weight}
+     */
+    @ApolloJsonValue("${rank.score.weight.layer.and.creative:{}}")
+    private Map<String, Double> layerAndCreativeWeightMap;
+
+    @ApolloJsonValue("${rank.score.neg_sample_rate:0.01}")
+    Double negSampleRate;
+
+    Set<String> sparseFeatureSet;
+
+
+    @PostConstruct
+    public void afterInit() {
+        this.readBucketFile();
+        this.initSparseFeatureNames();
+    }
+
+
+    @Override
+    public List<AdRankItem> adItemRank(RankRecommendRequestParam request, ScoreParam scoreParam) {
+        Map<String, Double> weightParam = ObjUtil.nullOrDefault(weightMap, new HashMap<>());
+
+
+        Map<Long, Double> creativeScoreCoefficient = getCreativeScoreCoefficient();
+        Set<String> noApiAdVerIds = getNoApiAdVerIds();
+
+        long ts = System.currentTimeMillis() / 1000;
+
+        String brand = scoreParam.getRequestContext().getMachineinfoBrand();
+        if (StringUtils.isNotEmpty(brand)) {
+            scoreParam.getRequestContext().setMachineinfoBrand(brand + "-n");
+        }
+
+        long start = System.currentTimeMillis();
+        //过滤创意
+        filterRequestAdList(request, scoreParam);
+        // 特征处理
+        // feature1
+        Feature feature = this.getFeature(scoreParam, request);
+        if (feature == null) {
+            log.warn("adItemRank: feature is null, skip processing. request={}", request);
+            return new ArrayList<>();
+        }
+
+        Map<String, Map<String, String>> userFeature = feature.getUserFeature();
+        Map<String, Map<String, String>> videoFeature = feature.getVideoFeature();
+        Map<String, Map<String, Map<String, String>>> allAdVerFeature = feature.getAdVerFeature();
+        Map<String, Map<String, Map<String, String>>> allCidFeature = feature.getCidFeature();
+        Map<String, Map<String, Map<String, String>>> allSkuFeature = feature.getSkuFeature();
+        Map<String, String> reqFeature = this.getReqFeature(scoreParam, request);
+
+        Map<String, String> userFeatureMap = new HashMap<>();
+        Map<String, String> c1Feature = userFeature.getOrDefault("alg_mid_feature_ad_action", EMPTY_STRING_MAP);
+        List<TupleMapEntry<Tuple5>> midActionList = this.handleC1Feature(c1Feature, userFeatureMap);
+
+        Map<String, Double> midTimeDiffMap = this.parseC1FeatureListToTimeDiffMap(midActionList, ts);
+        Map<String, Double> actionStaticMap = this.parseC1FeatureListToActionStaticMap(midActionList);
+
+        Map<String, String> d2Feature = videoFeature.getOrDefault("alg_cid_feature_vid_cf_rank", EMPTY_STRING_MAP);
+        Map<String, String> d3Feature = videoFeature.getOrDefault("alg_vid_feature_basic_info", EMPTY_STRING_MAP);
+
+        Map<String, Map<String, Double>> vidRankMaps = this.parseD2FeatureMap(d2Feature);
+
+        Map<String, String> e1Feature = userFeature.getOrDefault("alg_mid_feature_return_tags", EMPTY_STRING_MAP);
+        Map<String, String> e2Feature = userFeature.getOrDefault("alg_mid_feature_share_tags", EMPTY_STRING_MAP);
+
+        Map<String, String> g1Feature = userFeature.getOrDefault("mid_return_video_cate", EMPTY_STRING_MAP);
+        Map<String, String> g2Feature = userFeature.getOrDefault("mid_share_video_cate", EMPTY_STRING_MAP);
+
+
+        userFeatureMap.put("brand", reqFeature.getOrDefault("brand", ""));
+        userFeatureMap.put("region", reqFeature.getOrDefault("region", ""));
+        userFeatureMap.put("city", reqFeature.getOrDefault("city", ""));
+        userFeatureMap.put("vid", reqFeature.getOrDefault("vid", ""));
+        userFeatureMap.put("apptype", reqFeature.getOrDefault("apptype", ""));
+        userFeatureMap.put("is_first_layer", reqFeature.getOrDefault("is_first_layer", ""));
+        userFeatureMap.put("root_source_scene", reqFeature.getOrDefault("root_source_scene", ""));
+        userFeatureMap.put("root_source_channel", reqFeature.getOrDefault("root_source_channel", ""));
+
+
+        userFeatureMap.put("cate1", d3Feature.get("merge_first_level_cate"));
+        userFeatureMap.put("cate2", d3Feature.get("merge_second_level_cate"));
+        userFeatureMap.put("user_vid_return_tags_2h", e1Feature.getOrDefault("tags_2h", null));
+        userFeatureMap.put("user_vid_return_tags_1d", e1Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_return_tags_3d", e1Feature.getOrDefault("tags_3d", null));
+        userFeatureMap.put("user_vid_return_tags_7d", e1Feature.getOrDefault("tags_7d", null));
+        userFeatureMap.put("user_vid_return_tags_14d", e1Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("title_split", d3Feature.getOrDefault("title_split", null));
+        userFeatureMap.put("user_vid_share_tags_1d", e2Feature.getOrDefault("tags_1d", null));
+        userFeatureMap.put("user_vid_share_tags_14d", e2Feature.getOrDefault("tags_14d", null));
+        userFeatureMap.put("user_vid_return_cate1_14d", g1Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_return_cate2_14d", g1Feature.getOrDefault("cate2_14d", null));
+        userFeatureMap.put("user_vid_share_cate1_14d", g2Feature.getOrDefault("cate1_14d", null));
+        userFeatureMap.put("user_vid_share_cate2_14d", g2Feature.getOrDefault("cate2_14d", null));
+
+        Map<String, String> sceneFeatureMap = this.handleSceneFeature(ts);
+        long time1 = System.currentTimeMillis();
+
+        boolean isGuaranteedFlow = getIsGuaranteedFlow(scoreParam);
+        Map<String, GuaranteeView> map = getGuaranteeViewMap(request, isGuaranteedFlow);
+        Map<Long, CorrectCpaParam> correctCpaMap = getCorrectCpaParamMap(request, scoreParam, reqFeature);
+        List<AdRankItem> adRankItems = new ArrayList<>();
+        Random random = new Random();
+        List<Future<AdRankItem>> futures = new ArrayList<>();
+        CountDownLatch cdl1 = new CountDownLatch(request.getAdIdList().size());
+        for (AdPlatformCreativeDTO dto : request.getAdIdList()) {
+            Future<AdRankItem> future = ThreadPoolFactory.feature().submit(() -> {
+                AdRankItem adRankItem = new AdRankItem();
+                try {
+                    adRankItem.setAdId(dto.getCreativeId());
+                    adRankItem.setCreativeCode(dto.getCreativeCode());
+                    adRankItem.setAdVerId(dto.getAdVerId());
+                    adRankItem.setVideoId(request.getVideoId());
+                    adRankItem.setCpa(dto.getCpa());
+                    adRankItem.setId(dto.getAdId());
+                    adRankItem.setCampaignId(dto.getCampaignId());
+                    adRankItem.setCpm(ObjUtil.nullOrDefault(dto.getCpm(), 90).doubleValue());
+                    adRankItem.setSkuId(dto.getSkuId());
+                    adRankItem.setCustomerId(dto.getCustomerId());
+                    adRankItem.setProfession(dto.getProfession());
+                    adRankItem.setRandom(random.nextInt(1000));
+                    if (noApiAdVerIds.contains(dto.getAdVerId())) {
+                        adRankItem.getExt().put("isApi", "0");
+                    } else {
+                        adRankItem.getExt().put("isApi", "1");
+                    }
+                    adRankItem.getExt().put("recallsources", dto.getRecallSources());
+                    adRankItem.getExt().put("correctCpaMap", JSONObject.toJSONString(correctCpaMap.get(dto.getAdId())));
+                    adRankItem.getExt().put("correctionFactor", correctCpaMap.get(dto.getAdId()).getCorrectionFactor());
+                    setGuaranteeWeight(map, dto.getAdVerId(), adRankItem.getExt(), isGuaranteedFlow, reqFeature);
+                    String cidStr = dto.getCreativeId().toString();
+                    Map<String, String> cidFeatureMap = adRankItem.getFeatureMap();
+                    Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+                    Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+
+                    Map<String, Map<String, String>> adVerFeature = allAdVerFeature.getOrDefault(dto.getAdVerId(), EMPTY_NESTED_MAP);
+                    Map<String, Map<String, String>> skuFeature = allSkuFeature.getOrDefault(String.valueOf(dto.getSkuId()), EMPTY_NESTED_MAP);
+                    Map<String, String> d1Feature = cidFeature.getOrDefault("alg_cid_feature_vid_cf", EMPTY_STRING_MAP);
+
+                    this.handleB1Feature(b1Feature, cidFeatureMap, cidStr);
+                    this.handleB2ToB5AndB8ToB9Feature(cidFeature, adVerFeature, cidFeatureMap);
+                    this.handleB6ToB7Feature(cidFeature, cidFeatureMap);
+                    this.handleC1UIFeature(midTimeDiffMap, actionStaticMap, cidFeatureMap, cidStr);
+                    this.handleD1Feature(d1Feature, cidFeatureMap);
+                    this.handleD2Feature(vidRankMaps, cidFeatureMap, cidStr);
+                    this.handleH1AndH2Feature(skuFeature, adVerFeature, cidFeatureMap);
+                    cidFeatureMap.put("cid", dto.getCreativeId() != null ? String.valueOf(dto.getCreativeId()) : "");
+                    cidFeatureMap.put("adid", dto.getAdId() != null ? String.valueOf(dto.getAdId()) : "");
+                    cidFeatureMap.put("adverid", dto.getAdVerId() != null ? dto.getAdVerId() : "");
+                    cidFeatureMap.put("profession", dto.getProfession() != null ? dto.getProfession() : "");
+                    cidFeatureMap.put("category_name", dto.getCategoryName() != null ? dto.getCategoryName() : "");
+                    cidFeatureMap.put("material_md5", dto.getMaterialMd5() != null ? dto.getMaterialMd5() : "");
+                    //DNN模型没训练过的cid才不传入广告相关的稀疏特征
+                    if (CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSetV2()) && !DnnCidDataHelper.getCidSetV2().contains(adRankItem.getAdId())) {
+                        cidFeatureMap.put("cid", "");
+                        cidFeatureMap.put("adid", "");
+                        cidFeatureMap.put("adverid", "");
+                    }
+                    return adRankItem;
+                } finally {
+                    cdl1.countDown();
+                }
+            });
+            futures.add(future);
+        }
+        try {
+            cdl1.await(300, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        for (Future<AdRankItem> future : futures) {
+            try {
+                if (future.isDone()) {
+                    adRankItems.add(future.get());
+                }
+            } catch (Exception e) {
+                log.error("Feature handle error", e);
+            }
+        }
+
+        long time2 = System.currentTimeMillis();
+        // feature3
+        // 请求级别的 tag 分词缓存,所有广告共享(同一用户的 tags 相同)
+        Map<String, List<String>> tagWordsCache = new ConcurrentHashMap<>();
+        CountDownLatch cdl2 = new CountDownLatch(adRankItems.size() * 2);
+        for (AdRankItem item : adRankItems) {
+            String cidStr = String.valueOf(item.getAdId());
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(cidStr, EMPTY_NESTED_MAP);
+            Map<String, String> b1Feature = cidFeature.getOrDefault("alg_cid_feature_basic_info", EMPTY_STRING_MAP);
+            String title = b1Feature.getOrDefault("cidtitle", "");
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleE1AndE2Feature(e1Feature, e2Feature, title, item.getFeatureMap(), scoreParam, tagWordsCache);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+            ThreadPoolFactory.defaultPool().submit(() -> {
+                try {
+                    this.handleD3AndB1Feature(d3Feature, title, item.getFeatureMap(), scoreParam);
+                } finally {
+                    cdl2.countDown();
+                }
+            });
+        }
+        try {
+            cdl2.await(150, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+
+        long time3 = System.currentTimeMillis();
+        // 分桶
+        userFeatureMap = this.featureBucket(userFeatureMap);
+        CountDownLatch cdl4 = new CountDownLatch(adRankItems.size());
+        for (AdRankItem adRankItem : adRankItems) {
+            ThreadPoolFactory.feature().submit(() -> {
+                try {
+                    Map<String, String> featureMap = adRankItem.getFeatureMap();
+                    adRankItem.setFeatureMap(this.featureBucket(featureMap));
+                } finally {
+                    cdl4.countDown();
+                }
+            });
+        }
+        try {
+            cdl4.await(100, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            log.error("handleE1AndE2Feature and handleD3AndB1Feature wait timeout", e);
+        }
+        long time4 = System.currentTimeMillis();
+        // 打分排序
+        // getScorerPipeline
+
+        if (CollectionUtils.isEmpty(adRankItems)) {
+            log.error("adRankItems is empty");
+        }
+        List<AdRankItem> result = ScorerUtils.getScorerPipeline(ScorerUtils.PAI_SCORE_CONF_20250804).scoring(sceneFeatureMap, userFeatureMap, adRankItems);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("scoring result is empty");
+        }
+        long time5 = System.currentTimeMillis();
+        int viewLimit = NumberUtils.toInt(paramsMap.getOrDefault("viewLimit", "3000"));
+        // calibrate score for negative sampling or cold start
+        for (AdRankItem item : result) {
+            double originalScore = item.getLrScore();
+            double calibratedScore = originalScore / (originalScore + (1 - originalScore) / negSampleRate);
+            // 该创意尚未在模型中训练,打分不可靠
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+            Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+            double view3Day = Double.parseDouble(b3Feature.getOrDefault("ad_view_3d", "0"));
+            if ((CollectionUtils.isNotEmpty(DnnCidDataHelper.getCidSetV2()) && !DnnCidDataHelper.getCidSetV2().contains(item.getAdId()))
+                    || view3Day <= viewLimit) {
+                double view = Double.parseDouble(b3Feature.getOrDefault("ad_view_14d", "0"));
+                double conver = Double.parseDouble(b3Feature.getOrDefault("ad_conversion_14d", "0"));
+                double smoothCxr = NumUtil.divSmoothV1(conver, view, 1.64);
+                //模型打分和统计计算取打分更低的
+                item.getScoreMap().put("cvcvrItemValue", 1.0);
+                if (smoothCxr <= calibratedScore) {
+                    calibratedScore = smoothCxr;
+                    item.getScoreMap().put("cvcvrItemValue", 2.0);
+                }
+            }
+            item.setLrScore(calibratedScore);
+            item.getScoreMap().put("originCtcvrScore", originalScore);
+            item.getScoreMap().put("modelCtcvrScore", calibratedScore);
+            item.getScoreMap().put("ctcvrScore", calibratedScore);
+        }
+
+        String calibModelName = paramsMap.getOrDefault("calibModelName", "dnnV3");
+        calculateCtcvrScore(result, request, scoreParam, calibModelName, reqFeature);
+        if (CollectionUtils.isEmpty(result)) {
+            log.error("calculateCtcvrScore result is empty");
+        }
+        // loop
+        double cpmCoefficient = weightParam.getOrDefault("cpmCoefficient", 0.9);
+        boolean isGuaranteeType = false;
+        // 查询人群分层信息
+        String peopleLayer = Optional.of(reqFeature)
+                .map(f -> f.get("layer"))
+                .map(s -> s.replace("-炸", ""))
+                .orElse(null);
+
+        // 控制曝光参数
+        String expOldKey = paramsMap.getOrDefault("expOldKey", "ad_view_yesterday");
+        double expOldThreshold = NumberUtils.toDouble(paramsMap.getOrDefault("expOldThreshold", "1000"));
+        String expNewKey = paramsMap.getOrDefault("expNewKey", "ad_view_today");
+        double expNewThreshold = NumberUtils.toDouble(paramsMap.getOrDefault("expNewThreshold", "3000"));
+        double expLowerWeight = NumberUtils.toDouble(paramsMap.getOrDefault("expLowerWeight", "0.2"));
+        double expUpperWeight = NumberUtils.toDouble(paramsMap.getOrDefault("expUpperWeight", "1.0"));
+        double expScale = NumberUtils.toDouble(paramsMap.getOrDefault("expScale", "10.0"));
+        for (AdRankItem item : result) {
+            double bid = item.getCpa();
+            if (scoreParam.getExpCodeSet().contains(correctCpaExp1) || scoreParam.getExpCodeSet().contains(correctCpaExp2)) {
+                Double correctionFactor = (Double) item.getExt().get("correctionFactor");
+                item.getScoreMap().put("correctionFactor", correctionFactor);
+                bid = bid * correctionFactor;
+            }
+            item.getScoreMap().put("ecpm", item.getLrScore() * bid * 1000);
+            if (isGuaranteedFlow && item.getExt().get("isGuaranteed") != null && (boolean) item.getExt().get("isGuaranteed")) {
+                isGuaranteeType = true;
+            }
+
+            // 控制曝光权重
+            Map<String, Map<String, String>> cidFeature = allCidFeature.getOrDefault(String.valueOf(item.getAdId()), EMPTY_NESTED_MAP);
+            Map<String, String> b3Feature = cidFeature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+            double expWeight = getExpWeight(b3Feature,
+                    expOldKey, expOldThreshold,
+                    expNewKey, expNewThreshold,
+                    expLowerWeight, expUpperWeight, expScale);
+
+            String layerAndCreativeWeightMapKey = getLayerAndCreativeWeightMapKey(peopleLayer, String.valueOf(item.getAdId()));
+            // 人群分层&创意的权重
+            double layerAndCreativeWeight = getLayerAndCreativeWeight(layerAndCreativeWeightMapKey);
+            double scoreCoefficient = creativeScoreCoefficient.getOrDefault(item.getAdId(), 1d);
+            double guaranteeScoreCoefficient = getGuaranteeScoreCoefficient(isGuaranteedFlow, item.getExt());
+            double score = expWeight * item.getLrScore() * bid * scoreCoefficient * guaranteeScoreCoefficient * layerAndCreativeWeight;
+            item.getScoreMap().put("guaranteeScoreCoefficient", guaranteeScoreCoefficient);
+            item.getScoreMap().put("cpa", item.getCpa());
+            item.getScoreMap().put("cpm", item.getCpm());
+            item.getScoreMap().put("bid", bid);
+            item.getScoreMap().put("cpmCoefficient", cpmCoefficient);
+            item.getScoreMap().put("scoreCoefficient", scoreCoefficient);
+            item.getFeatureMap().putAll(userFeatureMap);
+            item.getFeatureMap().putAll(sceneFeatureMap);
+
+            // 没有转化回传的广告主,使用后台配置的CPM
+            if (noApiAdVerIds.contains(item.getAdVerId())) {
+                score = item.getCpm() * cpmCoefficient / 1000;
+            }
+            item.setScore(score);
+        }
+
+
+        result.sort(ComparatorUtil.equalsRandomComparator());
+
+        String logModelName = paramsMap.getOrDefault("logModelName", "dnnV3");
+        if (CollectionUtils.isNotEmpty(result)) {
+            AdRankItem top1Item = result.get(0);
+            List<String> participateCompetitionType = new ArrayList<>();
+            participateCompetitionType.add("engine");
+            top1Item.getExt().put("isGuaranteeType", isGuaranteeType);
+            if (isGuaranteeType) {
+                participateCompetitionType.add("guarantee");
+            }
+            top1Item.getExt().put("participateCompetitionType", StringUtils.join(participateCompetitionType, ","));
+            Double modelCtcvrScore = top1Item.getScoreMap().get("modelCtcvrScore");
+            Double ctcvrScore = top1Item.getScoreMap().get("ctcvrScore");
+            if (scoreParam.getExpCodeSet().contains(checkoutEcpmExp)) {
+                top1Item.getExt().put("ecpm", ctcvrScore * top1Item.getCpa() * 1000);
+                String filterEcpmValue = paramsMap.getOrDefault("filterEcpm", filterEcpm);
+                top1Item.getExt().put("filterEcpm", filterEcpmValue);
+            } else {
+                top1Item.getExt().put("ecpm", modelCtcvrScore * top1Item.getCpa() * 1000);
+            }
+            putMetaFeature(top1Item, feature, reqFeature, sceneFeatureMap, request);
+            top1Item.getExt().put("model", logModelName);
+            String coefficientRate = paramsMap.getOrDefault("coefficientRate", "1");
+            top1Item.getExt().put("coefficientRate", coefficientRate);
+        }
+        long time6 = System.currentTimeMillis();
+        log.info("cost={}, getFeature={}, handleFeature={},  similar={}, bucketFeature={}, getScorerPipeline={}, " +
+                        "other={}, adIdSize={}, adRankItemsSize={}",
+                time6 - start, time1 - start, time2 - time1, time3 - time2, time4 - time3,
+                time5 - time4, time6 - time5, request.getAdIdList().size(), adRankItems.size());
+
+        return result;
+    }
+
+    /**
+     * 获取人群分层和创意的权重
+     *
+     * @param key
+     * @return
+     */
+    private Double getLayerAndCreativeWeight(String key) {
+        if (StringUtils.isBlank(key)) {
+            return 1d;
+        }
+        return layerAndCreativeWeightMap.getOrDefault(key, 1d);
+    }
+
+    /**
+     * 获取人群分层和创意的权重key
+     *
+     * @param layer
+     * @param creativeId
+     * @return
+     */
+    private String getLayerAndCreativeWeightMapKey(String layer, String creativeId) {
+        if (StringUtils.isBlank(layer) || StringUtils.isBlank(creativeId)) {
+            return null;
+        }
+        return layer + "_" + creativeId;
+    }
+
+
+    private void handleB1Feature(Map<String, String> b1Feature, Map<String, String> cidFeatureMap, String cid) {
+        cidFeatureMap.put("cid_" + cid, "0.1");
+        // if (StringUtils.isNotBlank(b1Feature.get("adid"))) {
+        //     String adId = b1Feature.get("adid");
+        //     cidFeatureMap.put("adid_" + adId, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("adverid"))) {
+            String adVerId = b1Feature.get("adverid");
+            cidFeatureMap.put("adverid_" + adVerId, "0.1");
+        }
+        // if (StringUtils.isNotBlank(b1Feature.get("targeting_conversion"))) {
+        //     String targetingConversion = b1Feature.get("targeting_conversion");
+        //     cidFeatureMap.put("targeting_conversion_" + targetingConversion, idDefaultValue);
+        // }
+        if (StringUtils.isNotBlank(b1Feature.get("cpa"))) {
+            String cpa = b1Feature.get("cpa");
+            cidFeatureMap.put("cpa", cpa);
+        }
+    }
+
+    private void handleB2ToB5AndB8ToB9Feature(Map<String, Map<String, String>> c1Feature, Map<String, Map<String, String>> adVerFeature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b2Feature = adVerFeature.getOrDefault("alg_cid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> b3Feature = c1Feature.getOrDefault("alg_cid_feature_cid_action", EMPTY_STRING_MAP);
+        Map<String, String> b4Feature = c1Feature.getOrDefault("alg_cid_feature_region_action", EMPTY_STRING_MAP);
+        Map<String, String> b5Feature = c1Feature.getOrDefault("alg_cid_feature_app_action", EMPTY_STRING_MAP);
+        Map<String, String> b8Feature = c1Feature.getOrDefault("alg_cid_feature_brand_action", EMPTY_STRING_MAP);
+        Map<String, String> b9Feature = c1Feature.getOrDefault("alg_cid_feature_weChatVersion_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("1h", "2h", "3h", "6h", "12h", "1d", "3d", "7d", "yesterday", "today");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b2Feature, "b2"),
+                new Tuple2<>(b3Feature, "b3"),
+                new Tuple2<>(b4Feature, "b4"),
+                new Tuple2<>(b5Feature, "b5"),
+                new Tuple2<>(b8Feature, "b8"),
+                new Tuple2<>(b9Feature, "b9")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private void handleB6ToB7Feature(Map<String, Map<String, String>> c1Feature, Map<String, String> cidFeatureMap) {
+        Map<String, String> b6Feature = c1Feature.getOrDefault("alg_cid_feature_week_action", EMPTY_STRING_MAP);
+        Map<String, String> b7Feature = c1Feature.getOrDefault("alg_cid_feature_hour_action", EMPTY_STRING_MAP);
+
+        List<String> timeList = Arrays.asList("7d", "14d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(b6Feature, "b6"),
+                new Tuple2<>(b7Feature, "b7")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                double view = Double.parseDouble(feature.getOrDefault("ad_view_" + time, "0"));
+                double click = Double.parseDouble(feature.getOrDefault("ad_click_" + time, "0"));
+                double conver = Double.parseDouble(feature.getOrDefault("ad_conversion_" + time, "0"));
+                double income = Double.parseDouble(feature.getOrDefault("ad_income_" + time, "0"));
+                double cpc = NumUtil.div(income, click);
+                double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+                double ctcvr = NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR);
+                double ecpm = ctr * cpc * 1000;
+                cidFeatureMap.put(prefix + "_" + time + "_ctr", String.valueOf(ctr));
+                cidFeatureMap.put(prefix + "_" + time + "_ctcvr", String.valueOf(ctcvr));
+                cidFeatureMap.put(prefix + "_" + time + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver", String.valueOf(conver));
+                cidFeatureMap.put(prefix + "_" + time + "_ecpm", String.valueOf(ecpm));
+
+                cidFeatureMap.put(prefix + "_" + time + "_click", String.valueOf(click));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*log(view)", String.valueOf(conver * NumUtil.log(view)));
+                cidFeatureMap.put(prefix + "_" + time + "_conver*ctcvr", String.valueOf(conver * ctcvr));
+            }
+        }
+
+    }
+
+    private List<TupleMapEntry<Tuple5>> handleC1Feature(Map<String, String> c1Feature, Map<String, String> featureMap) {
+
+        //用户近1年内是否有转化
+        if (c1Feature.containsKey("user_has_conver_1y") && c1Feature.get("user_has_conver_1y") != null) {
+            featureMap.put("user_has_conver_1y", c1Feature.get("user_has_conver_1y"));
+        }
+        //用户历史转化过品类
+        if (c1Feature.containsKey("user_conver_ad_class") && c1Feature.get("user_conver_ad_class") != null) {
+            featureMap.put("user_conver_ad_class", c1Feature.get("user_conver_ad_class"));
+        }
+
+        // 用户特征
+        List<TupleMapEntry<Tuple5>> midActionList = new ArrayList<>();
+        if (c1Feature.containsKey("action")) {
+            String action = c1Feature.get("action");
+            midActionList = Arrays.stream(action.split(","))
+                    .map(r -> {
+                        String[] rList = r.split(":");
+                        Tuple5 tuple5 = new Tuple5(rList[1], rList[2], rList[3], rList[4], rList[5]);
+                        return new TupleMapEntry<>(rList[0], tuple5);
+                    })
+                    // TODO 倒排
+                    .sorted((a, b) -> Integer.compare(Integer.parseInt(b.value.f1), Integer.parseInt(a.value.f1)))
+                    .collect(Collectors.toList());
+        }
+
+        double viewAll = midActionList.size();
+        double clickAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f2)).sum();
+        double converAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f3)).sum();
+        double incomeAll = midActionList.stream().mapToInt(e -> Integer.parseInt(e.value.f4)).sum();
+        featureMap.put("viewAll", String.valueOf(viewAll));
+        featureMap.put("clickAll", String.valueOf(clickAll));
+        featureMap.put("converAll", String.valueOf(converAll));
+        featureMap.put("incomeAll", String.valueOf(incomeAll));
+        featureMap.put("ctr_all", String.valueOf(NumUtil.div(clickAll, viewAll)));
+        featureMap.put("ctcvr_all", String.valueOf(NumUtil.div(converAll, viewAll)));
+        featureMap.put("cvr_all", String.valueOf(NumUtil.div(clickAll, converAll)));
+        featureMap.put("ecpm_all", String.valueOf(NumUtil.div(incomeAll * 1000, viewAll)));
+        if (CollectionUtils.isNotEmpty(midActionList)) {
+            List<String> cidClickList = new ArrayList<>();
+            List<String> cidConverList = new ArrayList<>();
+            for (TupleMapEntry<Tuple5> tupleMapEntry : midActionList) {
+                String cid = tupleMapEntry.key;
+                String click = tupleMapEntry.value.f2;
+                String conver = tupleMapEntry.value.f3;
+                if (Objects.equals(click, "1")) {
+                    cidClickList.add(cid);
+                }
+                if (Objects.equals(conver, "1")) {
+                    cidConverList.add(cid);
+                }
+            }
+            featureMap.put("user_cid_click_list", String.join(",", cidClickList));
+            featureMap.put("user_cid_conver_list", String.join(",", cidConverList));
+        }
+        return midActionList;
+    }
+
+    private void handleC1UIFeature(Map<String, Double> midTimeDiffMap, Map<String, Double> midActionStatic, Map<String, String> featureMap, String cid) {
+        if (midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+            featureMap.put("timediff_view", String.valueOf(midTimeDiffMap.getOrDefault("timediff_view_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_click_" + cid)) {
+            featureMap.put("timediff_click", String.valueOf(midTimeDiffMap.getOrDefault("timediff_click_" + cid, 0.0)));
+        }
+        if (midTimeDiffMap.containsKey("timediff_conver_" + cid)) {
+            featureMap.put("timediff_conver", String.valueOf(midTimeDiffMap.getOrDefault("timediff_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid)) {
+            featureMap.put("actionstatic_view", String.valueOf(midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            featureMap.put("actionstatic_click", String.valueOf(midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            featureMap.put("actionstatic_conver", String.valueOf(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_income_" + cid)) {
+            featureMap.put("actionstatic_income", String.valueOf(midActionStatic.getOrDefault("actionstatic_income_" + cid, 0.0)));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double ctr = NumUtil.div(
+                    midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0),
+                    midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0)
+            );
+            featureMap.put("actionstatic_ctr", String.valueOf(ctr));
+        }
+        if (midActionStatic.containsKey("actionstatic_view_" + cid) && midActionStatic.containsKey("actionstatic_conver_" + cid)) {
+            double ctcvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_view_" + cid, 0.0));
+            featureMap.put("actionstatic_ctcvr", String.valueOf(ctcvr));
+        }
+        if (midActionStatic.containsKey("actionstatic_conver_" + cid) && midActionStatic.containsKey("actionstatic_click_" + cid)) {
+            double cvr = NumUtil.div(midActionStatic.getOrDefault("actionstatic_conver_" + cid, 0.0), midActionStatic.getOrDefault("actionstatic_click_" + cid, 0.0));
+            featureMap.put("actionstatic_cvr", String.valueOf(cvr));
+        }
+    }
+
+    private void handleD1Feature(Map<String, String> d1Feature, Map<String, String> featureMap) {
+        for (String prefix : Arrays.asList("3h", "6h", "12h", "1d", "3d", "7d")) {
+            double view = Double.parseDouble(d1Feature.getOrDefault("ad_view_" + prefix, "0"));
+            double click = Double.parseDouble(d1Feature.getOrDefault("ad_click_" + prefix, "0"));
+            double conver = Double.parseDouble(d1Feature.getOrDefault("ad_conversion_" + prefix, "0"));
+            double income = Double.parseDouble(d1Feature.getOrDefault("ad_income_" + prefix, "0"));
+            double cpc = NumUtil.div(income, click);
+            double ctr = NumUtil.divSmoothV2(click, view, CTR_SMOOTH_BETA_FACTOR);
+            featureMap.put("d1_feature_" + prefix + "_ctr", String.valueOf(ctr));
+            featureMap.put("d1_feature_" + prefix + "_ctcvr", String.valueOf(NumUtil.divSmoothV2(conver, view, CTCVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_cvr", String.valueOf(NumUtil.divSmoothV2(conver, click, CVR_SMOOTH_BETA_FACTOR)));
+            featureMap.put("d1_feature_" + prefix + "_conver", String.valueOf(conver));
+            featureMap.put("d1_feature_" + prefix + "_ecpm", String.valueOf(ctr * cpc * 1000));
+        }
+    }
+
+    private void handleD2Feature(Map<String, Map<String, Double>> vidRankMaps, Map<String, String> featureMap, String cid) {
+        if (MapUtils.isEmpty(vidRankMaps)) {
+            return;
+        }
+
+        List<String> prefixes1 = Arrays.asList("ctr", "ctcvr", "ecpm");
+        // List<String> prefixes1 = Arrays.asList("ctr", "ctcvr");
+        List<String> prefixes2 = Arrays.asList("1d", "3d", "7d", "14d");
+
+        for (String prefix1 : prefixes1) {
+            for (String prefix2 : prefixes2) {
+                String combinedKey = prefix1 + "_" + prefix2;
+                if (vidRankMaps.containsKey(combinedKey)) {
+                    Double rank = vidRankMaps.get(combinedKey).getOrDefault(cid, 0.0);
+                    if (rank >= 1.0) {
+                        featureMap.put("vid_rank_" + combinedKey, String.valueOf(NumUtil.div(1, rank)));
+                    }
+                }
+            }
+        }
+    }
+
+    private void handleH1AndH2Feature(Map<String, Map<String, String>> skuFeature,
+                                      Map<String, Map<String, String>> adVerFeature,
+                                      Map<String, String> cidFeatureMap) {
+        Map<String, String> h1Feature = adVerFeature.getOrDefault("alg_mid_feature_adver_action", EMPTY_STRING_MAP);
+        Map<String, String> h2Feature = skuFeature.getOrDefault("alg_mid_feature_sku_action", EMPTY_STRING_MAP);
+        List<String> timeList = Arrays.asList("3d", "7d", "30d");
+        List<Tuple2<Map<String, String>, String>> featureList = Arrays.asList(
+                new Tuple2<>(h1Feature, "adverid"),
+                new Tuple2<>(h2Feature, "skuid")
+        );
+        for (Tuple2<Map<String, String>, String> tuple2 : featureList) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            for (String time : timeList) {
+                String timeValue = feature.getOrDefault(time, "");
+                if (StringUtils.isNotEmpty(timeValue)) {
+                    String[] split = timeValue.split(",");
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "view" + "_" + time, split[0]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "click" + "_" + time, split[1]);
+                    cidFeatureMap.put("user" + "_" + prefix + "_" + "conver" + "_" + time, split[2]);
+                }
+            }
+        }
+
+
+    }
+
+    private void handleD3AndB1Feature(Map<String, String> d3Feature, String cTitle, Map<String, String> featureMap,
+                                      ScoreParam scoreParam) {
+        if (MapUtils.isEmpty(d3Feature) || !d3Feature.containsKey("title") || StringUtils.isEmpty(cTitle)) {
+            return;
+        }
+        String vTitle = d3Feature.get("title");
+        double score;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            score = SimilarityUtils.word2VecSimilarity(cTitle, vTitle);
+        } else {
+            score = Similarity.conceptSimilarity(cTitle, vTitle);
+        }
+        featureMap.put("ctitle_vtitle_similarity", String.valueOf(score));
+    }
+
+    private void handleE1AndE2Feature(Map<String, String> e1Feature, Map<String, String> e2Feature, String title,
+                                      Map<String, String> featureMap, ScoreParam scoreParam,
+                                      Map<String, List<String>> tagWordsCache) {
+        if (StringUtils.isEmpty(title)) {
+            return;
+        }
+
+        // 预先分词 title,在整个方法中复用,避免重复分词
+        List<String> titleWords = null;
+        if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+            titleWords = SimilarityUtils.segment(title);
+        }
+
+        List<Tuple2<Map<String, String>, String>> tuple2List = Arrays.asList(new Tuple2<>(e1Feature, "e1"), new Tuple2<>(e2Feature, "e2"));
+
+        List<String> tagsFieldList = Arrays.asList("tags_3d", "tags_7d", "tags_14d");
+        for (Tuple2<Map<String, String>, String> tuple2 : tuple2List) {
+            Map<String, String> feature = tuple2.f1;
+            String prefix = tuple2.f2;
+            if (MapUtils.isEmpty(feature)) {
+                continue;
+            }
+
+            for (String tagsField : tagsFieldList) {
+                if (StringUtils.isNotEmpty(feature.get(tagsField))) {
+                    String tags = feature.get(tagsField);
+                    Double[] doubles;
+                    if (scoreParam.getExpCodeSet().contains(word2vecExp)) {
+                        // 使用缓存的 title 分词结果和请求级别的 tag 分词缓存
+                        doubles = ExtractorUtils.funcC34567ForTagsNewWithCache(tags, title, titleWords, tagWordsCache);
+                    } else {
+                        doubles = ExtractorUtils.funcC34567ForTags(tags, title);
+                    }
+                    featureMap.put(prefix + "_" + tagsField + "_matchnum", String.valueOf(doubles[0]));
+                    featureMap.put(prefix + "_" + tagsField + "_maxscore", String.valueOf(doubles[1]));
+                    featureMap.put(prefix + "_" + tagsField + "_avgscore", String.valueOf(doubles[2]));
+                }
+            }
+        }
+    }
+
+    private Map<String, Double> parseC1FeatureListToTimeDiffMap(List<TupleMapEntry<Tuple5>> midActionList, long ts) {
+        Map<String, Double> midTimeDiffMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double tsHistory = Double.parseDouble(entry.value.f1);
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double d = (ts - tsHistory) / 3600 / 24;
+            if (!midTimeDiffMap.containsKey("timediff_view_" + cid)) {
+                midTimeDiffMap.put("timediff_view_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_click_" + cid) && click > 0) {
+                midTimeDiffMap.put("timediff_click_" + cid, NumUtil.div(1, d));
+            }
+            if (!midTimeDiffMap.containsKey("timediff_conver_" + cid) && conver > 0) {
+                midTimeDiffMap.put("timediff_conver_" + cid, NumUtil.div(1, d));
+            }
+        }
+        return midTimeDiffMap;
+    }
+
+    private Map<String, Double> parseC1FeatureListToActionStaticMap(List<TupleMapEntry<Tuple5>> midActionList) {
+        Map<String, Double> midActionStaticsMap = new HashMap<>();
+        for (TupleMapEntry<Tuple5> entry : midActionList) {
+            String cid = entry.key;
+            double click = Double.parseDouble(entry.value.f2);
+            double conver = Double.parseDouble(entry.value.f3);
+            double income = Double.parseDouble(entry.value.f4);
+
+            Double viewSum = midActionStaticsMap.getOrDefault("actionstatic_view_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_view_" + cid, 1 + viewSum);
+
+            Double clickSum = midActionStaticsMap.getOrDefault("actionstatic_click_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_click_" + cid, clickSum + click);
+
+            Double converSum = midActionStaticsMap.getOrDefault("actionstatic_conver_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_conver_" + cid, converSum + conver);
+
+            Double incomSum = midActionStaticsMap.getOrDefault("actionstatic_income_" + cid, 0.0);
+            midActionStaticsMap.put("actionstatic_income_" + cid, incomSum + income);
+        }
+
+        return midActionStaticsMap;
+    }
+
+    private Map<String, Map<String, Double>> parseD2FeatureMap(Map<String, String> d2Feature) {
+        Map<String, Map<String, Double>> vidRankMaps = new HashMap<>();
+        for (Map.Entry<String, String> entry : d2Feature.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            Map<String, Double> valueMap = Arrays.stream(value.split(",")).map(r -> r.split(":")).collect(Collectors.toMap(rList -> rList[0], rList -> Double.parseDouble(rList[2])));
+            vidRankMaps.put(key, valueMap);
+        }
+        return vidRankMaps;
+    }
+
+    private void readBucketFile() {
+        if (MapUtils.isNotEmpty(bucketsMap)) {
+            return;
+        }
+        synchronized (this) {
+            String bucketFile = "20250217_ad_bucket_688.txt";
+            InputStream resourceStream = this.getClass().getClassLoader().getResourceAsStream(bucketFile);
+            if (resourceStream != null) {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
+                    Map<String, double[]> bucketsMap = new HashMap<>();
+                    Map<String, Double> bucketsLen = new HashMap<>();
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        // 替换空格和换行符,过滤空行
+                        line = line.replace(" ", "").replaceAll("\n", "");
+                        if (!line.isEmpty()) {
+                            String[] rList = line.split("\t");
+                            if (rList.length == 3) {
+                                String key = rList[0];
+                                double value1 = Double.parseDouble(rList[1]);
+                                bucketsLen.put(key, value1);
+                                double[] value2 = Arrays.stream(rList[2].split(",")).mapToDouble(Double::valueOf).toArray();
+                                bucketsMap.put(key, value2);
+                            }
+                        }
+                    }
+                    this.bucketsMap = bucketsMap;
+                    this.bucketsLen = bucketsLen;
+                } catch (IOException e) {
+                    log.error("something is wrong in parse bucket file: ", e);
+                }
+                log.info("load bucket file success: {}", bucketFile);
+            } else {
+                log.error("no bucket file");
+            }
+        }
+    }
+
+    private void initSparseFeatureNames() {
+        this.sparseFeatureSet = new HashSet<String>() {{
+            add("brand");
+            add("region");
+            add("city");
+            add("vid");
+            add("cate1");
+            add("cate2");
+            add("cid");
+            add("adid");
+            add("adverid");
+            add("user_cid_click_list");
+            add("user_cid_conver_list");
+            add("user_vid_return_tags_2h");
+            add("user_vid_return_tags_1d");
+            add("user_vid_return_tags_3d");
+            add("user_vid_return_tags_7d");
+            add("user_vid_return_tags_14d");
+            add("apptype");
+            add("hour");
+            add("hour_quarter");
+            add("root_source_scene");
+            add("root_source_channel");
+            add("is_first_layer");
+            add("title_split");
+            add("profession");
+            add("user_vid_share_tags_1d");
+            add("user_vid_share_tags_14d");
+            add("user_vid_return_cate1_14d");
+            add("user_vid_return_cate2_14d");
+            add("user_vid_share_cate1_14d");
+            add("user_vid_share_cate2_14d");
+            add("user_has_conver_1y");
+            add("user_adverid_view_3d");
+            add("user_adverid_click_3d");
+            add("user_adverid_conver_3d");
+            add("user_adverid_view_7d");
+            add("user_adverid_click_7d");
+            add("user_adverid_conver_7d");
+            add("user_adverid_view_30d");
+            add("user_adverid_click_30d");
+            add("user_adverid_conver_30d");
+            add("user_skuid_view_3d");
+            add("user_skuid_click_3d");
+            add("user_skuid_conver_3d");
+            add("user_skuid_view_7d");
+            add("user_skuid_click_7d");
+            add("user_skuid_conver_7d");
+            add("user_skuid_view_30d");
+            add("user_skuid_click_30d");
+            add("user_skuid_conver_30d");
+            add("user_conver_ad_class");
+            add("category_name");
+            add("material_md5");
+        }};
+    }
+
+    private Map<String, String> featureBucket(Map<String, String> featureMap) {
+        // 使用 HashMap 替代 ConcurrentHashMap,分桶操作是单线程的
+        Map<String, String> newFeatureMap = new HashMap<>(featureMap.size());
+        for (Map.Entry<String, String> entry : featureMap.entrySet()) {
+            try {
+                String name = entry.getKey();
+                if (this.sparseFeatureSet.contains(name)) {
+                    if (entry.getValue() != null) {
+                        newFeatureMap.put(name, entry.getValue());
+                    }
+                    continue;
+                }
+                double score = Double.parseDouble(entry.getValue());
+                // 注意:0值、不在分桶文件中的特征,会被过滤掉。
+                if (score > 1E-8) {
+                    if (this.bucketsMap.containsKey(name) && this.bucketsLen.containsKey(name)) {
+                        double[] buckets = this.bucketsMap.get(name);
+                        double bucketNum = this.bucketsLen.get(name);
+                        Double scoreNew = 1.0 / bucketNum * (ExtractorUtils.findInsertPosition(buckets, score) + 1.0);
+                        newFeatureMap.put(name, String.valueOf(scoreNew));
+                    } else {
+                        newFeatureMap.put(name, String.valueOf(score));
+                    }
+                }
+            } catch (Exception e) {
+                log.error("featureBucket error: ", e);
+            }
+        }
+        return newFeatureMap;
+    }
+
+    private double getExpWeight(Map<String, String> featureMap,
+                                String expOldKey, double expOldThreshold,
+                                String expNewKey, double expNewThreshold,
+                                double expLowerWeight, double expUpperWeight, double expScale) {
+        try {
+            if (null != featureMap) {
+                double oldView = Double.parseDouble(featureMap.getOrDefault(expOldKey, "0"));
+                if (oldView < expOldThreshold) {
+                    double newView = Double.parseDouble(featureMap.getOrDefault(expNewKey, "0"));
+                    return getExpWeight(expLowerWeight, expUpperWeight, expScale, expNewThreshold, newView);
+                }
+            }
+        } catch (Exception e) {
+            log.error("getExpWeight error: ", e);
+        }
+        return 1.0;
+    }
+
+    private double getExpWeight(double lowerWeight, double upperWeight, double scale, double upperExp, double exp) {
+        if (exp >= upperExp) {
+            return 1.0;
+        }
+        double weight = Math.log(exp + 1) / scale;
+        return Math.min(Math.max(lowerWeight, weight), upperWeight);
+    }
+}